Merge branch 'main' into signing-test
Some checks failed
CI (Debug) / build (push) Has been cancelled
CI (Release) / build (push) Has been cancelled
CI (Release) / release (push) Has been cancelled
CI (Release) / release-test (push) Has been cancelled

This commit is contained in:
bluepilledgreat 2025-03-15 09:09:52 +00:00
commit 912e5da6d6
62 changed files with 5532 additions and 156 deletions

View File

@ -45,6 +45,16 @@ body:
description: Provide a comprehensive description of the problem you're facing. Don't forget to attach any additional resources you may have, such as log files and screenshots.
validations:
required: true
- type: textarea
id: repro-steps
attributes:
label: How do you reproduce the problem?
description: Include the steps to reproduce the problem from start to finish. Include details such as FastFlags you added and settings you changed.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
- type: textarea
id: log
attributes:

View File

@ -8,7 +8,7 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: pizzaboxer.Bloxstrap
token: ${{ secrets.WINGET_TOKEN }}

View File

@ -11,6 +11,8 @@
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
<ResourceDictionary x:Name="CustomTheme" Source="UI/Style/Dark.xaml" /> <!-- NOTE: WpfUiWindow::ApplyTheme relies on this order. If you plan to change the order, please update the index in the function. -->
<ResourceDictionary x:Name="Default" Source="UI/Style/Default.xaml" />
</ResourceDictionary.MergedDictionaries>
<FontFamily x:Key="Rubik">pack://application:,,,/Resources/Fonts/#Rubik Light</FontFamily>

View File

@ -42,7 +42,7 @@ namespace Bloxstrap
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid);
public static readonly MD5 MD5Provider = MD5.Create();
@ -54,6 +54,8 @@ namespace Bloxstrap
public static readonly JsonManager<State> State = new();
public static readonly JsonManager<RobloxState> RobloxState = new();
public static readonly FastFlagManager FastFlags = new();
public static readonly HttpClient HttpClient = new(
@ -181,6 +183,22 @@ namespace Bloxstrap
}
}
public static void AssertWindowsOSVersion()
{
const string LOG_IDENT = "App::AssertWindowsOSVersion";
int major = Environment.OSVersion.Version.Major;
if (major < 10) // Windows 10 and newer only
{
Logger.WriteLine(LOG_IDENT, $"Detected unsupported Windows version ({Environment.OSVersion.Version}).");
if (!LaunchSettings.QuietFlag.Active)
Frontend.ShowMessageBox(Strings.App_OSDeprecation_Win7_81, MessageBoxImage.Error);
Terminate(ErrorCode.ERROR_INVALID_FUNCTION);
}
}
protected override void OnStartup(StartupEventArgs e)
{
const string LOG_IDENT = "App::OnStartup";
@ -213,6 +231,8 @@ namespace Bloxstrap
#endif
}
Logger.WriteLine(LOG_IDENT, $"OSVersion: {Environment.OSVersion}");
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
Logger.WriteLine(LOG_IDENT, $"Temp path is {Paths.Temp}");
Logger.WriteLine(LOG_IDENT, $"WindowsStartMenu path is {Paths.WindowsStartMenu}");
@ -292,6 +312,7 @@ namespace Bloxstrap
{
Logger.Initialize(true);
Logger.WriteLine(LOG_IDENT, "Not installed, launching the installer");
AssertWindowsOSVersion(); // prevent new installs from unsupported operating systems
LaunchHandler.LaunchInstaller();
}
else
@ -317,6 +338,7 @@ namespace Bloxstrap
Settings.Load();
State.Load();
RobloxState.Load();
FastFlags.Load();
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))

View File

@ -16,7 +16,7 @@ namespace Bloxstrap.AppData
public override string ExecutableName => "RobloxPlayerBeta.exe";
public override AppState State => App.State.Prop.Player;
public override AppState State => App.RobloxState.Prop.Player;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{

View File

@ -10,7 +10,7 @@
public override string ExecutableName => "RobloxStudioBeta.exe";
public override AppState State => App.State.Prop.Studio;
public override AppState State => App.RobloxState.Prop.Studio;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{

View File

@ -7,8 +7,8 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>2.8.5</Version>
<FileVersion>2.8.5</FileVersion>
<Version>2.9.0</Version>
<FileVersion>2.9.0</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
@ -25,9 +25,14 @@
<Resource Include="Resources\MessageBox\Information.png" />
<Resource Include="Resources\MessageBox\Question.png" />
<Resource Include="Resources\MessageBox\Warning.png" />
<EmbeddedResource Include="UI\Style\Editor-Theme-Dark.xshd" />
<EmbeddedResource Include="UI\Style\Editor-Theme-Light.xshd" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\CustomBootstrapperSchema.json" />
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate_Blank.xml" />
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate_Simple.xml" />
<EmbeddedResource Include="Resources\Icon2008.ico" />
<EmbeddedResource Include="Resources\Icon2011.ico" />
<EmbeddedResource Include="Resources\Icon2017.ico" />
@ -49,16 +54,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PackageReference Include="Markdig" Version="0.40.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
<PackageReference Include="XamlAnimatedGif" Version="2.3.0" />
</ItemGroup>
<ItemGroup>

View File

@ -45,10 +45,11 @@ namespace Bloxstrap
private readonly FastZipEvents _fastZipEvents = new();
private readonly CancellationTokenSource _cancelTokenSource = new();
private readonly IAppData AppData;
private readonly LaunchMode _launchMode;
private IAppData AppData = default!;
private LaunchMode _launchMode;
private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs;
private Version? _latestVersion = null;
private string _latestVersionGuid = null!;
private string _latestVersionDirectory = null!;
private PackageManifest _versionPackageManifest = null!;
@ -58,8 +59,9 @@ namespace Bloxstrap
private double _taskbarProgressIncrement;
private double _taskbarProgressMaximum;
private long _totalDownloadedBytes = 0;
private bool _packageExtractionSuccess = true;
private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || App.State.Prop.ForceReinstall || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
private bool _noConnection = false;
private AsyncMutex? _mutex;
@ -69,6 +71,9 @@ namespace Bloxstrap
public IBootstrapperDialog? Dialog = null;
public bool IsStudioLaunch => _launchMode != LaunchMode.Player;
public string MutexName { get; set; } = "Bloxstrap-Bootstrapper";
public bool QuitIfMutexExists { get; set; } = false;
#endregion
#region Core
@ -78,10 +83,23 @@ namespace Bloxstrap
// https://github.com/icsharpcode/SharpZipLib/blob/master/src/ICSharpCode.SharpZipLib/Zip/FastZip.cs/#L669-L680
// exceptions don't get thrown if we define events without actually binding to the failure events. probably a bug. ¯\_(ツ)_/¯
_fastZipEvents.FileFailure += (_, e) => throw e.Exception;
_fastZipEvents.FileFailure += (_, e) =>
{
// only give a pass to font files (no idea whats wrong with them)
if (!e.Name.EndsWith(".ttf"))
throw e.Exception;
App.Logger.WriteLine("FastZipEvents::OnFileFailure", $"Failed to extract {e.Name}");
_packageExtractionSuccess = false;
};
_fastZipEvents.DirectoryFailure += (_, e) => throw e.Exception;
_fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested;
SetupAppData();
}
private void SetupAppData()
{
AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData();
Deployment.BinaryType = AppData.BinaryType;
}
@ -179,25 +197,29 @@ namespace Bloxstrap
}
#endif
App.AssertWindowsOSVersion();
// ensure only one instance of the bootstrapper is running at the time
// so that we don't have stuff like two updates happening simultaneously
bool mutexExists = false;
bool mutexExists = Utilities.DoesMutexExist(MutexName);
try
if (mutexExists)
{
Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close();
App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting...");
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
mutexExists = true;
}
catch (Exception)
{
// no mutex exists
if (!QuitIfMutexExists)
{
App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, waiting...");
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
}
else
{
App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, exiting!");
return;
}
}
// wait for mutex to be released if it's not yet
await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper");
await using var mutex = new AsyncMutex(false, MutexName);
await mutex.AcquireAsync(_cancelTokenSource.Token);
_mutex = mutex;
@ -207,6 +229,7 @@ namespace Bloxstrap
{
App.Settings.Load();
App.State.Load();
App.RobloxState.Load();
}
if (!_noConnection)
@ -221,17 +244,42 @@ namespace Bloxstrap
}
}
CleanupVersionsFolder(); // cleanup after background updater
bool allModificationsApplied = true;
if (!_noConnection)
{
if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)
await UpgradeRoblox();
{
bool backgroundUpdaterMutexOpen = Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater");
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
backgroundUpdaterMutexOpen = false; // we want to actually update lol
App.Logger.WriteLine(LOG_IDENT, $"Background updater running: {backgroundUpdaterMutexOpen}");
if (backgroundUpdaterMutexOpen && _mustUpgrade)
{
// I am Forced Upgrade, killer of Background Updates
Utilities.KillBackgroundUpdater();
backgroundUpdaterMutexOpen = false;
}
if (!backgroundUpdaterMutexOpen)
{
if (IsEligibleForBackgroundUpdate())
StartBackgroundUpdater();
else
await UpgradeRoblox();
}
}
if (_cancelTokenSource.IsCancellationRequested)
return;
// we require deployment details for applying modifications for a worst case scenario,
// where we'd need to restore files from a package that isn't present on disk and needs to be redownloaded
await ApplyModifications();
allModificationsApplied = await ApplyModifications();
}
// check registry entries for every launch, just in case the stock bootstrapper changes it back
@ -245,7 +293,15 @@ namespace Bloxstrap
await mutex.ReleaseAsync();
if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested)
{
// show some balloon tips
if (!_packageExtractionSuccess)
Frontend.ShowBalloonTip(Strings.Bootstrapper_ExtractionFailed_Title, Strings.Bootstrapper_ExtractionFailed_Message, ToolTipIcon.Warning);
else if (!allModificationsApplied)
Frontend.ShowBalloonTip(Strings.Bootstrapper_ModificationsFailed_Title, Strings.Bootstrapper_ModificationsFailed_Message, ToolTipIcon.Warning);
StartRoblox();
}
await mutex.ReleaseAsync();
@ -267,12 +323,17 @@ namespace Bloxstrap
using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel");
var match = Regex.Match(
App.LaunchSettings.RobloxLaunchArgs,
"channel:([a-zA-Z0-9-_]+)",
App.LaunchSettings.RobloxLaunchArgs,
"channel:([a-zA-Z0-9-_]+)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
);
if (match.Groups.Count == 2)
if (App.LaunchSettings.ChannelFlag.Active && !string.IsNullOrEmpty(App.LaunchSettings.ChannelFlag.Data))
{
App.Logger.WriteLine(LOG_IDENT, $"Channel set to {App.LaunchSettings.ChannelFlag.Data} from arguments");
Deployment.Channel = App.LaunchSettings.ChannelFlag.Data.ToLowerInvariant();
}
else if (match.Groups.Count == 2)
{
Deployment.Channel = match.Groups[1].Value.ToLowerInvariant();
}
@ -289,37 +350,125 @@ namespace Bloxstrap
if (!Deployment.IsDefaultChannel)
App.SendStat("robloxChannel", Deployment.Channel);
ClientVersion clientVersion;
try
if (!App.LaunchSettings.VersionFlag.Active || string.IsNullOrEmpty(App.LaunchSettings.VersionFlag.Data))
{
clientVersion = await Deployment.GetInfo();
ClientVersion clientVersion;
try
{
clientVersion = await Deployment.GetInfo();
}
catch (InvalidChannelException ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}");
Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo();
}
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
_latestVersionGuid = clientVersion.VersionGuid;
_latestVersion = Utilities.ParseVersionSafe(clientVersion.Version);
}
catch (InvalidChannelException ex)
else
{
App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}");
Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo();
App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments");
_latestVersionGuid = App.LaunchSettings.VersionFlag.Data;
// we can't determine the version
}
if (clientVersion.IsBehindDefaultChannel)
{
App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because it's behind production");
Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo();
}
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
_latestVersionGuid = clientVersion.VersionGuid;
_latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid);
string pkgManifestUrl = Deployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt");
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
_versionPackageManifest = new(pkgManifestData);
// this can happen if version is set through arguments
if (_launchMode == LaunchMode.Unknown)
{
App.Logger.WriteLine(LOG_IDENT, "Identifying launch mode from package manifest");
bool isPlayer = _versionPackageManifest.Exists(x => x.Name == "RobloxApp.zip");
App.Logger.WriteLine(LOG_IDENT, $"isPlayer: {isPlayer}");
_launchMode = isPlayer ? LaunchMode.Player : LaunchMode.Studio;
SetupAppData(); // we need to set it up again
}
}
private bool IsEligibleForBackgroundUpdate()
{
const string LOG_IDENT = "Bootstrapper::IsEligibleForBackgroundUpdate";
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Is the background updater process");
return false;
}
if (!App.Settings.Prop.BackgroundUpdatesEnabled)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Background updates disabled");
return false;
}
if (IsStudioLaunch)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Studio launch");
return false;
}
if (_mustUpgrade)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Must upgrade is true");
return false;
}
// at least 3GB of free space
const long minimumFreeSpace = 3_000_000_000;
long space = Filesystem.GetFreeDiskSpace(Paths.Base);
if (space < minimumFreeSpace)
{
App.Logger.WriteLine(LOG_IDENT, $"Not eligible: User has {space} free space, at least {minimumFreeSpace} is required");
return false;
}
if (_latestVersion == default)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Latest version is undefined");
return false;
}
Version? currentVersion = Utilities.GetRobloxVersion(AppData);
if (currentVersion == default)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Current version is undefined");
return false;
}
// always normally upgrade for downgrades
if (currentVersion.Minor > _latestVersion.Minor)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Downgrade");
return false;
}
// only background update if we're:
// - one major update behind
// - the same major update
int diff = _latestVersion.Minor - currentVersion.Minor;
if (diff == 0 || diff == 1)
{
App.Logger.WriteLine(LOG_IDENT, "Eligible");
return true;
}
else
{
App.Logger.WriteLine(LOG_IDENT, $"Not eligible: Major version diff is {diff}");
return false;
}
}
private void StartRoblox()
@ -649,16 +798,51 @@ namespace Bloxstrap
#endregion
#region Roblox Install
private void CleanupVersionsFolder()
private static bool TryDeleteRobloxInDirectory(string dir)
{
string clientPath = Path.Combine(dir, "RobloxPlayerBeta.exe");
if (!File.Exists(dir))
{
clientPath = Path.Combine(dir, "RobloxStudioBeta.exe");
if (!File.Exists(dir))
return true; // ok???
}
try
{
File.Delete(clientPath);
return true;
}
catch (Exception)
{
return false;
}
}
public static void CleanupVersionsFolder()
{
const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder";
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Background updater tried to cleanup, stopping!");
return;
}
foreach (string dir in Directory.GetDirectories(Paths.Versions))
{
string dirName = Path.GetFileName(dir);
if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid)
if (dirName != App.RobloxState.Prop.Player.VersionGuid && dirName != App.RobloxState.Prop.Studio.VersionGuid)
{
// TODO: this is too expensive
//Filesystem.AssertReadOnlyDirectory(dir);
// check if it's still being used first
// we dont want to accidentally delete the files of a running roblox instance
if (!TryDeleteRobloxInDirectory(dir))
continue;
try
{
Directory.Delete(dir, true);
@ -729,11 +913,11 @@ namespace Bloxstrap
_isInstalling = true;
// make sure nothing is running before continuing upgrade
if (!IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss
if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && !IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss
KillRobloxPlayers();
// get a fully clean install
if (Directory.Exists(_latestVersionDirectory))
if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && Directory.Exists(_latestVersionDirectory))
{
try
{
@ -886,23 +1070,26 @@ namespace Bloxstrap
var allPackageHashes = new List<string>();
allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values);
allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values);
allPackageHashes.AddRange(App.RobloxState.Prop.Player.PackageHashes.Values);
allPackageHashes.AddRange(App.RobloxState.Prop.Studio.PackageHashes.Values);
foreach (string hash in cachedPackageHashes)
if (!App.Settings.Prop.DebugDisableVersionPackageCleanup)
{
if (!allPackageHashes.Contains(hash))
foreach (string hash in cachedPackageHashes)
{
App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}");
try
if (!allPackageHashes.Contains(hash))
{
File.Delete(Path.Combine(Paths.Downloads, hash));
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!");
App.Logger.WriteException(LOG_IDENT, ex);
App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}");
try
{
File.Delete(Path.Combine(Paths.Downloads, hash));
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!");
App.Logger.WriteException(LOG_IDENT, ex);
}
}
}
}
@ -913,7 +1100,7 @@ namespace Bloxstrap
AppData.State.Size = distributionSize;
int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size;
int totalSize = App.RobloxState.Prop.Player.Size + App.RobloxState.Prop.Studio.Size;
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{
@ -923,14 +1110,32 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
App.State.Save();
App.RobloxState.Save();
_isInstalling = false;
}
private async Task ApplyModifications()
private static void StartBackgroundUpdater()
{
const string LOG_IDENT = "Bootstrapper::StartBackgroundUpdater";
if (Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater"))
{
App.Logger.WriteLine(LOG_IDENT, "Background updater already running");
return;
}
App.Logger.WriteLine(LOG_IDENT, "Starting background updater");
Process.Start(Paths.Process, "-backgroundupdater");
}
private async Task<bool> ApplyModifications()
{
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
bool success = true;
SetStatus(Strings.Bootstrapper_Status_ApplyingModifications);
// handle file mods
@ -1006,7 +1211,7 @@ namespace Bloxstrap
foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories))
{
if (_cancelTokenSource.IsCancellationRequested)
return;
return true;
// get relative directory path
string relativeFile = file.Substring(Paths.Modifications.Length + 1);
@ -1038,10 +1243,18 @@ namespace Bloxstrap
Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
Filesystem.AssertReadOnly(fileVersionFolder);
File.Copy(fileModFolder, fileVersionFolder, true);
Filesystem.AssertReadOnly(fileVersionFolder);
App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder");
try
{
File.Copy(fileModFolder, fileVersionFolder, true);
Filesystem.AssertReadOnly(fileVersionFolder);
App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to apply modification ({relativeFile})");
App.Logger.WriteException(LOG_IDENT, ex);
success = false;
}
}
// the manifest is primarily here to keep track of what files have been
@ -1050,7 +1263,7 @@ namespace Bloxstrap
var fileRestoreMap = new Dictionary<string, List<string>>();
foreach (string fileLocation in App.State.Prop.ModManifest)
foreach (string fileLocation in App.RobloxState.Prop.ModManifest)
{
if (modFolderFiles.Contains(fileLocation))
continue;
@ -1088,17 +1301,31 @@ namespace Bloxstrap
if (package is not null)
{
if (_cancelTokenSource.IsCancellationRequested)
return;
return true;
await DownloadPackage(package);
ExtractPackage(package, entry.Value);
}
}
App.State.Prop.ModManifest = modFolderFiles;
App.State.Save();
// make sure we're not overwriting a new update
// if we're the background update process, always overwrite
if (App.LaunchSettings.BackgroundUpdaterFlag.Active || !App.RobloxState.HasFileOnDiskChanged())
{
App.RobloxState.Prop.ModManifest = modFolderFiles;
App.RobloxState.Save();
}
else
{
App.Logger.WriteLine(LOG_IDENT, "RobloxState disk mismatch, not saving ModManifest");
}
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
if (!success)
App.Logger.WriteLine(LOG_IDENT, "Failed to apply all modifications");
return success;
}
private async Task DownloadPackage(Package package)

View File

@ -10,6 +10,7 @@
ByfronDialog,
[EnumName(StaticName = "Bloxstrap")]
FluentDialog,
FluentAeroDialog
FluentAeroDialog,
CustomDialog
}
}

View File

@ -0,0 +1,8 @@
namespace Bloxstrap.Enums
{
public enum CustomThemeTemplate
{
Blank,
Simple
}
}

View File

@ -3,6 +3,10 @@
public enum LaunchMode
{
None,
/// <summary>
/// Launch mode will be determined inside the bootstrapper. Only works if the VersionFlag is set.
/// </summary>
Unknown,
Player,
Studio,
StudioAuth

View File

@ -0,0 +1,60 @@
using Bloxstrap.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class CustomThemeException : Exception
{
/// <summary>
/// The exception message in English (for logging)
/// </summary>
public string EnglishMessage { get; } = null!;
public CustomThemeException(string translationString)
: base(Strings.ResourceManager.GetStringSafe(translationString))
{
EnglishMessage = Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB"));
}
public CustomThemeException(Exception innerException, string translationString)
: base(Strings.ResourceManager.GetStringSafe(translationString), innerException)
{
EnglishMessage = Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB"));
}
public CustomThemeException(string translationString, params object?[] args)
: base(string.Format(Strings.ResourceManager.GetStringSafe(translationString), args))
{
EnglishMessage = string.Format(Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")), args);
}
public CustomThemeException(Exception innerException, string translationString, params object?[] args)
: base(string.Format(Strings.ResourceManager.GetStringSafe(translationString), args), innerException)
{
EnglishMessage = string.Format(Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")), args);
}
public override string ToString()
{
StringBuilder sb = new StringBuilder(GetType().ToString());
if (!string.IsNullOrEmpty(Message))
sb.Append($": {Message}");
if (!string.IsNullOrEmpty(EnglishMessage) && Message != EnglishMessage)
sb.Append($" ({EnglishMessage})");
if (InnerException != null)
sb.Append($"\r\n ---> {InnerException}\r\n ");
if (StackTrace != null)
sb.Append($"\r\n{StackTrace}");
return sb.ToString();
}
}
}

View File

@ -13,7 +13,8 @@
BootstrapperStyle.ProgressDialog,
BootstrapperStyle.LegacyDialog2011,
BootstrapperStyle.LegacyDialog2008,
BootstrapperStyle.VistaDialog
BootstrapperStyle.VistaDialog,
BootstrapperStyle.CustomDialog
};
}
}

View File

@ -0,0 +1,36 @@
using System.Text;
namespace Bloxstrap.Extensions
{
static class CustomThemeTemplateEx
{
const string EXAMPLES_URL = "https://github.com/bloxstraplabs/custom-bootstrapper-examples";
public static string GetFileName(this CustomThemeTemplate template)
{
return $"CustomBootstrapperTemplate_{template}.xml";
}
public static string GetFileContents(this CustomThemeTemplate template)
{
string contents = Encoding.UTF8.GetString(Resource.Get(template.GetFileName()).Result);
switch (template)
{
case CustomThemeTemplate.Blank:
{
string moreText = string.Format(Strings.CustomTheme_Templates_Blank_MoreExamples, EXAMPLES_URL);
return string.Format(contents, Strings.CustomTheme_Templates_Blank_UIElements, moreText);
}
case CustomThemeTemplate.Simple:
{
string moreText = string.Format(Strings.CustomTheme_Templates_Simple_MoreExamples, EXAMPLES_URL);
return string.Format(contents, moreText);
}
default:
Debug.Assert(false);
return contents;
}
}
}
}

View File

@ -197,7 +197,7 @@ namespace Bloxstrap
var processes = new List<Process>();
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
if (!String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid))
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
if (App.IsStudioVisible)
@ -587,16 +587,23 @@ namespace Bloxstrap
}
}
if (Utilities.CompareVersions(existingVer, "2.8.3") == VersionComparison.LessThan)
if (Utilities.CompareVersions(existingVer, "2.9.0") == VersionComparison.LessThan)
{
// force reinstallation
App.State.Prop.Player.VersionGuid = "";
App.State.Prop.Studio.VersionGuid = "";
// move from App.State to App.RobloxState
if (App.State.Prop.GetDeprecatedPlayer() != null)
App.RobloxState.Prop.Player = App.State.Prop.GetDeprecatedPlayer()!;
if (App.State.Prop.GetDeprecatedStudio() != null)
App.RobloxState.Prop.Studio = App.State.Prop.GetDeprecatedStudio()!;
if (App.State.Prop.GetDeprecatedModManifest() != null)
App.RobloxState.Prop.ModManifest = App.State.Prop.GetDeprecatedModManifest()!;
}
App.Settings.Save();
App.FastFlags.Save();
App.State.Save();
App.RobloxState.Save();
}
if (currentVer is null)

View File

@ -8,6 +8,11 @@ namespace Bloxstrap
public T Prop { get; set; } = new();
/// <summary>
/// The file hash when last retrieved from disk
/// </summary>
public string? LastFileHash { get; private set; }
public virtual string ClassName => typeof(T).Name;
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
@ -22,12 +27,15 @@ namespace Bloxstrap
try
{
T? settings = JsonSerializer.Deserialize<T>(File.ReadAllText(FileLocation));
string contents = File.ReadAllText(FileLocation);
T? settings = JsonSerializer.Deserialize<T>(contents);
if (settings is null)
throw new ArgumentNullException("Deserialization returned null");
Prop = settings;
LastFileHash = MD5Hash.FromString(contents);
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
}
@ -74,7 +82,11 @@ namespace Bloxstrap
try
{
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
string contents = JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(FileLocation, contents);
LastFileHash = MD5Hash.FromString(contents);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
@ -89,5 +101,13 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
}
/// <summary>
/// Is the file on disk different to the one deserialised during this session?
/// </summary>
public bool HasFileOnDiskChanged()
{
return LastFileHash != MD5Hash.FromFile(FileLocation);
}
}
}

View File

@ -4,6 +4,7 @@ using Windows.Win32;
using Windows.Win32.Foundation;
using Bloxstrap.UI.Elements.Dialogs;
using Bloxstrap.Enums;
namespace Bloxstrap
{
@ -58,6 +59,11 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
LaunchWatcher();
}
else if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Opening background updater");
LaunchBackgroundUpdater();
}
else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
{
App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})");
@ -295,5 +301,51 @@ namespace Bloxstrap
App.Terminate();
});
}
public static void LaunchBackgroundUpdater()
{
const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater";
// Activate some LaunchFlags we need
App.LaunchSettings.QuietFlag.Active = true;
App.LaunchSettings.NoLaunchFlag.Active = true;
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
App.Bootstrapper = new Bootstrapper(LaunchMode.Player)
{
MutexName = "Bloxstrap-BackgroundUpdater",
QuitIfMutexExists = true
};
CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(() =>
{
App.Logger.WriteLine(LOG_IDENT, "Started event waiter");
using (EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent"))
handle.WaitOne();
App.Logger.WriteLine(LOG_IDENT, "Received close event, killing it all!");
App.Bootstrapper.Cancel();
}, cts.Token);
Task.Run(App.Bootstrapper.Run).ContinueWith(t =>
{
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
cts.Cancel(); // stop event waiter
if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception);
}
App.Terminate();
});
App.Logger.WriteLine(LOG_IDENT, "Exiting");
}
}
}

View File

@ -12,25 +12,33 @@ namespace Bloxstrap
{
public class LaunchSettings
{
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag QuietFlag { get; } = new("quiet");
public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
public LaunchFlag UninstallFlag { get; } = new("uninstall");
public LaunchFlag QuietFlag { get; } = new("quiet");
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag UninstallFlag { get; } = new("uninstall");
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ForceFlag { get; } = new("force");
#if DEBUG
public bool BypassUpdateCheck => true;
@ -87,6 +95,13 @@ namespace Bloxstrap
RobloxLaunchArgs = arg;
startIdx = 1;
}
else if (arg.StartsWith("version-"))
{
App.Logger.WriteLine(LOG_IDENT, "Got version argument");
VersionFlag.Active = true;
VersionFlag.Data = arg;
startIdx = 1;
}
}
// parse
@ -108,11 +123,18 @@ namespace Bloxstrap
continue;
}
if (flag.Active)
{
App.Logger.WriteLine(LOG_IDENT, $"Tried to set {identifier} flag twice");
continue;
}
flag.Active = true;
if (i < Args.Length - 1 && Args[i+1] is string nextArg && !nextArg.StartsWith('-'))
{
flag.Data = nextArg;
i++;
App.Logger.WriteLine(LOG_IDENT, $"Identifier '{identifier}' is active with data");
}
else
@ -121,6 +143,9 @@ namespace Bloxstrap
}
}
if (VersionFlag.Active)
RobloxLaunchMode = LaunchMode.Unknown; // determine in bootstrapper
if (PlayerFlag.Active)
ParsePlayer(PlayerFlag.Data);
else if (StudioFlag.Active)

View File

@ -12,7 +12,5 @@
public string BootstrapperVersion { get; set; } = null!;
public DateTime? Timestamp { get; set; }
public bool IsBehindDefaultChannel { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.Persistable
{
public class RobloxState
{
public AppState Player { get; set; } = new();
public AppState Studio { get; set; } = new();
public List<string> ModManifest { get; set; } = new();
}
}

View File

@ -17,6 +17,9 @@ namespace Bloxstrap.Models.Persistable
public bool UseFastFlagManager { get; set; } = true;
public bool WPFSoftwareRender { get; set; } = false;
public bool EnableAnalytics { get; set; } = true;
public bool BackgroundUpdatesEnabled { get; set; } = true;
public bool DebugDisableVersionPackageCleanup { get; set; } = false;
public string? SelectedCustomTheme { get; set; } = null;
// integration configuration
public bool EnableActivityTracking { get; set; } = true;

View File

@ -6,12 +6,28 @@
public bool PromptWebView2Install { get; set; } = true;
public AppState Player { get; set; } = new();
public AppState Studio { get; set; } = new();
public bool ForceReinstall { get; set; } = false;
public WindowState SettingsWindow { get; set; } = new();
public List<string> ModManifest { get; set; } = new();
#region Deprecated properties
/// <summary>
/// Deprecated, use App.RobloxState.Player
/// </summary>
public AppState? Player { private get; set; }
public AppState? GetDeprecatedPlayer() => Player;
/// <summary>
/// Deprecated, use App.RobloxState.Studio
/// </summary>
public AppState? Studio { private get; set; }
public AppState? GetDeprecatedStudio() => Studio;
/// <summary>
/// Deprecated, use App.RobloxState.ModManifest
/// </summary>
public List<string>? ModManifest { private get; set; }
public List<string>? GetDeprecatedModManifest() => ModManifest;
#endregion
}
}

View File

@ -22,6 +22,7 @@
public static string Integrations { get; private set; } = "";
public static string Versions { get; private set; } = "";
public static string Modifications { get; private set; } = "";
public static string CustomThemes { get; private set; } = "";
public static string Application { get; private set; } = "";
@ -37,6 +38,7 @@
Integrations = Path.Combine(Base, "Integrations");
Versions = Path.Combine(Base, "Versions");
Modifications = Path.Combine(Base, "Modifications");
CustomThemes = Path.Combine(Base, "CustomThemes");
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
}

View File

@ -21,5 +21,10 @@ namespace Bloxstrap
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
public static async Task<string> GetString(string name)
{
return Encoding.UTF8.GetString(await Get(name));
}
}
}

View File

@ -0,0 +1,521 @@
{
"Elements": {
"FrameworkElement": {
"IsCreatable": false,
"Attributes": {
"Name": "string",
"Visibility": "Visibility",
"IsEnabled": "bool",
"Margin": "Thickness",
"Height": "double",
"Width": "double",
"HorizontalAlignment": "HorizontalAlignment",
"VerticalAlignment": "VerticalAlignment",
"RenderTransform": "Transform",
"LayoutTransform": "Transform",
"Opacity": "double",
"OpacityMask": "Brush",
"RenderTransformOrigin": "Point",
"Panel.ZIndex": "int",
"Grid.Row": "int",
"Grid.RowSpan": "int",
"Grid.Column": "int",
"Grid.ColumnSpan": "int"
}
},
"Control": {
"SuperClass": "FrameworkElement",
"IsCreatable": false,
"Attributes": {
"Padding": "Thickness",
"BorderThickness": "Thickness",
"Foreground": "Brush",
"Background": "Brush",
"BorderBrush": "Brush",
"FontSize": "double",
"FontWeight": "FontWeight",
"FontStyle": "FontStyle",
"FontFamily": "FontFamily"
}
},
"BloxstrapCustomBootstrapper": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"Version": "int",
"Theme": "Theme",
"Title": "string",
"IgnoreTitleBarInset": "bool",
"WindowCornerPreference": "WindowCornerPreference"
}
},
"TitleBar": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"ShowMinimize": "bool",
"ShowClose": "bool",
"Title": "string"
}
},
"Button": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"Content": "object"
}
},
"RangeBase": {
"SuperClass": "Control",
"IsCreatable": false,
"Attributes": {
"Value": "double",
"Maximum": "double"
}
},
"ProgressBar": {
"SuperClass": "RangeBase",
"IsCreatable": true,
"Attributes": {
"IsIndeterminate": "bool",
"CornerRadius": "CornerRadius",
"IndicatorCornerRadius": "CornerRadius"
}
},
"ProgressRing": {
"SuperClass": "RangeBase",
"IsCreatable": true,
"Attributes": {
"IsIndeterminate": "bool"
}
},
"TextBlock": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Text": "string",
"Foreground": "Brush",
"Background": "Brush",
"FontSize": "double",
"FontWeight": "FontWeight",
"FontStyle": "FontStyle",
"FontFamily": "FontFamily",
"LineHeight": "double",
"LineStackingStrategy": "LineStackingStrategy",
"TextAlignment": "TextAlignment",
"TextTrimming": "TextTrimming",
"TextWrapping": "TextWrapping",
"TextDecorations": "TextDecorations",
"IsHyphenationEnabled": "bool",
"BaselineOffset": "double",
"Padding": "Thickness"
}
},
"MarkdownTextBlock": {
"SuperClass": "TextBlock",
"IsCreatable": true,
"Attributes": {}
},
"Image": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Stretch": "Stretch",
"StretchDirection": "StretchDirection",
"Source": "ImageSource",
"IsAnimated": "bool"
}
},
"Grid": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"RowDefinitions": "object",
"ColumnDefinitions": "object"
}
},
"StackPanel": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Orientation": "Orientation"
}
},
"Border": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Background": "Brush",
"BorderBrush": "Brush",
"BorderThickness": "Thickness",
"Padding": "Thickness",
"CornerRadius": "CornerRadius"
}
},
"RowDefinition": {
"IsCreatable": true,
"Attributes": {
"Height": "GridLength",
"MinHeight": "double",
"MaxHeight": "double"
}
},
"ColumnDefinition": {
"IsCreatable": true,
"Attributes": {
"Width": "GridLength",
"MinWidth": "double",
"MaxWidth": "double"
}
},
"ScaleTransform": {
"IsCreatable": true,
"Attributes": {
"ScaleX": "double",
"ScaleY": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"SkewTransform": {
"IsCreatable": true,
"Attributes": {
"AngleX": "double",
"AngleY": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"RotateTransform": {
"IsCreatable": true,
"Attributes": {
"Angle": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"TranslateTransform": {
"IsCreatable": true,
"Attributes": {
"X": "double",
"Y": "double"
}
},
"Brush": {
"IsCreatable": false,
"Attributes": {
"Opacity": "double"
}
},
"SolidColorBrush": {
"SuperClass": "Brush",
"IsCreatable": true,
"Attributes": {
"Color": "Color"
}
},
"ImageBrush": {
"SuperClass": "Brush",
"IsCreatable": true,
"Attributes": {
"AlignmentX": "AlignmentX",
"AlignmentY": "AlignmentY",
"Stretch": "Stretch",
"TileMode": "TileMode",
"ViewboxUnits": "BrushMappingMode",
"ViewportUnits": "BrushMappingMode",
"Viewbox": "Rect",
"Viewport": "Rect",
"ImageSource": "ImageSource"
}
},
"LinearGradientBrush": {
"SuperClass": "Brush",
"IsCreatable": true,
"Attributes": {
"StartPoint": "Point",
"EndPoint": "Point",
"ColorInterpolationMode": "ColorInterpolationMode",
"MappingMode": "BrushMappingMode",
"SpreadMethod": "GradientSpreadMethod"
}
},
"GradientStop": {
"IsCreatable": true,
"Attributes": {
"Color": "Color",
"Offset": "double"
}
},
"Shape": {
"SuperClass": "FrameworkElement",
"IsCreatable": false,
"Attributes": {
"Fill": "Brush",
"Stroke": "Brush",
"Stretch": "Stretch",
"StrokeDashCap": "PenLineCap",
"StrokeDashOffset": "double",
"StrokeEndLineCap": "PenLineCap",
"StrokeLineJoin": "PenLineJoin",
"StrokeMiterLimit": "double",
"StrokeStartLineCap": "PenLineCap",
"StrokeThickness": "double"
}
},
"Ellipse": {
"SuperClass": "Shape",
"IsCreatable": true,
"Attributes": {}
},
"Line": {
"SuperClass": "Shape",
"IsCreatable": true,
"Attributes": {
"X1": "double",
"X2": "double",
"Y1": "double",
"Y2": "double"
}
},
"Rectangle": {
"SuperClass": "Shape",
"IsCreatable": true,
"Attributes": {
"RadiusX": "double",
"RadiusY": "double"
}
},
"BlurEffect": {
"IsCreatable": true,
"Attributes": {
"KernelType": "KernelType",
"Radius": "double",
"RenderingBias": "RenderingBias"
}
},
"DropShadowEffect": {
"IsCreatable": true,
"Attributes": {
"BlurRadius": "double",
"Direction": "double",
"Opacity": "double",
"ShadowDepth": "double",
"RenderingBias": "RenderingBias",
"Color": "Color"
}
}
},
"Types": {
"string": {},
"bool": {
"Values": [
"True",
"False"
]
},
"int": {},
"double": {},
"object": { "CanHaveElement": true },
"Thickness": {},
"Rect": {},
"Point": {},
"CornerRadius": {},
"Brush": { "CanHaveElement": true },
"Color": {},
"ImageSource": {},
"Transform": { "CanHaveElement": true },
"FontFamily": {},
"GridLength": {},
"Visibility": {
"Values": [
"Visible",
"Hidden",
"Collapsed"
]
},
"HorizontalAlignment": {
"Values": [
"Left",
"Center",
"Right",
"Stretch"
]
},
"VerticalAlignment": {
"Values": [
"Top",
"Center",
"Bottom",
"Stretch"
]
},
"Theme": {
"Values": [
"Default",
"Dark",
"Light"
]
},
"FontWeight": {
"Values": [
"Thin",
"ExtraLight",
"UltraLight",
"Medium",
"Normal",
"Regular",
"DemiBold",
"SemiBold",
"Bold",
"ExtraBold",
"UltraBold",
"Black",
"Heavy",
"ExtraBlack",
"ExtraHeavy"
]
},
"FontStyle": {
"Values": [
"Normal",
"Italic",
"Oblique"
]
},
"LineStackingStrategy": {
"Values": [
"BlockLineHeight",
"MaxHeight"
]
},
"TextAlignment": {
"Values": [
"Left",
"Right",
"Center",
"Justify"
]
},
"TextTrimming": {
"Values": [
"None",
"CharacterEllipsis",
"WordEllipsis"
]
},
"TextWrapping": {
"Values": [
"WrapWithOverflow",
"NoWrap",
"Wrap"
]
},
"TextDecorations": {
"Values": [
"Baseline",
"OverLine",
"Strikethrough",
"Underline"
]
},
"Stretch": {
"Values": [
"None",
"Fill",
"Uniform",
"UniformToFill"
]
},
"StretchDirection": {
"Values": [
"UpOnly",
"DownOnly",
"Both"
]
},
"AlignmentX": {
"Values": [
"Left",
"Center",
"Right"
]
},
"AlignmentY": {
"Values": [
"Top",
"Center",
"Bottom"
]
},
"TileMode": {
"Values": [
"None",
"FlipX",
"FlipY",
"FlipXY",
"Tile"
]
},
"BrushMappingMode": {
"Values": [
"Absolute",
"RelativeToBoundingBox"
]
},
"ColorInterpolationMode": {
"Values": [
"ScRgbLinearInterpolation",
"SRgbLinearInterpolation"
]
},
"GradientSpreadMethod": {
"Values": [
"Pad",
"Reflect",
"Repeat"
]
},
"PenLineCap": {
"Values": [
"Flat",
"Square",
"Round",
"Triangle"
]
},
"PenLineJoin": {
"Values": [
"Miter",
"Bevel",
"Round"
]
},
"KernelType": {
"Values": [
"Gaussian",
"Box"
]
},
"RenderingBias": {
"Values": [
"Performance",
"Quality"
]
},
"Orientation": {
"Values": [
"Horizontal",
"Vertical"
]
},
"WindowCornerPreference": {
"Values": [
"Default",
"DoNotRound",
"Round",
"RoundSmall"
]
}
}
}

View File

@ -0,0 +1,4 @@
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="500">
<!-- {0} -->
<!-- {1} -->
</BloxstrapCustomBootstrapper>

View File

@ -0,0 +1,9 @@
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="520" IgnoreTitleBarInset="True" Theme="Default" Margin="30">
<!-- {0} -->
<TitleBar Title="" ShowMinimize="False" ShowClose="False" />
<Image Source="{Icon}" Height="100" Width="100" HorizontalAlignment="Center" Margin="0,15,0,0" />
<TextBlock HorizontalAlignment="Center" Name="StatusText" FontSize="20" Margin="0,170,0,0" />
<ProgressBar Width="450" Height="12" Name="PrimaryProgressBar" HorizontalAlignment="Center" Margin="0,200,0,0" />
<Button Content="Cancel" Name="CancelButton" HorizontalAlignment="Center" Margin="0,225,0,0" Height="30" Width="100" />
</BloxstrapCustomBootstrapper>

View File

@ -151,6 +151,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Roblox no longer supports Windows 7 or 8.1. To continue playing Roblox, please upgrade to Windows 10 or newer..
/// </summary>
public static string App_OSDeprecation_Win7_81 {
get {
return ResourceManager.GetString("App.OSDeprecation.Win7_81", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website..
/// </summary>
@ -169,6 +178,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Some content may be missing. Force a Roblox reinstallation in settings to fix this..
/// </summary>
public static string Bootstrapper_ExtractionFailed_Message {
get {
return ResourceManager.GetString("Bootstrapper.ExtractionFailed.Message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to extract all files.
/// </summary>
public static string Bootstrapper_ExtractionFailed_Title {
get {
return ResourceManager.GetString("Bootstrapper.ExtractionFailed.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap tried to upgrade Roblox but can&apos;t because Roblox&apos;s files are still in use.
///
@ -198,6 +225,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Not all modifications will be present in the current launch..
/// </summary>
public static string Bootstrapper_ModificationsFailed_Message {
get {
return ResourceManager.GetString("Bootstrapper.ModificationsFailed.Message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to apply all modifications.
/// </summary>
public static string Bootstrapper_ModificationsFailed_Title {
get {
return ResourceManager.GetString("Bootstrapper.ModificationsFailed.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap does not have enough disk space to download and install Roblox. Please free up some disk space and try again..
/// </summary>
@ -396,6 +441,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Create New.
/// </summary>
public static string Common_CreateNew {
get {
return ResourceManager.GetString("Common.CreateNew", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom.
/// </summary>
@ -432,6 +486,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
public static string Common_Edit {
get {
return ResourceManager.GetString("Common.Edit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
@ -450,6 +513,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Import.
/// </summary>
public static string Common_Import {
get {
return ResourceManager.GetString("Common.Import", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import from file.
/// </summary>
@ -585,6 +657,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Rename.
/// </summary>
public static string Common_Rename {
get {
return ResourceManager.GetString("Common.Rename", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
@ -639,6 +720,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Template.
/// </summary>
public static string Common_Template {
get {
return ResourceManager.GetString("Common.Template", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Value.
/// </summary>
@ -802,6 +892,421 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to File must be a ZIP.
/// </summary>
public static string CustomTheme_Add_Errors_FileNotZip {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.FileNotZip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name cannot be empty.
/// </summary>
public static string CustomTheme_Add_Errors_NameEmpty {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameEmpty", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name contains illegal characters.
/// </summary>
public static string CustomTheme_Add_Errors_NameIllegalCharacters {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameIllegalCharacters", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name cannot be used.
/// </summary>
public static string CustomTheme_Add_Errors_NameReserved {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameReserved", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name is already in use.
/// </summary>
public static string CustomTheme_Add_Errors_NameTaken {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameTaken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown error.
/// </summary>
public static string CustomTheme_Add_Errors_Unknown {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.Unknown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid or corrupted ZIP file.
/// </summary>
public static string CustomTheme_Add_Errors_ZipInvalidData {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.ZipInvalidData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme file could not be found in the ZIP file.
/// </summary>
public static string CustomTheme_Add_Errors_ZipMissingThemeFile {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.ZipMissingThemeFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add Custom Theme.
/// </summary>
public static string CustomTheme_Add_Title {
get {
return ResourceManager.GetString("CustomTheme.Add.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom Theme {0}.
/// </summary>
public static string CustomTheme_DefaultName {
get {
return ResourceManager.GetString("CustomTheme.DefaultName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save changes to {0}?.
/// </summary>
public static string CustomTheme_Editor_ConfirmSave {
get {
return ResourceManager.GetString("CustomTheme.Editor.ConfirmSave", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Theme Directory.
/// </summary>
public static string CustomTheme_Editor_OpenThemeDirectory {
get {
return ResourceManager.GetString("CustomTheme.Editor.OpenThemeDirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preview.
/// </summary>
public static string CustomTheme_Editor_Preview {
get {
return ResourceManager.GetString("CustomTheme.Editor.Preview", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save.
/// </summary>
public static string CustomTheme_Editor_Save {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An error occurred while saving your theme..
/// </summary>
public static string CustomTheme_Editor_Save_Error {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save.Error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme successfully saved!.
/// </summary>
public static string CustomTheme_Editor_Save_Success {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save.Success", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Editing &quot;{0}&quot;.
/// </summary>
public static string CustomTheme_Editor_Title {
get {
return ResourceManager.GetString("CustomTheme.Editor.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom dialog has already been initialised.
/// </summary>
public static string CustomTheme_Errors_DialogAlreadyInitialised {
get {
return ResourceManager.GetString("CustomTheme.Errors.DialogAlreadyInitialised", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} uses blacklisted scheme {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeBlacklistedUriScheme {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeBlacklistedUriScheme", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} has invalid {1}: {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeConversionError {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeConversionError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} is not a valid {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeInvalidType {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeInvalidType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Element {0} is missing the {1} attribute.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMissing {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMissing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} is missing its child.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMissingChild {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMissingChild", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} can only have one child.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMultipleChildren {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMultipleChildren", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} can only have one {1} defined.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMultipleDefinitions {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMultipleDefinitions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} must be larger than {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMustBeLargerThanMin {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} must be smaller than {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMustBeSmallerThanMax {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} could not be parsed into a {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeParseError {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeParseError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} {2} is null.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeParseErrorNull {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeParseErrorNull", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} cannot have a child of {1}.
/// </summary>
public static string CustomTheme_Errors_ElementInvalidChild {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementInvalidChild", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} can only have one child.
/// </summary>
public static string CustomTheme_Errors_ElementMultipleChildren {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementMultipleChildren", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} failed to create {1}: {2}.
/// </summary>
public static string CustomTheme_Errors_ElementTypeCreationFailed {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementTypeCreationFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme XML root is not {0}.
/// </summary>
public static string CustomTheme_Errors_InvalidRoot {
get {
return ResourceManager.GetString("CustomTheme.Errors.InvalidRoot", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No custom theme selected.
/// </summary>
public static string CustomTheme_Errors_NoThemeSelected {
get {
return ResourceManager.GetString("CustomTheme.Errors.NoThemeSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to setup custom bootstrapper: {0}.
///Defaulting to {1}..
/// </summary>
public static string CustomTheme_Errors_SetupFailed {
get {
return ResourceManager.GetString("CustomTheme.Errors.SetupFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom bootstrappers can only have a maximum of {0} elements, got {1}..
/// </summary>
public static string CustomTheme_Errors_TooManyElements {
get {
return ResourceManager.GetString("CustomTheme.Errors.TooManyElements", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown element {0}.
/// </summary>
public static string CustomTheme_Errors_UnknownElement {
get {
return ResourceManager.GetString("CustomTheme.Errors.UnknownElement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} Unknown {1} &apos;{2}&apos;.
/// </summary>
public static string CustomTheme_Errors_UnknownEnumValue {
get {
return ResourceManager.GetString("CustomTheme.Errors.UnknownEnumValue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version is not a number.
/// </summary>
public static string CustomTheme_Errors_VersionNotNumber {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotNumber", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version {1} is not recognised.
/// </summary>
public static string CustomTheme_Errors_VersionNotRecognised {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotRecognised", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version is not set.
/// </summary>
public static string CustomTheme_Errors_VersionNotSet {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version {1} is no longer supported.
/// </summary>
public static string CustomTheme_Errors_VersionNotSupported {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotSupported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to parse the theme file: {0}.
/// </summary>
public static string CustomTheme_Errors_XMLParseFailed {
get {
return ResourceManager.GetString("CustomTheme.Errors.XMLParseFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Examples of custom bootstrappers can be found at {0}.
/// </summary>
public static string CustomTheme_Templates_Blank_MoreExamples {
get {
return ResourceManager.GetString("CustomTheme.Templates.Blank.MoreExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Put UI elements here.
/// </summary>
public static string CustomTheme_Templates_Blank_UIElements {
get {
return ResourceManager.GetString("CustomTheme.Templates.Blank.UIElements", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Find more custom bootstrapper examples at {0}.
/// </summary>
public static string CustomTheme_Templates_Simple_MoreExamples {
get {
return ResourceManager.GetString("CustomTheme.Templates.Simple.MoreExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add Fast Flag.
/// </summary>
@ -1098,6 +1603,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Custom.
/// </summary>
public static string Enums_BootstrapperStyle_CustomDialog {
get {
return ResourceManager.GetString("Enums.BootstrapperStyle.CustomDialog", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap (Glass).
/// </summary>
@ -1161,6 +1675,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Blank.
/// </summary>
public static string Enums_CustomThemeTemplate_Blank {
get {
return ResourceManager.GetString("Enums.CustomThemeTemplate.Blank", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Simple.
/// </summary>
public static string Enums_CustomThemeTemplate_Simple {
get {
return ResourceManager.GetString("Enums.CustomThemeTemplate.Simple", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Catmoji.
/// </summary>
@ -1761,6 +2293,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Apache License 2.0.
/// </summary>
public static string Menu_About_Licenses_Apache {
get {
return ResourceManager.GetString("Menu.About.Licenses.Apache", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to BSD 2-Clause License.
/// </summary>
@ -1905,6 +2446,33 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Failed to delete custom theme {0}: {1}.
/// </summary>
public static string Menu_Appearance_CustomThemes_DeleteFailed {
get {
return ResourceManager.GetString("Menu.Appearance.CustomThemes.DeleteFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No custom theme selected..
/// </summary>
public static string Menu_Appearance_CustomThemes_NoneSelected {
get {
return ResourceManager.GetString("Menu.Appearance.CustomThemes.NoneSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to rename custom theme {0}: {1}.
/// </summary>
public static string Menu_Appearance_CustomThemes_RenameFailed {
get {
return ResourceManager.GetString("Menu.Appearance.CustomThemes.RenameFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Configure how Bloxstrap should look..
/// </summary>
@ -2013,6 +2581,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Update Roblox in the background instead of waiting. Not recommended for slow networks. At least 3GB of free storage space is required for this feature to work..
/// </summary>
public static string Menu_Behaviour_BackgroundUpdates_Description {
get {
return ResourceManager.GetString("Menu.Behaviour.BackgroundUpdates.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Background updates.
/// </summary>
public static string Menu_Behaviour_BackgroundUpdates_Title {
get {
return ResourceManager.GetString("Menu.Behaviour.BackgroundUpdates.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Prevent against closures of your existing game from accidentally launching another one..
/// </summary>

View File

@ -1239,6 +1239,9 @@ Would you like to enable test mode?</value>
<data name="Dialog.Exception.Version" xml:space="preserve">
<value>Version {0}</value>
</data>
<data name="Enums.BootstrapperStyle.CustomDialog" xml:space="preserve">
<value>Custom</value>
</data>
<data name="Bootstrapper.FilesInUse" xml:space="preserve">
<value>Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use.
@ -1267,4 +1270,221 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
<value>All Bloxstrap logs</value>
<comment>Label that appears next to a checkbox</comment>
</data>
<data name="App.OSDeprecation.Win7_81" xml:space="preserve">
<value>Roblox no longer supports Windows 7 or 8.1. To continue playing Roblox, please upgrade to Windows 10 or newer.</value>
</data>
<data name="Bootstrapper.ExtractionFailed.Title" xml:space="preserve">
<value>Failed to extract all files</value>
</data>
<data name="Bootstrapper.ExtractionFailed.Message" xml:space="preserve">
<value>Some content may be missing. Force a Roblox reinstallation in settings to fix this.</value>
</data>
<data name="Bootstrapper.ModificationsFailed.Title" xml:space="preserve">
<value>Failed to apply all modifications</value>
</data>
<data name="Bootstrapper.ModificationsFailed.Message" xml:space="preserve">
<value>Not all modifications will be present in the current launch.</value>
</data>
<data name="Menu.About.Licenses.Apache" xml:space="preserve">
<value>Apache License 2.0</value>
</data>
<data name="Enums.CustomThemeTemplate.Blank" xml:space="preserve">
<value>Blank</value>
</data>
<data name="Enums.CustomThemeTemplate.Simple" xml:space="preserve">
<value>Simple</value>
</data>
<data name="CustomTheme.Errors.InvalidRoot" xml:space="preserve">
<value>Theme XML root is not {0}</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.DialogAlreadyInitialised" xml:space="preserve">
<value>Custom dialog has already been initialised</value>
</data>
<data name="CustomTheme.Errors.TooManyElements" xml:space="preserve">
<value>Custom bootstrappers can only have a maximum of {0} elements, got {1}.</value>
<comment>{0} and {1} are numbers</comment>
</data>
<data name="CustomTheme.Errors.VersionNotSet" xml:space="preserve">
<value>{0} version is not set</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.VersionNotNumber" xml:space="preserve">
<value>{0} version is not a number</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.VersionNotSupported" xml:space="preserve">
<value>{0} version {1} is no longer supported</value>
<comment>{0} is the element name (e.g. Button), {1} is the version number</comment>
</data>
<data name="CustomTheme.Errors.VersionNotRecognised" xml:space="preserve">
<value>{0} version {1} is not recognised</value>
<comment>{0} is the element name (e.g. Button), {1} is the version number</comment>
</data>
<data name="CustomTheme.Errors.ElementInvalidChild" xml:space="preserve">
<value>{0} cannot have a child of {1}</value>
<comment>{0} and {1} are element names (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.UnknownElement" xml:space="preserve">
<value>Unknown element {0}</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.XMLParseFailed" xml:space="preserve">
<value>Failed to parse the theme file: {0}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeConversionError" xml:space="preserve">
<value>{0} has invalid {1}: {2}</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text), {2} is the error reason</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMissing" xml:space="preserve">
<value>Element {0} is missing the {1} attribute</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeInvalidType" xml:space="preserve">
<value>{0}.{1} is not a valid {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the type name (e.g. string)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMustBeLargerThanMin" xml:space="preserve">
<value>{0}.{1} must be larger than {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is a number</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax" xml:space="preserve">
<value>{0}.{1} must be smaller than {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is a number</comment>
</data>
<data name="CustomTheme.Errors.UnknownEnumValue" xml:space="preserve">
<value>{0} Unknown {1} '{2}'</value>
<comment>{0} is the element name (e.g. Button), {1} is the enum name (e.g. WindowCornerType), {2} is the value</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMultipleDefinitions" xml:space="preserve">
<value>{0} can only have one {1} defined</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMultipleChildren" xml:space="preserve">
<value>{0}.{1} can only have one child</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementMultipleChildren" xml:space="preserve">
<value>{0} can only have one child</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMissingChild" xml:space="preserve">
<value>{0}.{1} is missing its child</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeParseError" xml:space="preserve">
<value>{0}.{1} could not be parsed into a {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the type name (e.g. string)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeParseErrorNull" xml:space="preserve">
<value>{0}.{1} {2} is null</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the type name (e.g. string)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeBlacklistedUriScheme" xml:space="preserve">
<value>{0}.{1} uses blacklisted scheme {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the URI scheme (e.g. http)</comment>
</data>
<data name="CustomTheme.Errors.ElementTypeCreationFailed" xml:space="preserve">
<value>{0} failed to create {1}: {2}</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text), {2} is the error reason</comment>
</data>
<data name="CustomTheme.Editor.Title" xml:space="preserve">
<value>Editing "{0}"</value>
</data>
<data name="CustomTheme.Editor.Save.Success" xml:space="preserve">
<value>Theme successfully saved!</value>
</data>
<data name="CustomTheme.Editor.Save.Error" xml:space="preserve">
<value>An error occurred while saving your theme.</value>
</data>
<data name="CustomTheme.Editor.ConfirmSave" xml:space="preserve">
<value>Save changes to {0}?</value>
</data>
<data name="CustomTheme.Editor.Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="CustomTheme.Editor.Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="CustomTheme.Editor.OpenThemeDirectory" xml:space="preserve">
<value>Open Theme Directory</value>
</data>
<data name="Common.CreateNew" xml:space="preserve">
<value>Create New</value>
</data>
<data name="Common.Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="CustomTheme.Add.Title" xml:space="preserve">
<value>Add Custom Theme</value>
</data>
<data name="Common.Template" xml:space="preserve">
<value>Template</value>
</data>
<data name="CustomTheme.Add.Errors.NameEmpty" xml:space="preserve">
<value>Name cannot be empty</value>
</data>
<data name="CustomTheme.Add.Errors.NameIllegalCharacters" xml:space="preserve">
<value>Name contains illegal characters</value>
</data>
<data name="CustomTheme.Add.Errors.NameReserved" xml:space="preserve">
<value>Name cannot be used</value>
</data>
<data name="CustomTheme.Add.Errors.Unknown" xml:space="preserve">
<value>Unknown error</value>
</data>
<data name="CustomTheme.Add.Errors.NameTaken" xml:space="preserve">
<value>Name is already in use</value>
</data>
<data name="CustomTheme.Add.Errors.FileNotZip" xml:space="preserve">
<value>File must be a ZIP</value>
</data>
<data name="CustomTheme.Add.Errors.ZipMissingThemeFile" xml:space="preserve">
<value>Theme file could not be found in the ZIP file</value>
</data>
<data name="CustomTheme.Add.Errors.ZipInvalidData" xml:space="preserve">
<value>Invalid or corrupted ZIP file</value>
</data>
<data name="CustomTheme.Errors.NoThemeSelected" xml:space="preserve">
<value>No custom theme selected</value>
</data>
<data name="CustomTheme.Errors.SetupFailed" xml:space="preserve">
<value>Failed to setup custom bootstrapper: {0}.
Defaulting to {1}.</value>
<comment>{0} is the error reason, {1} is the theme name (e.g. Bloxstrap (Classic))</comment>
</data>
<data name="Menu.Appearance.CustomThemes.NoneSelected" xml:space="preserve">
<value>No custom theme selected.</value>
</data>
<data name="Common.Rename" xml:space="preserve">
<value>Rename</value>
</data>
<data name="Common.Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Menu.Appearance.CustomThemes.DeleteFailed" xml:space="preserve">
<value>Failed to delete custom theme {0}: {1}</value>
</data>
<data name="Menu.Appearance.CustomThemes.RenameFailed" xml:space="preserve">
<value>Failed to rename custom theme {0}: {1}</value>
</data>
<data name="Menu.Behaviour.BackgroundUpdates.Title" xml:space="preserve">
<value>Background updates</value>
</data>
<data name="Menu.Behaviour.BackgroundUpdates.Description" xml:space="preserve">
<value>Update Roblox in the background instead of waiting. Not recommended for slow networks. At least 3GB of free storage space is required for this feature to work.</value>
</data>
<data name="CustomTheme.Templates.Blank.UIElements" xml:space="preserve">
<value>Put UI elements here</value>
</data>
<data name="CustomTheme.Templates.Blank.MoreExamples" xml:space="preserve">
<value>Examples of custom bootstrappers can be found at {0}</value>
</data>
<data name="CustomTheme.Templates.Simple.MoreExamples" xml:space="preserve">
<value>Find more custom bootstrapper examples at {0}</value>
</data>
<data name="CustomTheme.DefaultName" xml:space="preserve">
<value>Custom Theme {0}</value>
<comment>{0} is a string (e.g. '1', '1-1234')</comment>
</data>
</root>

View File

@ -176,16 +176,15 @@
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");
App.Logger.WriteException(LOG_IDENT, ex);
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettings.roblox.com" + path);
}
// check if channel is behind LIVE
if (!isDefaultChannel)
{
var defaultClientVersion = await GetInfo(DefaultChannel);
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan)
clientVersion.IsBehindDefaultChannel = true;
try
{
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettings.roblox.com" + path);
}
catch (HttpRequestException httpEx)
when (!isDefaultChannel && BadChannelCodes.Contains(httpEx.StatusCode))
{
throw new InvalidChannelException(httpEx.StatusCode);
}
}
ClientVersionCache[cacheKey] = clientVersion;

View File

@ -10,8 +10,8 @@
mc:Ignorable="d"
Title="{x:Static resources:Strings.About_Title}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
MinWidth="740"
Width="740"
MinWidth="800"
Width="800"
Height="440"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"

View File

@ -43,30 +43,44 @@
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="DiscordRPC" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<ui:CardAction Grid.Row="1" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<ui:CardAction Grid.Row="1" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="13" Text="SharpZipLib" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<StackPanel>
<TextBlock FontSize="14" Text="Markdig" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/AvalonEdit/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="AvalonEdit" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/XamlAnimatedGif/XamlAnimatedGif/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="14" Text="XamlAnimatedGif" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_Apache}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -18,9 +18,15 @@ namespace Bloxstrap.UI.Elements.Base
public void ApplyTheme()
{
const int customThemeIndex = 2; // index for CustomTheme merged dictionary
_themeService.SetTheme(App.Settings.Prop.Theme.GetFinal() == Enums.Theme.Dark ? ThemeType.Dark : ThemeType.Light);
_themeService.SetSystemAccent();
// there doesn't seem to be a way to query the name for merged dictionaries
var dict = new ResourceDictionary { Source = new Uri($"pack://application:,,,/UI/Style/{Enum.GetName(App.Settings.Prop.Theme.GetFinal())}.xaml") };
Application.Current.Resources.MergedDictionaries[customThemeIndex] = dict;
#if QA_BUILD
this.BorderBrush = System.Windows.Media.Brushes.Red;
this.BorderThickness = new Thickness(4);

View File

@ -104,7 +104,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
public ByfronDialog()
{
string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false);
string version = Utilities.GetRobloxVersionStr(Bootstrapper?.IsStudioLaunch ?? false);
_viewModel = new ByfronDialogViewModel(this, version);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;

View File

@ -0,0 +1,90 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Xml;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
// https://stackoverflow.com/a/2961702
private static T? ConvertValue<T>(string input) where T : struct
{
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return (T?)converter.ConvertFromInvariantString(input);
}
return default;
}
catch (NotSupportedException)
{
return default;
}
}
private static object? GetTypeFromXElement(TypeConverter converter, XElement xmlElement, string attributeName)
{
string? attributeValue = xmlElement.Attribute(attributeName)?.Value?.ToString();
if (attributeValue == null)
return null;
try
{
return converter.ConvertFromInvariantString(attributeValue);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementAttributeConversionError", xmlElement.Name, attributeName, ex.Message);
}
}
private static ThicknessConverter ThicknessConverter { get; } = new ThicknessConverter();
private static object? GetThicknessFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(ThicknessConverter, xmlElement, attributeName);
private static RectConverter RectConverter { get; } = new RectConverter();
private static object? GetRectFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(RectConverter, xmlElement, attributeName);
private static ColorConverter ColorConverter { get; } = new ColorConverter();
private static object? GetColorFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(ColorConverter, xmlElement, attributeName);
private static PointConverter PointConverter { get; } = new PointConverter();
private static object? GetPointFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(PointConverter, xmlElement, attributeName);
private static CornerRadiusConverter CornerRadiusConverter { get; } = new CornerRadiusConverter();
private static object? GetCornerRadiusFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(CornerRadiusConverter, xmlElement, attributeName);
private static GridLengthConverter GridLengthConverter { get; } = new GridLengthConverter();
private static object? GetGridLengthFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(GridLengthConverter, xmlElement, attributeName);
private static BrushConverter BrushConverter { get; } = new BrushConverter();
/// <summary>
/// Return type of string = Name of DynamicResource
/// Return type of brush = ... The Brush!!!
/// </summary>
private static object? GetBrushFromXElement(XElement element, string attributeName)
{
string? value = element.Attribute(attributeName)?.Value?.ToString();
if (value == null)
return null;
// dynamic resource name
if (value.StartsWith('{') && value.EndsWith('}'))
return value[1..^1];
try
{
return BrushConverter.ConvertFromInvariantString(value);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementAttributeConversionError", element.Name, attributeName, ex.Message);
}
}
}
}

View File

@ -0,0 +1,151 @@
using System.Windows;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
const int Version = 1;
private class DummyFrameworkElement : FrameworkElement { }
private const int MaxElements = 100;
private bool _initialised = false;
// prevent users from creating elements with the same name multiple times
private List<string> UsedNames { get; } = new List<string>();
private string ThemeDir { get; set; } = "";
delegate object HandleXmlElementDelegate(CustomDialog dialog, XElement xmlElement);
private static Dictionary<string, HandleXmlElementDelegate> _elementHandlerMap = new Dictionary<string, HandleXmlElementDelegate>()
{
["BloxstrapCustomBootstrapper"] = HandleXmlElement_BloxstrapCustomBootstrapper_Fake,
["TitleBar"] = HandleXmlElement_TitleBar,
["Button"] = HandleXmlElement_Button,
["ProgressBar"] = HandleXmlElement_ProgressBar,
["ProgressRing"] = HandleXmlElement_ProgressRing,
["TextBlock"] = HandleXmlElement_TextBlock,
["MarkdownTextBlock"] = HandleXmlElement_MarkdownTextBlock,
["Image"] = HandleXmlElement_Image,
["Grid"] = HandleXmlElement_Grid,
["StackPanel"] = HandleXmlElement_StackPanel,
["Border"] = HandleXmlElement_Border,
["SolidColorBrush"] = HandleXmlElement_SolidColorBrush,
["ImageBrush"] = HandleXmlElement_ImageBrush,
["LinearGradientBrush"] = HandleXmlElement_LinearGradientBrush,
["GradientStop"] = HandleXmlElement_GradientStop,
["ScaleTransform"] = HandleXmlElement_ScaleTransform,
["SkewTransform"] = HandleXmlElement_SkewTransform,
["RotateTransform"] = HandleXmlElement_RotateTransform,
["TranslateTransform"] = HandleXmlElement_TranslateTransform,
["BlurEffect"] = HandleXmlElement_BlurEffect,
["DropShadowEffect"] = HandleXmlElement_DropShadowEffect,
["Ellipse"] = HandleXmlElement_Ellipse,
["Line"] = HandleXmlElement_Line,
["Rectangle"] = HandleXmlElement_Rectangle,
["RowDefinition"] = HandleXmlElement_RowDefinition,
["ColumnDefinition"] = HandleXmlElement_ColumnDefinition
};
private static T HandleXml<T>(CustomDialog dialog, XElement xmlElement) where T : class
{
if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString()))
throw new CustomThemeException("CustomTheme.Errors.UnknownElement", xmlElement.Name);
var element = _elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement);
if (element is not T)
throw new CustomThemeException("CustomTheme.Errors.ElementInvalidChild", xmlElement.Parent!.Name, xmlElement.Name);
return (T)element;
}
private static void AddXml(CustomDialog dialog, XElement xmlElement)
{
if (xmlElement.Name.ToString().StartsWith($"{xmlElement.Parent!.Name}."))
return; // not an xml element
var uiElement = HandleXml<UIElement>(dialog, xmlElement);
if (uiElement is not DummyFrameworkElement)
dialog.ElementGrid.Children.Add(uiElement);
}
private static void AssertThemeVersion(string? versionStr)
{
if (string.IsNullOrEmpty(versionStr))
throw new CustomThemeException("CustomTheme.Errors.VersionNotSet", "BloxstrapCustomBootstrapper");
if (!uint.TryParse(versionStr, out uint version))
throw new CustomThemeException("CustomTheme.Errors.VersionNotNumber", "BloxstrapCustomBootstrapper");
switch (version)
{
case Version:
break;
case 0: // Themes made between Oct 19, 2024 to Mar 11, 2025 (on the feature/custom-bootstrappers branch)
throw new CustomThemeException("CustomTheme.Errors.VersionNotSupported", "BloxstrapCustomBootstrapper", version);
default:
throw new CustomThemeException("CustomTheme.Errors.VersionNotRecognised", "BloxstrapCustomBootstrapper", version);
}
}
private void HandleXmlBase(XElement xml)
{
if (_initialised)
throw new CustomThemeException("CustomTheme.Errors.DialogAlreadyInitialised");
if (xml.Name != "BloxstrapCustomBootstrapper")
throw new CustomThemeException("CustomTheme.Errors.InvalidRoot", "BloxstrapCustomBootstrapper");
AssertThemeVersion(xml.Attribute("Version")?.Value);
if (xml.Descendants().Count() > MaxElements)
throw new CustomThemeException("CustomTheme.Errors.TooManyElements", MaxElements, xml.Descendants().Count());
_initialised = true;
// handle root
HandleXmlElement_BloxstrapCustomBootstrapper(this, xml);
// handle everything else
foreach (var child in xml.Elements())
AddXml(this, child);
}
#region Public APIs
public void ApplyCustomTheme(string name, string contents)
{
ThemeDir = System.IO.Path.Combine(Paths.CustomThemes, name);
XElement xml;
try
{
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
xml = XElement.Load(ms);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.XMLParseFailed", ex.Message);
}
HandleXmlBase(xml);
}
public void ApplyCustomTheme(string name)
{
string path = System.IO.Path.Combine(Paths.CustomThemes, name, "Theme.xml");
ApplyCustomTheme(name, File.ReadAllText(path));
}
#endregion
}
}

View File

@ -0,0 +1,772 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
using System.Xml.Linq;
using Wpf.Ui.Markup;
using Bloxstrap.UI.Elements.Controls;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
#region Transformation
private static Transform HandleXmlElement_ScaleTransform(CustomDialog dialog, XElement xmlElement)
{
var st = new ScaleTransform();
st.ScaleX = ParseXmlAttribute<double>(xmlElement, "ScaleX", 1);
st.ScaleY = ParseXmlAttribute<double>(xmlElement, "ScaleY", 1);
st.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
st.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
return st;
}
private static Transform HandleXmlElement_SkewTransform(CustomDialog dialog, XElement xmlElement)
{
var st = new SkewTransform();
st.AngleX = ParseXmlAttribute<double>(xmlElement, "AngleX", 0);
st.AngleY = ParseXmlAttribute<double>(xmlElement, "AngleY", 0);
st.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
st.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
return st;
}
private static Transform HandleXmlElement_RotateTransform(CustomDialog dialog, XElement xmlElement)
{
var rt = new RotateTransform();
rt.Angle = ParseXmlAttribute<double>(xmlElement, "Angle", 0);
rt.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
rt.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
return rt;
}
private static Transform HandleXmlElement_TranslateTransform(CustomDialog dialog, XElement xmlElement)
{
var tt = new TranslateTransform();
tt.X = ParseXmlAttribute<double>(xmlElement, "X", 0);
tt.Y = ParseXmlAttribute<double>(xmlElement, "Y", 0);
return tt;
}
#endregion
#region Effects
private static BlurEffect HandleXmlElement_BlurEffect(CustomDialog dialog, XElement xmlElement)
{
var effect = new BlurEffect();
effect.KernelType = ParseXmlAttribute<KernelType>(xmlElement, "KernelType", KernelType.Gaussian);
effect.Radius = ParseXmlAttribute<double>(xmlElement, "Radius", 5);
effect.RenderingBias = ParseXmlAttribute<RenderingBias>(xmlElement, "RenderingBias", RenderingBias.Performance);
return effect;
}
private static DropShadowEffect HandleXmlElement_DropShadowEffect(CustomDialog dialog, XElement xmlElement)
{
var effect = new DropShadowEffect();
effect.BlurRadius = ParseXmlAttribute<double>(xmlElement, "BlurRadius", 5);
effect.Direction = ParseXmlAttribute<double>(xmlElement, "Direction", 315);
effect.Opacity = ParseXmlAttribute<double>(xmlElement, "Opacity", 1);
effect.ShadowDepth = ParseXmlAttribute<double>(xmlElement, "ShadowDepth", 5);
effect.RenderingBias = ParseXmlAttribute<RenderingBias>(xmlElement, "RenderingBias", RenderingBias.Performance);
var color = GetColorFromXElement(xmlElement, "Color");
if (color is Color)
effect.Color = (Color)color;
return effect;
}
#endregion
#region Brushes
private static void HandleXml_Brush(Brush brush, XElement xmlElement)
{
brush.Opacity = ParseXmlAttribute<double>(xmlElement, "Opacity", 1.0);
}
private static Brush HandleXmlElement_SolidColorBrush(CustomDialog dialog, XElement xmlElement)
{
var brush = new SolidColorBrush();
HandleXml_Brush(brush, xmlElement);
object? color = GetColorFromXElement(xmlElement, "Color");
if (color is Color)
brush.Color = (Color)color;
return brush;
}
private static Brush HandleXmlElement_ImageBrush(CustomDialog dialog, XElement xmlElement)
{
var imageBrush = new ImageBrush();
HandleXml_Brush(imageBrush, xmlElement);
imageBrush.AlignmentX = ParseXmlAttribute<AlignmentX>(xmlElement, "AlignmentX", AlignmentX.Center);
imageBrush.AlignmentY = ParseXmlAttribute<AlignmentY>(xmlElement, "AlignmentY", AlignmentY.Center);
imageBrush.Stretch = ParseXmlAttribute<Stretch>(xmlElement, "Stretch", Stretch.Fill);
imageBrush.TileMode = ParseXmlAttribute<TileMode>(xmlElement, "TileMode", TileMode.None);
imageBrush.ViewboxUnits = ParseXmlAttribute<BrushMappingMode>(xmlElement, "ViewboxUnits", BrushMappingMode.RelativeToBoundingBox);
imageBrush.ViewportUnits = ParseXmlAttribute<BrushMappingMode>(xmlElement, "ViewportUnits", BrushMappingMode.RelativeToBoundingBox);
var viewbox = GetRectFromXElement(xmlElement, "Viewbox");
if (viewbox is Rect)
imageBrush.Viewbox = (Rect)viewbox;
var viewport = GetRectFromXElement(xmlElement, "Viewport");
if (viewport is Rect)
imageBrush.Viewport = (Rect)viewport;
var sourceData = GetImageSourceData(dialog, "ImageSource", xmlElement);
if (sourceData.IsIcon)
{
// bind the icon property
Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(imageBrush, ImageBrush.ImageSourceProperty, binding);
}
else
{
BitmapImage bitmapImage;
try
{
bitmapImage = new BitmapImage(sourceData.Uri!);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message);
}
imageBrush.ImageSource = bitmapImage;
}
return imageBrush;
}
private static GradientStop HandleXmlElement_GradientStop(CustomDialog dialog, XElement xmlElement)
{
var gs = new GradientStop();
object? color = GetColorFromXElement(xmlElement, "Color");
if (color is Color)
gs.Color = (Color)color;
gs.Offset = ParseXmlAttribute<double>(xmlElement, "Offset", 0.0);
return gs;
}
private static Brush HandleXmlElement_LinearGradientBrush(CustomDialog dialog, XElement xmlElement)
{
var brush = new LinearGradientBrush();
HandleXml_Brush(brush, xmlElement);
object? startPoint = GetPointFromXElement(xmlElement, "StartPoint");
if (startPoint is Point)
brush.StartPoint = (Point)startPoint;
object? endPoint = GetPointFromXElement(xmlElement, "EndPoint");
if (endPoint is Point)
brush.EndPoint = (Point)endPoint;
brush.ColorInterpolationMode = ParseXmlAttribute<ColorInterpolationMode>(xmlElement, "ColorInterpolationMode", ColorInterpolationMode.SRgbLinearInterpolation);
brush.MappingMode = ParseXmlAttribute<BrushMappingMode>(xmlElement, "MappingMode", BrushMappingMode.RelativeToBoundingBox);
brush.SpreadMethod = ParseXmlAttribute<GradientSpreadMethod>(xmlElement, "SpreadMethod", GradientSpreadMethod.Pad);
foreach (var child in xmlElement.Elements())
brush.GradientStops.Add(HandleXml<GradientStop>(dialog, child));
return brush;
}
private static void ApplyBrush_UIElement(CustomDialog dialog, FrameworkElement uiElement, string name, DependencyProperty dependencyProperty, XElement xmlElement)
{
// check if attribute exists
object? brushAttr = GetBrushFromXElement(xmlElement, name);
if (brushAttr is Brush)
{
uiElement.SetValue(dependencyProperty, brushAttr);
return;
}
else if (brushAttr is string)
{
uiElement.SetResourceReference(dependencyProperty, brushAttr);
return;
}
// check if element exists
var brushElement = xmlElement.Element($"{xmlElement.Name}.{name}");
if (brushElement == null)
return;
var first = brushElement.FirstNode as XElement;
if (first == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissingChild", xmlElement.Name, name);
var brush = HandleXml<Brush>(dialog, first);
uiElement.SetValue(dependencyProperty, brush);
}
#endregion
#region Shapes
private static void HandleXmlElement_Shape(CustomDialog dialog, Shape shape, XElement xmlElement)
{
HandleXmlElement_FrameworkElement(dialog, shape, xmlElement);
ApplyBrush_UIElement(dialog, shape, "Fill", Shape.FillProperty, xmlElement);
ApplyBrush_UIElement(dialog, shape, "Stroke", Shape.StrokeProperty, xmlElement);
shape.Stretch = ParseXmlAttribute<Stretch>(xmlElement, "Stretch", Stretch.Fill);
shape.StrokeDashCap = ParseXmlAttribute<PenLineCap>(xmlElement, "StrokeDashCap", PenLineCap.Flat);
shape.StrokeDashOffset = ParseXmlAttribute<double>(xmlElement, "StrokeDashOffset", 0);
shape.StrokeEndLineCap = ParseXmlAttribute<PenLineCap>(xmlElement, "StrokeEndLineCap", PenLineCap.Flat);
shape.StrokeLineJoin = ParseXmlAttribute<PenLineJoin>(xmlElement, "StrokeLineJoin", PenLineJoin.Miter);
shape.StrokeMiterLimit = ParseXmlAttribute<double>(xmlElement, "StrokeMiterLimit", 10);
shape.StrokeStartLineCap = ParseXmlAttribute<PenLineCap>(xmlElement, "StrokeStartLineCap", PenLineCap.Flat);
shape.StrokeThickness = ParseXmlAttribute<double>(xmlElement, "StrokeThickness", 1);
}
private static Ellipse HandleXmlElement_Ellipse(CustomDialog dialog, XElement xmlElement)
{
var ellipse = new Ellipse();
HandleXmlElement_Shape(dialog, ellipse, xmlElement);
return ellipse;
}
private static Line HandleXmlElement_Line(CustomDialog dialog, XElement xmlElement)
{
var line = new Line();
HandleXmlElement_Shape(dialog, line, xmlElement);
line.X1 = ParseXmlAttribute<double>(xmlElement, "X1", 0);
line.X2 = ParseXmlAttribute<double>(xmlElement, "X2", 0);
line.Y1 = ParseXmlAttribute<double>(xmlElement, "Y1", 0);
line.Y2 = ParseXmlAttribute<double>(xmlElement, "Y2", 0);
return line;
}
private static Rectangle HandleXmlElement_Rectangle(CustomDialog dialog, XElement xmlElement)
{
var rectangle = new Rectangle();
HandleXmlElement_Shape(dialog, rectangle, xmlElement);
rectangle.RadiusX = ParseXmlAttribute<double>(xmlElement, "RadiusX", 0);
rectangle.RadiusY = ParseXmlAttribute<double>(xmlElement, "RadiusY", 0);
return rectangle;
}
#endregion
#region Elements
private static void HandleXmlElement_FrameworkElement(CustomDialog dialog, FrameworkElement uiElement, XElement xmlElement)
{
// prevent two elements from having the same name
string? name = xmlElement.Attribute("Name")?.Value?.ToString();
if (name != null)
{
if (dialog.UsedNames.Contains(name))
throw new Exception($"{xmlElement.Name} has duplicate name {name}");
dialog.UsedNames.Add(name);
}
uiElement.Name = name;
uiElement.Visibility = ParseXmlAttribute<Visibility>(xmlElement, "Visibility", Visibility.Visible);
uiElement.IsEnabled = ParseXmlAttribute<bool>(xmlElement, "IsEnabled", true);
object? margin = GetThicknessFromXElement(xmlElement, "Margin");
if (margin != null)
uiElement.Margin = (Thickness)margin;
uiElement.Height = ParseXmlAttribute<double>(xmlElement, "Height", double.NaN);
uiElement.Width = ParseXmlAttribute<double>(xmlElement, "Width", double.NaN);
// default values of these were originally Stretch but that was no good
uiElement.HorizontalAlignment = ParseXmlAttribute<HorizontalAlignment>(xmlElement, "HorizontalAlignment", HorizontalAlignment.Left);
uiElement.VerticalAlignment = ParseXmlAttribute<VerticalAlignment>(xmlElement, "VerticalAlignment", VerticalAlignment.Top);
uiElement.Opacity = ParseXmlAttribute<double>(xmlElement, "Opacity", 1);
ApplyBrush_UIElement(dialog, uiElement, "OpacityMask", FrameworkElement.OpacityMaskProperty, xmlElement);
object? renderTransformOrigin = GetPointFromXElement(xmlElement, "RenderTransformOrigin");
if (renderTransformOrigin is Point)
uiElement.RenderTransformOrigin = (Point)renderTransformOrigin;
int zIndex = ParseXmlAttributeClamped(xmlElement, "Panel.ZIndex", defaultValue: 0, min: 0, max: 1000);
Panel.SetZIndex(uiElement, zIndex);
int gridRow = ParseXmlAttribute<int>(xmlElement, "Grid.Row", 0);
Grid.SetRow(uiElement, gridRow);
int gridRowSpan = ParseXmlAttribute<int>(xmlElement, "Grid.RowSpan", 1);
Grid.SetRowSpan(uiElement, gridRowSpan);
int gridColumn = ParseXmlAttribute<int>(xmlElement, "Grid.Column", 0);
Grid.SetColumn(uiElement, gridColumn);
int gridColumnSpan = ParseXmlAttribute<int>(xmlElement, "Grid.ColumnSpan", 1);
Grid.SetColumnSpan(uiElement, gridColumnSpan);
ApplyTransformations_UIElement(dialog, uiElement, xmlElement);
ApplyEffects_UIElement(dialog, uiElement, xmlElement);
}
private static void HandleXmlElement_Control(CustomDialog dialog, Control uiElement, XElement xmlElement)
{
HandleXmlElement_FrameworkElement(dialog, uiElement, xmlElement);
object? padding = GetThicknessFromXElement(xmlElement, "Padding");
if (padding != null)
uiElement.Padding = (Thickness)padding;
object? borderThickness = GetThicknessFromXElement(xmlElement, "BorderThickness");
if (borderThickness != null)
uiElement.BorderThickness = (Thickness)borderThickness;
ApplyBrush_UIElement(dialog, uiElement, "Foreground", Control.ForegroundProperty, xmlElement);
ApplyBrush_UIElement(dialog, uiElement, "Background", Control.BackgroundProperty, xmlElement);
ApplyBrush_UIElement(dialog, uiElement, "BorderBrush", Control.BorderBrushProperty, xmlElement);
var fontSize = ParseXmlAttributeNullable<double>(xmlElement, "FontSize");
if (fontSize is double)
uiElement.FontSize = (double)fontSize;
uiElement.FontWeight = GetFontWeightFromXElement(xmlElement);
uiElement.FontStyle = GetFontStyleFromXElement(xmlElement);
// NOTE: font family can both be the name of the font or a uri
string? fontFamily = GetFullPath(dialog, xmlElement.Attribute("FontFamily")?.Value);
if (fontFamily != null)
uiElement.FontFamily = new System.Windows.Media.FontFamily(fontFamily);
}
private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper(CustomDialog dialog, XElement xmlElement)
{
xmlElement.SetAttributeValue("Visibility", "Collapsed"); // don't show the bootstrapper yet!!!
xmlElement.SetAttributeValue("IsEnabled", "True");
HandleXmlElement_Control(dialog, dialog, xmlElement);
dialog.Opacity = 1;
// transfer effect to element grid
dialog.ElementGrid.RenderTransform = dialog.RenderTransform;
dialog.RenderTransform = null;
dialog.ElementGrid.LayoutTransform = dialog.LayoutTransform;
dialog.LayoutTransform = null;
dialog.ElementGrid.Effect = dialog.Effect;
dialog.Effect = null;
var theme = ParseXmlAttribute<Theme>(xmlElement, "Theme", Theme.Default);
if (theme == Theme.Default)
theme = App.Settings.Prop.Theme;
var wpfUiTheme = theme.GetFinal() == Theme.Dark ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light;
dialog.Resources.MergedDictionaries.Clear();
dialog.Resources.MergedDictionaries.Add(new ThemesDictionary() { Theme = wpfUiTheme });
dialog.DefaultBorderThemeOverwrite = wpfUiTheme;
dialog.WindowCornerPreference = ParseXmlAttribute<Wpf.Ui.Appearance.WindowCornerPreference>(xmlElement, "WindowCornerPreference", Wpf.Ui.Appearance.WindowCornerPreference.Round);
// disable default window border if border is modified
if (xmlElement.Attribute("BorderBrush") != null || xmlElement.Attribute("BorderThickness") != null)
dialog.DefaultBorderEnabled = false;
// set the margin & padding on the element grid
dialog.ElementGrid.Margin = dialog.Margin;
// TODO: put elementgrid inside a border?
dialog.Margin = new Thickness(0, 0, 0, 0);
dialog.Padding = new Thickness(0, 0, 0, 0);
string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap";
dialog.Title = title;
bool ignoreTitleBarInset = ParseXmlAttribute<bool>(xmlElement, "IgnoreTitleBarInset", false);
if (ignoreTitleBarInset)
{
Grid.SetRow(dialog.ElementGrid, 0);
Grid.SetRowSpan(dialog.ElementGrid, 2);
}
return new DummyFrameworkElement();
}
private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper_Fake(CustomDialog dialog, XElement xmlElement)
{
// this only exists to error out the theme if someone tries to use two BloxstrapCustomBootstrappers
throw new Exception($"{xmlElement.Parent!.Name} cannot have a child of {xmlElement.Name}");
}
private static DummyFrameworkElement HandleXmlElement_TitleBar(CustomDialog dialog, XElement xmlElement)
{
xmlElement.SetAttributeValue("Name", "TitleBar"); // prevent two titlebars from existing
xmlElement.SetAttributeValue("IsEnabled", "True");
HandleXmlElement_Control(dialog, dialog.RootTitleBar, xmlElement);
// get rid of all effects
dialog.RootTitleBar.RenderTransform = null;
dialog.RootTitleBar.LayoutTransform = null;
dialog.RootTitleBar.Effect = null;
Panel.SetZIndex(dialog.RootTitleBar, 1001); // always show above others
// properties we dont want modifiable
dialog.RootTitleBar.Height = double.NaN;
dialog.RootTitleBar.Width = double.NaN;
dialog.RootTitleBar.HorizontalAlignment = HorizontalAlignment.Stretch;
dialog.RootTitleBar.Margin = new Thickness(0, 0, 0, 0);
dialog.RootTitleBar.ShowMinimize = ParseXmlAttribute<bool>(xmlElement, "ShowMinimize", true);
dialog.RootTitleBar.ShowClose = ParseXmlAttribute<bool>(xmlElement, "ShowClose", true);
string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap";
dialog.RootTitleBar.Title = title;
return new DummyFrameworkElement(); // dont add anything
}
private static UIElement HandleXmlElement_Button(CustomDialog dialog, XElement xmlElement)
{
var button = new Button();
HandleXmlElement_Control(dialog, button, xmlElement);
button.Content = GetContentFromXElement(dialog, xmlElement);
if (xmlElement.Attribute("Name")?.Value == "CancelButton")
{
Binding cancelEnabledBinding = new Binding("CancelEnabled") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(button, Button.IsEnabledProperty, cancelEnabledBinding);
Binding cancelCommandBinding = new Binding("CancelInstallCommand");
BindingOperations.SetBinding(button, Button.CommandProperty, cancelCommandBinding);
}
return button;
}
private static void HandleXmlElement_RangeBase(CustomDialog dialog, RangeBase rangeBase, XElement xmlElement)
{
HandleXmlElement_Control(dialog, rangeBase, xmlElement);
rangeBase.Value = ParseXmlAttribute<double>(xmlElement, "Value", 0);
rangeBase.Maximum = ParseXmlAttribute<double>(xmlElement, "Maximum", 100);
}
private static UIElement HandleXmlElement_ProgressBar(CustomDialog dialog, XElement xmlElement)
{
var progressBar = new Wpf.Ui.Controls.ProgressBar();
HandleXmlElement_RangeBase(dialog, progressBar, xmlElement);
progressBar.IsIndeterminate = ParseXmlAttribute<bool>(xmlElement, "IsIndeterminate", false);
object? cornerRadius = GetCornerRadiusFromXElement(xmlElement, "CornerRadius");
if (cornerRadius != null)
progressBar.CornerRadius = (CornerRadius)cornerRadius;
object? indicatorCornerRadius = GetCornerRadiusFromXElement(xmlElement, "IndicatorCornerRadius");
if (indicatorCornerRadius != null)
progressBar.IndicatorCornerRadius = (CornerRadius)indicatorCornerRadius;
if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressBar")
{
Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, ProgressBar.IsIndeterminateProperty, isIndeterminateBinding);
Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, ProgressBar.MaximumProperty, maximumBinding);
Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, ProgressBar.ValueProperty, valueBinding);
}
return progressBar;
}
private static UIElement HandleXmlElement_ProgressRing(CustomDialog dialog, XElement xmlElement)
{
var progressBar = new Wpf.Ui.Controls.ProgressRing();
HandleXmlElement_RangeBase(dialog, progressBar, xmlElement);
progressBar.IsIndeterminate = ParseXmlAttribute<bool>(xmlElement, "IsIndeterminate", false);
if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressRing")
{
Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.IsIndeterminateProperty, isIndeterminateBinding);
Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.MaximumProperty, maximumBinding);
Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.ValueProperty, valueBinding);
}
return progressBar;
}
private static void HandleXmlElement_TextBlock_Base(CustomDialog dialog, TextBlock textBlock, XElement xmlElement)
{
HandleXmlElement_FrameworkElement(dialog, textBlock, xmlElement);
ApplyBrush_UIElement(dialog, textBlock, "Foreground", TextBlock.ForegroundProperty, xmlElement);
ApplyBrush_UIElement(dialog, textBlock, "Background", TextBlock.BackgroundProperty, xmlElement);
var fontSize = ParseXmlAttributeNullable<double>(xmlElement, "FontSize");
if (fontSize is double)
textBlock.FontSize = (double)fontSize;
textBlock.FontWeight = GetFontWeightFromXElement(xmlElement);
textBlock.FontStyle = GetFontStyleFromXElement(xmlElement);
textBlock.LineHeight = ParseXmlAttribute<double>(xmlElement, "LineHeight", double.NaN);
textBlock.LineStackingStrategy = ParseXmlAttribute<LineStackingStrategy>(xmlElement, "LineStackingStrategy", LineStackingStrategy.MaxHeight);
textBlock.TextAlignment = ParseXmlAttribute<TextAlignment>(xmlElement, "TextAlignment", TextAlignment.Center);
textBlock.TextTrimming = ParseXmlAttribute<TextTrimming>(xmlElement, "TextTrimming", TextTrimming.None);
textBlock.TextWrapping = ParseXmlAttribute<TextWrapping>(xmlElement, "TextWrapping", TextWrapping.NoWrap);
textBlock.TextDecorations = GetTextDecorationsFromXElement(xmlElement);
textBlock.IsHyphenationEnabled = ParseXmlAttribute<bool>(xmlElement, "IsHyphenationEnabled", false);
textBlock.BaselineOffset = ParseXmlAttribute<double>(xmlElement, "BaselineOffset", double.NaN);
// NOTE: font family can both be the name of the font or a uri
string? fontFamily = GetFullPath(dialog, xmlElement.Attribute("FontFamily")?.Value);
if (fontFamily != null)
textBlock.FontFamily = new System.Windows.Media.FontFamily(fontFamily);
object? padding = GetThicknessFromXElement(xmlElement, "Padding");
if (padding != null)
textBlock.Padding = (Thickness)padding;
}
private static UIElement HandleXmlElement_TextBlock(CustomDialog dialog, XElement xmlElement)
{
var textBlock = new TextBlock();
HandleXmlElement_TextBlock_Base(dialog, textBlock, xmlElement);
textBlock.Text = GetTranslatedText(xmlElement.Attribute("Text")?.Value);
if (xmlElement.Attribute("Name")?.Value == "StatusText")
{
Binding textBinding = new Binding("Message") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, textBinding);
}
return textBlock;
}
private static UIElement HandleXmlElement_MarkdownTextBlock(CustomDialog dialog, XElement xmlElement)
{
var textBlock = new MarkdownTextBlock();
HandleXmlElement_TextBlock_Base(dialog, textBlock, xmlElement);
string? text = GetTranslatedText(xmlElement.Attribute("Text")?.Value);
if (text != null)
textBlock.MarkdownText = text;
return textBlock;
}
private static UIElement HandleXmlElement_Image(CustomDialog dialog, XElement xmlElement)
{
var image = new Image();
HandleXmlElement_FrameworkElement(dialog, image, xmlElement);
image.Stretch = ParseXmlAttribute<Stretch>(xmlElement, "Stretch", Stretch.Uniform);
image.StretchDirection = ParseXmlAttribute<StretchDirection>(xmlElement, "StretchDirection", StretchDirection.Both);
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); // should this be modifiable by the user?
var sourceData = GetImageSourceData(dialog, "Source", xmlElement);
if (sourceData.IsIcon)
{
// bind the icon property
Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(image, Image.SourceProperty, binding);
}
else
{
bool isAnimated = ParseXmlAttribute<bool>(xmlElement, "IsAnimated", false);
if (!isAnimated)
{
BitmapImage bitmapImage;
try
{
bitmapImage = new BitmapImage(sourceData.Uri!);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message);
}
image.Source = bitmapImage;
}
else
{
XamlAnimatedGif.AnimationBehavior.SetSourceUri(image, sourceData.Uri!);
}
}
return image;
}
private static RowDefinition HandleXmlElement_RowDefinition(CustomDialog dialog, XElement xmlElement)
{
var rowDefinition = new RowDefinition();
var height = GetGridLengthFromXElement(xmlElement, "Height");
if (height != null)
rowDefinition.Height = (GridLength)height;
rowDefinition.MinHeight = ParseXmlAttribute<double>(xmlElement, "MinHeight", 0);
rowDefinition.MaxHeight = ParseXmlAttribute<double>(xmlElement, "MaxHeight", double.PositiveInfinity);
return rowDefinition;
}
private static ColumnDefinition HandleXmlElement_ColumnDefinition(CustomDialog dialog, XElement xmlElement)
{
var columnDefinition = new ColumnDefinition();
var width = GetGridLengthFromXElement(xmlElement, "Width");
if (width != null)
columnDefinition.Width = (GridLength)width;
columnDefinition.MinWidth = ParseXmlAttribute<double>(xmlElement, "MinWidth", 0);
columnDefinition.MaxWidth = ParseXmlAttribute<double>(xmlElement, "MaxWidth", double.PositiveInfinity);
return columnDefinition;
}
private static void HandleXmlElement_Grid_RowDefinitions(Grid grid, CustomDialog dialog, XElement xmlElement)
{
foreach (var element in xmlElement.Elements())
{
var rowDefinition = HandleXml<RowDefinition>(dialog, element);
grid.RowDefinitions.Add(rowDefinition);
}
}
private static void HandleXmlElement_Grid_ColumnDefinitions(Grid grid, CustomDialog dialog, XElement xmlElement)
{
foreach (var element in xmlElement.Elements())
{
var columnDefinition = HandleXml<ColumnDefinition>(dialog, element);
grid.ColumnDefinitions.Add(columnDefinition);
}
}
private static Grid HandleXmlElement_Grid(CustomDialog dialog, XElement xmlElement)
{
var grid = new Grid();
HandleXmlElement_FrameworkElement(dialog, grid, xmlElement);
bool rowsSet = false;
bool columnsSet = false;
foreach (var element in xmlElement.Elements())
{
if (element.Name == "Grid.RowDefinitions")
{
if (rowsSet)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", "Grid", "RowDefinitions");
rowsSet = true;
HandleXmlElement_Grid_RowDefinitions(grid, dialog, element);
}
else if (element.Name == "Grid.ColumnDefinitions")
{
if (columnsSet)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", "Grid", "ColumnDefinitions");
columnsSet = true;
HandleXmlElement_Grid_ColumnDefinitions(grid, dialog, element);
}
else if (element.Name.ToString().StartsWith("Grid."))
{
continue; // ignore others
}
else
{
var uiElement = HandleXml<FrameworkElement>(dialog, element);
grid.Children.Add(uiElement);
}
}
return grid;
}
private static StackPanel HandleXmlElement_StackPanel(CustomDialog dialog, XElement xmlElement)
{
var stackPanel = new StackPanel();
HandleXmlElement_FrameworkElement(dialog, stackPanel, xmlElement);
stackPanel.Orientation = ParseXmlAttribute<Orientation>(xmlElement, "Orientation", Orientation.Vertical);
foreach (var element in xmlElement.Elements())
{
var uiElement = HandleXml<FrameworkElement>(dialog, element);
stackPanel.Children.Add(uiElement);
}
return stackPanel;
}
private static Border HandleXmlElement_Border(CustomDialog dialog, XElement xmlElement)
{
var border = new Border();
HandleXmlElement_FrameworkElement(dialog, border, xmlElement);
ApplyBrush_UIElement(dialog, border, "Background", Border.BackgroundProperty, xmlElement);
ApplyBrush_UIElement(dialog, border, "BorderBrush", Border.BorderBrushProperty, xmlElement);
object? borderThickness = GetThicknessFromXElement(xmlElement, "BorderThickness");
if (borderThickness != null)
border.BorderThickness = (Thickness)borderThickness;
object? padding = GetThicknessFromXElement(xmlElement, "Padding");
if (padding != null)
border.Padding = (Thickness)padding;
object? cornerRadius = GetCornerRadiusFromXElement(xmlElement, "CornerRadius");
if (cornerRadius != null)
border.CornerRadius = (CornerRadius)cornerRadius;
var children = xmlElement.Elements().Where(x => !x.Name.ToString().StartsWith("Border."));
if (children.Any())
{
if (children.Count() > 1)
throw new CustomThemeException("CustomTheme.Errors.ElementMultipleChildren", "Border");
border.Child = HandleXml<UIElement>(dialog, children.First());
}
return border;
}
#endregion
}
}

View File

@ -0,0 +1,300 @@
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
struct GetImageSourceDataResult
{
public bool IsIcon = false;
public Uri? Uri = null;
public GetImageSourceDataResult()
{
}
}
private static string GetXmlAttribute(XElement element, string attributeName, string? defaultValue = null)
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
{
if (defaultValue != null)
return defaultValue;
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissing", element.Name, attributeName);
}
return attribute.Value.ToString();
}
private static T ParseXmlAttribute<T>(XElement element, string attributeName, T? defaultValue = null) where T : struct
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
{
if (defaultValue != null)
return (T)defaultValue;
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissing", element.Name, attributeName);
}
T? parsed = ConvertValue<T>(attribute.Value);
if (parsed == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name);
return (T)parsed;
}
/// <summary>
/// ParseXmlAttribute but the default value is always null
/// </summary>
private static T? ParseXmlAttributeNullable<T>(XElement element, string attributeName) where T : struct
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
return null;
T? parsed = ConvertValue<T>(attribute.Value);
if (parsed == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name);
return (T)parsed;
}
private static void ValidateXmlElement(string elementName, string attributeName, int value, int? min = null, int? max = null)
{
if (min != null && value < min)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min);
if (max != null && value > max)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax", elementName, attributeName, max);
}
private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null)
{
if (min != null && value < min)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min);
if (max != null && value > max)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax", elementName, attributeName, max);
}
// You can't do numeric only generics in .NET 6. The feature is exclusive to .NET 7+.
private static int ParseXmlAttributeClamped(XElement element, string attributeName, int? defaultValue = null, int? min = null, int? max = null)
{
int value = ParseXmlAttribute<int>(element, attributeName, defaultValue);
ValidateXmlElement(element.Name.ToString(), attributeName, value, min, max);
return value;
}
private static FontWeight GetFontWeightFromXElement(XElement element)
{
string? value = element.Attribute("FontWeight")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
value = "Normal";
// bruh
// https://learn.microsoft.com/en-us/dotnet/api/system.windows.fontweights?view=windowsdesktop-6.0
switch (value)
{
case "Thin":
return FontWeights.Thin;
case "ExtraLight":
case "UltraLight":
return FontWeights.ExtraLight;
case "Medium":
return FontWeights.Medium;
case "Normal":
case "Regular":
return FontWeights.Normal;
case "DemiBold":
case "SemiBold":
return FontWeights.DemiBold;
case "Bold":
return FontWeights.Bold;
case "ExtraBold":
case "UltraBold":
return FontWeights.ExtraBold;
case "Black":
case "Heavy":
return FontWeights.Black;
case "ExtraBlack":
case "UltraBlack":
return FontWeights.UltraBlack;
default:
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "FontWeight", value);
}
}
private static FontStyle GetFontStyleFromXElement(XElement element)
{
string? value = element.Attribute("FontStyle")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
value = "Normal";
switch (value)
{
case "Normal":
return FontStyles.Normal;
case "Italic":
return FontStyles.Italic;
case "Oblique":
return FontStyles.Oblique;
default:
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "FontStyle", value);
}
}
private static TextDecorationCollection? GetTextDecorationsFromXElement(XElement element)
{
string? value = element.Attribute("TextDecorations")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
return null;
switch (value)
{
case "Baseline":
return TextDecorations.Baseline;
case "OverLine":
return TextDecorations.OverLine;
case "Strikethrough":
return TextDecorations.Strikethrough;
case "Underline":
return TextDecorations.Underline;
default:
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "TextDecorations", value);
}
}
private static string? GetTranslatedText(string? text)
{
if (text == null || !text.StartsWith('{') || !text.EndsWith('}'))
return text; // can't be translated (not in the correct format)
string resourceName = text[1..^1];
if (resourceName == "Version")
return App.Version;
return Strings.ResourceManager.GetStringSafe(resourceName);
}
private static string? GetFullPath(CustomDialog dialog, string? sourcePath)
{
if (sourcePath == null)
return null;
// TODO: this is bad :(
return sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\");
}
private static GetImageSourceDataResult GetImageSourceData(CustomDialog dialog, string name, XElement xmlElement)
{
string path = GetXmlAttribute(xmlElement, name);
if (path == "{Icon}")
return new GetImageSourceDataResult { IsIcon = true };
path = GetFullPath(dialog, path)!;
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out Uri? result))
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeParseError", xmlElement.Name, name, "Uri");
if (result == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeParseErrorNull", xmlElement.Name, name, "Uri");
if (result.Scheme != "file")
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeBlacklistedUriScheme", xmlElement.Name, name, result.Scheme);
return new GetImageSourceDataResult { Uri = result };
}
private static object? GetContentFromXElement(CustomDialog dialog, XElement xmlElement)
{
var contentAttr = xmlElement.Attribute("Content");
var contentElement = xmlElement.Element($"{xmlElement.Name}.Content");
if (contentAttr != null && contentElement != null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", xmlElement.Name, "Content");
if (contentAttr != null)
return GetTranslatedText(contentAttr.Value);
if (contentElement == null)
return null;
var children = contentElement.Elements();
if (children.Count() > 1)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleChildren", xmlElement.Name, "Content");
var first = contentElement.FirstNode as XElement;
if (first == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissingChild", xmlElement.Name, "Content");
var uiElement = HandleXml<UIElement>(dialog, first);
return uiElement;
}
private static void ApplyEffects_UIElement(CustomDialog dialog, UIElement uiElement, XElement xmlElement)
{
var effectElement = xmlElement.Element($"{xmlElement.Name}.Effect");
if (effectElement == null)
return;
var children = effectElement.Elements();
if (children.Count() > 1)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleChildren", xmlElement.Name, "Effect");
var child = children.FirstOrDefault();
if (child == null)
return;
Effect effect = HandleXml<Effect>(dialog, child);
uiElement.Effect = effect;
}
private static void ApplyTransformation_UIElement(CustomDialog dialog, string name, DependencyProperty property, UIElement uiElement, XElement xmlElement)
{
var transformElement = xmlElement.Element($"{xmlElement.Name}.{name}");
if (transformElement == null)
return;
var tg = new TransformGroup();
foreach (var child in transformElement.Elements())
{
Transform element = HandleXml<Transform>(dialog, child);
tg.Children.Add(element);
}
uiElement.SetValue(property, tg);
}
private static void ApplyTransformations_UIElement(CustomDialog dialog, UIElement uiElement, XElement xmlElement)
{
ApplyTransformation_UIElement(dialog, "RenderTransform", FrameworkElement.RenderTransformProperty, uiElement, xmlElement);
ApplyTransformation_UIElement(dialog, "LayoutTransform", FrameworkElement.LayoutTransformProperty, uiElement, xmlElement);
}
}
}

View File

@ -0,0 +1,55 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Bootstrapper.CustomDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Bootstrapper"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="Bloxstrap"
Width="800"
Height="450"
MinWidth="150"
MinHeight="150"
MaxWidth="1000"
MaxHeight="1000"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
ResizeMode="NoResize"
WindowBackdropType="Disable"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressState="{Binding Path=TaskbarProgressState}" ProgressValue="{Binding Path=TaskbarProgressValue}" />
</Window.TaskbarItemInfo>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar
x:Name="RootTitleBar"
Title="Bloxstrap"
Grid.Row="0"
Padding="8"
Panel.ZIndex="1001"
CanMaximize="False"
ShowClose="False"
ShowMaximize="False"
ShowMinimize="False" />
<Grid x:Name="ElementGrid" Grid.Row="1" />
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,122 @@
using Bloxstrap.UI.Elements.Bootstrapper.Base;
using Bloxstrap.UI.ViewModels.Bootstrapper;
using System.ComponentModel;
using System.Windows.Forms;
using System.Windows.Shell;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
/// <summary>
/// Interaction logic for CustomDialog.xaml
/// </summary>
public partial class CustomDialog : IBootstrapperDialog
{
private readonly BootstrapperDialogViewModel _viewModel;
public Bloxstrap.Bootstrapper? Bootstrapper { get; set; }
private bool _isClosing;
#region UI Elements
public string Message
{
get => _viewModel.Message;
set
{
_viewModel.Message = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.Message));
}
}
public ProgressBarStyle ProgressStyle
{
get => _viewModel.ProgressIndeterminate ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous;
set
{
_viewModel.ProgressIndeterminate = (value == ProgressBarStyle.Marquee);
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressIndeterminate));
}
}
public int ProgressMaximum
{
get => _viewModel.ProgressMaximum;
set
{
_viewModel.ProgressMaximum = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressMaximum));
}
}
public int ProgressValue
{
get => _viewModel.ProgressValue;
set
{
_viewModel.ProgressValue = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressValue));
}
}
public TaskbarItemProgressState TaskbarProgressState
{
get => _viewModel.TaskbarProgressState;
set
{
_viewModel.TaskbarProgressState = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressState));
}
}
public double TaskbarProgressValue
{
get => _viewModel.TaskbarProgressValue;
set
{
_viewModel.TaskbarProgressValue = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressValue));
}
}
public bool CancelEnabled
{
get => _viewModel.CancelEnabled;
set
{
_viewModel.CancelEnabled = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility));
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelEnabled));
}
}
#endregion
public CustomDialog()
{
InitializeComponent();
_viewModel = new BootstrapperDialogViewModel(this);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
}
private void UiWindow_Closing(object sender, CancelEventArgs e)
{
if (!_isClosing)
Bootstrapper?.Cancel();
}
#region IBootstrapperDialog Methods
public void ShowBootstrapper() => this.ShowDialog();
public void CloseBootstrapper()
{
_isClosing = true;
Dispatcher.BeginInvoke(this.Close);
}
public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback);
#endregion
}
}

View File

@ -0,0 +1,165 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Dialogs.AddCustomThemeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:viewmodels="clr-namespace:Bloxstrap.UI.ViewModels.Dialogs"
Title="Add Custom Theme"
Width="480"
MinHeight="0"
d:DataContext="{d:DesignInstance viewmodels:AddCustomThemeViewModel,
IsDesignTimeCreatable=True}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar
Title="{x:Static resources:Strings.CustomTheme_Add_Title}"
Grid.Row="0"
Grid.ColumnSpan="2"
Padding="8"
CanMaximize="False"
KeyboardNavigation.TabNavigation="None"
ShowMaximize="False"
ShowMinimize="False" />
<TabControl
x:Name="Tabs"
Grid.Row="1"
Margin="16"
SelectedIndex="{Binding Path=SelectedTab, Mode=TwoWay}">
<TabItem Header="{x:Static resources:Strings.Common_CreateNew}">
<Grid Grid.Row="1" Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid
Grid.Row="0"
Grid.ColumnSpan="2"
Margin="0,0,0,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
MinWidth="100"
VerticalAlignment="Center"
Text="{x:Static resources:Strings.Common_Name}" />
<TextBox
Grid.Row="0"
Grid.Column="1"
Text="{Binding Path=Name, Mode=TwoWay}" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Text="{Binding Path=NameError, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="{Binding Path=NameErrorVisibility, Mode=OneWay}" />
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="0"
MinWidth="100"
VerticalAlignment="Center"
Text="{x:Static resources:Strings.Common_Template}" />
<ComboBox
Grid.Row="1"
Grid.Column="1"
ItemsSource="{Binding Path=Templates, Mode=OneTime}"
Text="{Binding Path=Template, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</TabItem>
<TabItem Header="{x:Static resources:Strings.Common_Import}">
<Grid Margin="11">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
FontSize="14"
Text="{Binding Path=FilePath}"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="{Binding Path=FilePathVisibility}" />
<ui:Button
Grid.Row="1"
Margin="4"
HorizontalAlignment="Stretch"
Click="OnImportButtonClicked"
Content="{x:Static resources:Strings.Common_ImportFromFile}"
Icon="DocumentArrowUp16" />
<TextBlock
Grid.Row="2"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Text="{Binding Path=FileError}"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="{Binding Path=FileErrorVisibility}" />
</Grid>
</TabItem>
</TabControl>
<Border
Grid.Row="2"
Margin="0,10,0,0"
Padding="15"
Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel
HorizontalAlignment="Right"
FlowDirection="LeftToRight"
Orientation="Horizontal">
<Button
MinWidth="100"
Click="OnOkButtonClicked"
Content="{x:Static resources:Strings.Common_OK}" />
<Button
MinWidth="100"
Margin="12,0,0,0"
Content="{x:Static resources:Strings.Common_Cancel}"
IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,231 @@
using Bloxstrap.UI.Elements.Base;
using Bloxstrap.UI.ViewModels.Dialogs;
using Microsoft.Win32;
using System.IO.Compression;
using System.Windows;
namespace Bloxstrap.UI.Elements.Dialogs
{
/// <summary>
/// Interaction logic for AddCustomThemeDialog.xaml
/// </summary>
public partial class AddCustomThemeDialog : WpfUiWindow
{
private const int CreateNewTabId = 0;
private const int ImportTabId = 1;
private readonly AddCustomThemeViewModel _viewModel;
public bool Created { get; private set; } = false;
public string ThemeName { get; private set; } = "";
public bool OpenEditor { get; private set; } = false;
public AddCustomThemeDialog()
{
_viewModel = new AddCustomThemeViewModel();
_viewModel.Name = GenerateRandomName();
DataContext = _viewModel;
InitializeComponent();
}
private static string GetThemePath(string name)
{
return Path.Combine(Paths.CustomThemes, name, "Theme.xml");
}
private static string GenerateRandomName()
{
int count = Directory.GetDirectories(Paths.CustomThemes).Count();
int i = count + 1;
string name = string.Format(Strings.CustomTheme_DefaultName, i);
// TODO: this sucks
if (File.Exists(GetThemePath(name)))
name = string.Format(Strings.CustomTheme_DefaultName, $"{i}-{Random.Shared.Next(1, 100000)}"); // easy
return name;
}
private static string GetUniqueName(string name)
{
const int maxTries = 100;
if (!File.Exists(GetThemePath(name)))
return name;
for (int i = 1; i <= maxTries; i++)
{
string newName = $"{name}_{i}";
if (!File.Exists(GetThemePath(newName)))
return newName;
}
// last resort
return $"{name}_{Random.Shared.Next(maxTries+1, 1_000_000)}";
}
private static void CreateCustomTheme(string name, CustomThemeTemplate template)
{
string dir = Path.Combine(Paths.CustomThemes, name);
if (Directory.Exists(dir))
Directory.Delete(dir, true);
Directory.CreateDirectory(dir);
string themeFilePath = Path.Combine(dir, "Theme.xml");
string templateContent = template.GetFileContents();
File.WriteAllText(themeFilePath, templateContent);
}
private bool ValidateCreateNew()
{
const string LOG_IDENT = "AddCustomThemeDialog::ValidateCreateNew";
if (string.IsNullOrEmpty(_viewModel.Name))
{
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameEmpty;
return false;
}
var validationResult = PathValidator.IsFileNameValid(_viewModel.Name);
if (validationResult != PathValidator.ValidationResult.Ok)
{
switch (validationResult)
{
case PathValidator.ValidationResult.IllegalCharacter:
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameIllegalCharacters;
break;
case PathValidator.ValidationResult.ReservedFileName:
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameReserved;
break;
default:
App.Logger.WriteLine(LOG_IDENT, $"Got unhandled PathValidator::ValidationResult {validationResult}");
Debug.Assert(false);
_viewModel.NameError = Strings.CustomTheme_Add_Errors_Unknown;
break;
}
return false;
}
// better to check for the file instead of the directory so broken themes can be overwritten
string path = Path.Combine(Paths.CustomThemes, _viewModel.Name, "Theme.xml");
if (File.Exists(path))
{
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameTaken;
return false;
}
return true;
}
private bool ValidateImport()
{
const string LOG_IDENT = "AddCustomThemeDialog::ValidateImport";
if (!_viewModel.FilePath.EndsWith(".zip"))
{
_viewModel.FileError = Strings.CustomTheme_Add_Errors_FileNotZip;
return false;
}
try
{
using var zipFile = ZipFile.OpenRead(_viewModel.FilePath);
var entries = zipFile.Entries;
bool foundThemeFile = false;
foreach (var entry in entries)
{
if (entry.FullName == "Theme.xml")
{
foundThemeFile = true;
break;
}
}
if (!foundThemeFile)
{
_viewModel.FileError = Strings.CustomTheme_Add_Errors_ZipMissingThemeFile;
return false;
}
return true;
}
catch (InvalidDataException ex)
{
App.Logger.WriteLine(LOG_IDENT, "Got invalid data");
App.Logger.WriteException(LOG_IDENT, ex);
_viewModel.FileError = Strings.CustomTheme_Add_Errors_ZipInvalidData;
return false;
}
}
private void CreateNew()
{
if (!ValidateCreateNew())
return;
CreateCustomTheme(_viewModel.Name, _viewModel.Template);
Created = true;
ThemeName = _viewModel.Name;
OpenEditor = true;
Close();
}
private void Import()
{
if (!ValidateImport())
return;
string fileName = Path.GetFileNameWithoutExtension(_viewModel.FilePath);
string name = GetUniqueName(fileName);
string directory = Path.Combine(Paths.CustomThemes, name);
if (Directory.Exists(directory))
Directory.Delete(directory, true);
Directory.CreateDirectory(directory);
var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip();
fastZip.ExtractZip(_viewModel.FilePath, directory, null);
Created = true;
ThemeName = name;
OpenEditor = false;
Close();
}
private void OnOkButtonClicked(object sender, RoutedEventArgs e)
{
if (_viewModel.SelectedTab == CreateNewTabId)
CreateNew();
else
Import();
}
private void OnImportButtonClicked(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Filter = $"{Strings.FileTypes_ZipArchive}|*.zip"
};
if (dialog.ShowDialog() != true)
return;
_viewModel.FilePath = dialog.FileName;
}
}
}

View File

@ -0,0 +1,84 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Editor.BootstrapperEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Editor"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Editor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="{Binding Path=Title, Mode=OneTime}"
Width="1000"
Height="500"
d:DataContext="{d:DesignInstance dmodels:BootstrapperEditorWindowViewModel,
IsDesignTimeCreatable=True}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
Closing="OnClosing"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar
x:Name="RootTitleBar"
Title="{Binding Path=Title, Mode=OneTime}"
Grid.Row="0"
Padding="8"
ForceShutdown="False"
Icon="pack://application:,,,/Bloxstrap.ico"
MinimizeToTray="False"
UseSnapLayout="True" />
<avalonedit:TextEditor
x:Name="UIXML"
Grid.Row="1"
Margin="10,10,10,0"
ShowLineNumbers="True"
Style="{StaticResource NewTextEditor}"
SyntaxHighlighting="XML" />
<ui:Button
Grid.Row="2"
Margin="10"
Command="{Binding Path=OpenThemeFolderCommand, Mode=OneTime}"
Content="{x:Static resources:Strings.CustomTheme_Editor_OpenThemeDirectory}" />
<Grid
Grid.Row="2"
Margin="10"
HorizontalAlignment="Right">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:Button
Grid.Column="0"
Margin="0,0,4,0"
Command="{Binding Path=PreviewCommand, Mode=OneTime}"
Content="{x:Static resources:Strings.CustomTheme_Editor_Preview}" />
<ui:Button
Grid.Column="1"
Margin="4,0,0,0"
Appearance="Primary"
Command="{Binding Path=SaveCommand, Mode=OneTime}"
Content="{x:Static resources:Strings.CustomTheme_Editor_Save}" />
</Grid>
<ui:Snackbar
x:Name="Snackbar"
Grid.RowSpan="3"
Margin="200,0,200,20"
Panel.ZIndex="9"
Timeout="3000" />
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,580 @@
using System.Windows.Input;
using System.Xml;
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
using ICSharpCode.AvalonEdit.Highlighting;
using Bloxstrap.UI.Elements.Base;
using Bloxstrap.UI.ViewModels.Editor;
using System.Windows;
namespace Bloxstrap.UI.Elements.Editor
{
/// <summary>
/// Interaction logic for BootstrapperEditorWindow.xaml
/// </summary>
public partial class BootstrapperEditorWindow : WpfUiWindow
{
private static class CustomBootstrapperSchema
{
private class Schema
{
public Dictionary<string, Element> Elements { get; set; } = new Dictionary<string, Element>();
public Dictionary<string, Type> Types { get; set; } = new Dictionary<string, Type>();
}
private class Element
{
public string? SuperClass { get; set; } = null;
public bool IsCreatable { get; set; } = false;
// [AttributeName] = [TypeName]
public Dictionary<string, string> Attributes { get; set; } = new Dictionary<string, string>();
}
public class Type
{
public bool CanHaveElement { get; set; } = false;
public List<string>? Values { get; set; } = null;
}
private static Schema? _schema;
/// <summary>
/// Elements and their attributes
/// </summary>
public static SortedDictionary<string, SortedDictionary<string, string>> ElementInfo { get; set; } = new();
/// <summary>
/// Attributes of elements that can have property elements
/// </summary>
public static Dictionary<string, List<string>> PropertyElements { get; set; } = new();
/// <summary>
/// All type info
/// </summary>
public static SortedDictionary<string, Type> Types { get; set; } = new();
public static void ParseSchema()
{
if (_schema != null)
return;
_schema = JsonSerializer.Deserialize<Schema>(Resource.GetString("CustomBootstrapperSchema.json").Result);
if (_schema == null)
throw new Exception("Deserialised CustomBootstrapperSchema is null");
foreach (var type in _schema.Types)
Types.Add(type.Key, type.Value);
PopulateElementInfo();
}
private static (SortedDictionary<string, string>, List<string>) GetElementAttributes(string name, Element element)
{
if (ElementInfo.ContainsKey(name))
return (ElementInfo[name], PropertyElements[name]);
List<string> properties = new List<string>();
SortedDictionary<string, string> attributes = new();
foreach (var attribute in element.Attributes)
{
attributes.Add(attribute.Key, attribute.Value);
if (!Types.ContainsKey(attribute.Value))
throw new Exception($"Schema for type {attribute.Value} is missing. Blame Matt!");
Type type = Types[attribute.Value];
if (type.CanHaveElement)
properties.Add(attribute.Key);
}
if (element.SuperClass != null)
{
(SortedDictionary<string, string> superAttributes, List<string> superProperties) = GetElementAttributes(element.SuperClass, _schema!.Elements[element.SuperClass]);
foreach (var attribute in superAttributes)
attributes.Add(attribute.Key, attribute.Value);
foreach (var property in superProperties)
properties.Add(property);
}
properties.Sort();
ElementInfo[name] = attributes;
PropertyElements[name] = properties;
return (attributes, properties);
}
private static void PopulateElementInfo()
{
List<string> toRemove = new List<string>();
foreach (var element in _schema!.Elements)
{
GetElementAttributes(element.Key, element.Value);
if (!element.Value.IsCreatable)
toRemove.Add(element.Key);
}
// remove non-creatable from list now that everything is done
foreach (var name in toRemove)
{
ElementInfo.Remove(name);
}
}
}
private BootstrapperEditorWindowViewModel _viewModel;
private CompletionWindow? _completionWindow = null;
public BootstrapperEditorWindow(string name)
{
CustomBootstrapperSchema.ParseSchema();
string directory = Path.Combine(Paths.CustomThemes, name);
string themeContents = File.ReadAllText(Path.Combine(directory, "Theme.xml"));
themeContents = ToCRLF(themeContents); // make sure the theme is in CRLF. a function expects CRLF.
_viewModel = new BootstrapperEditorWindowViewModel();
_viewModel.ThemeSavedCallback = ThemeSavedCallback;
_viewModel.Directory = directory;
_viewModel.Name = name;
_viewModel.Title = string.Format(Strings.CustomTheme_Editor_Title, name);
_viewModel.Code = themeContents;
DataContext = _viewModel;
InitializeComponent();
UIXML.Text = _viewModel.Code;
UIXML.TextChanged += OnCodeChanged;
UIXML.TextArea.TextEntered += OnTextAreaTextEntered;
LoadHighlightingTheme();
}
private void LoadHighlightingTheme()
{
string name = $"Editor-Theme-{App.Settings.Prop.Theme.GetFinal()}.xshd";
using Stream xmlStream = Resource.GetStream(name);
using XmlReader reader = XmlReader.Create(xmlStream);
UIXML.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
UIXML.TextArea.TextView.SetResourceReference(ICSharpCode.AvalonEdit.Rendering.TextView.LinkTextForegroundBrushProperty, "NewTextEditorLink");
}
private void ThemeSavedCallback(bool success, string message)
{
if (success)
Snackbar.Show(Strings.CustomTheme_Editor_Save_Success, message, Wpf.Ui.Common.SymbolRegular.CheckmarkCircle32, Wpf.Ui.Common.ControlAppearance.Success);
else
Snackbar.Show(Strings.CustomTheme_Editor_Save_Error, message, Wpf.Ui.Common.SymbolRegular.ErrorCircle24, Wpf.Ui.Common.ControlAppearance.Danger);
}
private static string ToCRLF(string text)
{
return text.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
}
private void OnCodeChanged(object? sender, EventArgs e)
{
_viewModel.Code = UIXML.Text;
_viewModel.CodeChanged = true;
}
private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (!_viewModel.CodeChanged)
return;
var result = Frontend.ShowMessageBox(string.Format(Strings.CustomTheme_Editor_ConfirmSave, _viewModel.Name), MessageBoxImage.Information, MessageBoxButton.YesNoCancel);
if (result == MessageBoxResult.Cancel)
{
e.Cancel = true;
}
else if (result == MessageBoxResult.Yes)
{
_viewModel.SaveCommand.Execute(null);
}
}
private void OnTextAreaTextEntered(object sender, TextCompositionEventArgs e)
{
switch (e.Text)
{
case "<":
OpenElementAutoComplete();
break;
case " ":
OpenAttributeAutoComplete();
break;
case ".":
OpenPropertyElementAutoComplete();
break;
case "/":
AddEndTag();
break;
case ">":
CloseCompletionWindow();
break;
case "!":
CloseCompletionWindow();
break;
}
}
private (string, int) GetLineAndPosAtCaretPosition()
{
// this assumes the file was saved as CSLF (\r\n newlines)
int offset = UIXML.CaretOffset - 1;
int lineStartIdx = UIXML.Text.LastIndexOf('\n', offset);
int lineEndIdx = UIXML.Text.IndexOf('\n', offset);
string line;
int pos;
if (lineStartIdx == -1 && lineEndIdx == -1)
{
line = UIXML.Text;
pos = offset;
}
else if (lineStartIdx == -1)
{
line = UIXML.Text[..(lineEndIdx - 1)];
pos = offset;
}
else if (lineEndIdx == -1)
{
line = UIXML.Text[(lineStartIdx + 1)..];
pos = offset - lineStartIdx - 2;
}
else
{
line = UIXML.Text[(lineStartIdx + 1)..(lineEndIdx - 1)];
pos = offset - lineStartIdx - 2;
}
return (line, pos);
}
/// <summary>
/// Source: https://xsemmel.codeplex.com
/// </summary>
/// <param name="xml"></param>
/// <param name="offset"></param>
/// <returns></returns>
public static string? GetElementAtCursor(string xml, int offset, bool onlyAllowInside = false)
{
if (offset == xml.Length)
{
offset--;
}
int startIdx = xml.LastIndexOf('<', offset);
if (startIdx < 0) return null;
if (startIdx < xml.Length && xml[startIdx + 1] == '/')
{
startIdx = startIdx + 1;
}
int endIdx1 = xml.IndexOf(' ', startIdx);
if (endIdx1 == -1 /*|| endIdx1 > offset*/) endIdx1 = int.MaxValue;
int endIdx2 = xml.IndexOf('>', startIdx);
if (endIdx2 == -1 /*|| endIdx2 > offset*/)
{
endIdx2 = int.MaxValue;
}
else
{
if (onlyAllowInside && endIdx2 < offset)
return null; // we dont want attribute auto complete to show outside of elements
if (endIdx2 < xml.Length && xml[endIdx2 - 1] == '/')
{
endIdx2 = endIdx2 - 1;
}
}
int endIdx = Math.Min(endIdx1, endIdx2);
if (endIdx2 > 0 && endIdx2 < int.MaxValue && endIdx > startIdx)
{
string element = xml.Substring(startIdx + 1, endIdx - startIdx - 1);
return element == "!--" ? null : element; // dont treat comments as elements
}
else
{
return null;
}
}
/// <summary>
/// A space between the cursor and the element will completely cancel this function
/// </summary>
private string? GetElementAtCursorNoSpaces(string xml, int offset)
{
(string line, int pos) = GetLineAndPosAtCaretPosition();
string curr = "";
while (pos != -1)
{
char c = line[pos];
if (c == ' ' || c == '\t')
return null;
if (c == '<')
return curr;
curr = c + curr;
pos--;
}
return null;
}
/// <summary>
/// Returns null if not eligible to auto complete there.
/// Returns the name of the element to show the attributes for
/// </summary>
/// <returns></returns>
private string? ShowAttributesForElementName()
{
(string line, int pos) = GetLineAndPosAtCaretPosition();
// check if theres an even number of speech marks on the line
int numSpeech = line.Count(x => x == '"');
if (numSpeech % 2 == 0)
{
// we have an equal number, let's check if pos is in between the speech marks
int count = -1;
int idx = pos;
int size = line.Length - 1;
while (idx != -1)
{
count++;
if (size > idx + 1)
idx = line.IndexOf('"', idx + 1);
else
idx = -1;
}
if (count % 2 != 0)
{
// odd number of speech marks means we're inside a string right now
// we dont want to display attribute auto complete while we're inside a string
return null;
}
}
return GetElementAtCursor(UIXML.Text, UIXML.CaretOffset, true);
}
private void AddEndTag()
{
CloseCompletionWindow();
if (UIXML.Text.Length > 2 && UIXML.Text[UIXML.CaretOffset - 2] == '<')
{
var elementName = GetElementAtCursor(UIXML.Text, UIXML.CaretOffset - 3);
if (elementName == null)
return;
UIXML.TextArea.Document.Insert(UIXML.CaretOffset, $"{elementName}>");
}
else
{
if (UIXML.Text.Length > UIXML.CaretOffset && UIXML.Text[UIXML.CaretOffset] == '>')
return;
var elementName = ShowAttributesForElementName(); // re-using functions :)
if (elementName != null)
UIXML.TextArea.Document.Insert(UIXML.CaretOffset, ">");
}
}
private void OpenElementAutoComplete()
{
var data = new List<ICompletionData>();
foreach (var element in CustomBootstrapperSchema.ElementInfo.Keys)
data.Add(new ElementCompletionData(element));
ShowCompletionWindow(data);
}
private void OpenAttributeAutoComplete()
{
string? element = ShowAttributesForElementName();
if (element == null)
{
CloseCompletionWindow();
return;
}
if (!CustomBootstrapperSchema.ElementInfo.ContainsKey(element))
{
CloseCompletionWindow();
return;
}
var attributes = CustomBootstrapperSchema.ElementInfo[element];
var data = new List<ICompletionData>();
foreach (var attribute in attributes)
data.Add(new AttributeCompletionData(attribute.Key, () => OpenTypeValueAutoComplete(attribute.Value)));
ShowCompletionWindow(data);
}
private void OpenTypeValueAutoComplete(string typeName)
{
var typeValues = CustomBootstrapperSchema.Types[typeName].Values;
if (typeValues == null)
return;
var data = new List<ICompletionData>();
foreach (var value in typeValues)
data.Add(new TypeValueCompletionData(value));
ShowCompletionWindow(data);
}
private void OpenPropertyElementAutoComplete()
{
string? element = GetElementAtCursorNoSpaces(UIXML.Text, UIXML.CaretOffset);
if (element == null)
{
CloseCompletionWindow();
return;
}
if (!CustomBootstrapperSchema.PropertyElements.ContainsKey(element))
{
CloseCompletionWindow();
return;
}
var properties = CustomBootstrapperSchema.PropertyElements[element];
var data = new List<ICompletionData>();
foreach (var property in properties)
data.Add(new TypeValueCompletionData(property));
ShowCompletionWindow(data);
}
private void CloseCompletionWindow()
{
if (_completionWindow != null)
{
_completionWindow.Close();
_completionWindow = null;
}
}
private void ShowCompletionWindow(List<ICompletionData> completionData)
{
CloseCompletionWindow();
if (!completionData.Any())
return;
_completionWindow = new CompletionWindow(UIXML.TextArea);
IList<ICompletionData> data = _completionWindow.CompletionList.CompletionData;
foreach (var c in completionData)
data.Add(c);
_completionWindow.Show();
_completionWindow.Closed += (_, _) => _completionWindow = null;
}
}
public class ElementCompletionData : ICompletionData
{
public ElementCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
}
public class AttributeCompletionData : ICompletionData
{
private Action _openValueAutoCompleteAction;
public AttributeCompletionData(string text, Action openValueAutoCompleteAction)
{
_openValueAutoCompleteAction = openValueAutoCompleteAction;
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text + "=\"\"");
textArea.Caret.Offset = textArea.Caret.Offset - 1;
_openValueAutoCompleteAction();
}
}
public class TypeValueCompletionData : ICompletionData
{
public TypeValueCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
}
}

View File

@ -8,7 +8,7 @@
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="640" d:DesignWidth="800"
d:DesignHeight="900" d:DesignWidth="800"
Title="AppearancePage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
@ -43,17 +43,83 @@
<ui:Button Grid.Column="1" Content="{x:Static resources:Strings.Menu_Appearance_Preview}" HorizontalAlignment="Stretch" Margin="0,16,0,0" Command="{Binding PreviewBootstrapperCommand}" />
</Grid>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Appearance_Style_Title}"
Description="{x:Static resources:Strings.Menu_Appearance_Style_Description}">
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding Dialogs, Mode=OneTime}" Text="{Binding Dialog, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:OptionControl>
<ui:CardExpander Margin="0,8,0,0" IsExpanded="{Binding Path=CustomThemesExpanded, Mode=OneWay}" Style="{StaticResource NoUserExpansionCardExpanderStyle}">
<ui:CardExpander.Header>
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Appearance_Style_Title}" />
<TextBlock FontSize="12" Text="{x:Static resources:Strings.Menu_Appearance_Style_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding Dialogs, Mode=OneTime}" Text="{Binding Dialog, Mode=TwoWay}" Grid.Column="1">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Grid>
</ui:CardExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="CustomThemesListBox" Height="265" Grid.Row="0" Grid.Column="0" Margin="0,0,4,0" ItemsSource="{Binding CustomThemes, Mode=OneWay}" SelectionChanged="CustomThemeSelection" SelectedIndex="{Binding SelectedCustomThemeIndex, Mode=TwoWay}" />
<Grid Grid.Row="1" Grid.Column="0" Margin="0,8,4,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0" Margin="0,0,4,0" Icon="Add28" Content="{x:Static resources:Strings.Common_New}" HorizontalAlignment="Stretch" Command="{Binding AddCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="1" Margin="4,0,0,0" Icon="Delete28" Content="{x:Static resources:Strings.Common_Delete}" HorizontalAlignment="Stretch" Appearance="Danger" IsEnabled="{Binding IsCustomThemeSelected, Mode=OneWay}" Command="{Binding DeleteCustomThemeCommand, Mode=OneTime}" />
</Grid>
<StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Margin="4,0,0,0">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCustomThemeSelected}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomThemeName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0" Margin="0,0,4,0" Icon="Edit28" Content="{x:Static resources:Strings.Common_Rename}" HorizontalAlignment="Stretch" Command="{Binding RenameCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="1" Margin="4,0,4,0" Icon="DesktopEdit24" Content="{x:Static resources:Strings.Common_Edit}" HorizontalAlignment="Stretch" Command="{Binding EditCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="2" Margin="4,0,0,0" Icon="ArrowExportRtl24" Content="{x:Static resources:Strings.Common_Export}" HorizontalAlignment="Stretch" Command="{Binding ExportCustomThemeCommand, Mode=OneTime}" />
</Grid>
</StackPanel>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="{x:Static resources:Strings.Menu_Appearance_CustomThemes_NoneSelected}" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCustomThemeSelected}" Value="True">
<Setter Property="TextBlock.Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</ui:CardExpander>
<controls:OptionControl
x:Name="IconSelector"

View File

@ -1,5 +1,7 @@
using Bloxstrap.UI.ViewModels.Settings;
using System.Windows.Controls;
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
@ -12,5 +14,16 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
DataContext = new AppearanceViewModel(this);
InitializeComponent();
}
public void CustomThemeSelection(object sender, SelectionChangedEventArgs e)
{
AppearanceViewModel viewModel = (AppearanceViewModel)DataContext;
viewModel.SelectedCustomTheme = (string)((ListBox)sender).SelectedItem;
viewModel.SelectedCustomThemeName = viewModel.SelectedCustomTheme;
viewModel.OnPropertyChanged(nameof(viewModel.SelectedCustomTheme));
viewModel.OnPropertyChanged(nameof(viewModel.SelectedCustomThemeName));
}
}
}

View File

@ -30,13 +30,19 @@
<ui:ToggleSwitch IsChecked="{Binding ForceRobloxLanguage, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Behaviour_BackgroundUpdates_Title}"
Description="{x:Static resources:Strings.Menu_Behaviour_BackgroundUpdates_Description}">
<ui:ToggleSwitch IsChecked="{Binding BackgroundUpdates, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Title}"
Description="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Description}">
<controls:OptionControl.Style>
<Style TargetType="controls:OptionControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ForceRobloxReinstallation, Mode=OneTime}" Value="True">
<DataTrigger Binding="{Binding IsRobloxInstallationMissing, Mode=OneTime}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>

View File

@ -58,6 +58,32 @@ namespace Bloxstrap.UI
});
}
private static IBootstrapperDialog GetCustomBootstrapper()
{
const string LOG_IDENT = "Frontend::GetCustomBootstrapper";
Directory.CreateDirectory(Paths.CustomThemes);
try
{
if (App.Settings.Prop.SelectedCustomTheme == null)
throw new CustomThemeException("CustomTheme.Errors.NoThemeSelected");
CustomDialog dialog = new CustomDialog();
dialog.ApplyCustomTheme(App.Settings.Prop.SelectedCustomTheme);
return dialog;
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
if (!App.LaunchSettings.QuietFlag.Active)
ShowMessageBox(string.Format(Strings.CustomTheme_Errors_SetupFailed, ex.Message, "Bloxstrap"), MessageBoxImage.Error); // NOTE: Bloxstrap is the theme name
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
}
}
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
{
return style switch
@ -70,6 +96,7 @@ namespace Bloxstrap.UI
BootstrapperStyle.ByfronDialog => new ByfronDialog(),
BootstrapperStyle.FluentDialog => new FluentDialog(false),
BootstrapperStyle.FluentAeroDialog => new FluentDialog(true),
BootstrapperStyle.CustomDialog => GetCustomBootstrapper(),
_ => new FluentDialog(false)
};
}
@ -83,5 +110,17 @@ namespace Bloxstrap.UI
return messagebox.Result;
}));
}
public static void ShowBalloonTip(string title, string message, System.Windows.Forms.ToolTipIcon icon = System.Windows.Forms.ToolTipIcon.None, int timeout = 5)
{
var notifyIcon = new System.Windows.Forms.NotifyIcon
{
Icon = Properties.Resources.IconBloxstrap,
Text = App.ProjectName,
Visible = true
};
notifyIcon.ShowBalloonTip(timeout, title, message, icon);
}
}
}

View File

@ -0,0 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="NewTextEditorBackground" Color="#2D2D2D" />
<SolidColorBrush x:Key="NewTextEditorForeground" Color="White" />
<SolidColorBrush x:Key="NewTextEditorLink" Color="#3A9CEA" />
</ResourceDictionary>

View File

@ -0,0 +1,189 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
<!-- Taken from DefaultUiCardExpanderStyle -->
<Style x:Key="NoUserExpansionCardExpanderStyle" TargetType="{x:Type ui:CardExpander}">
<!-- Universal WPF UI focus -->
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<!-- Universal WPF UI focus -->
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource ControlFillColorDefault}" />
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource TextFillColorPrimary}" />
</Setter.Value>
</Setter>
<Setter Property="IconForeground">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource TextFillColorPrimary}" />
</Setter.Value>
</Setter>
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource CardExpanderBorderThemeThickness}" />
<Setter Property="Padding" Value="{StaticResource CardExpanderPadding}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Border.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="IconFilled" Value="False" />
<Setter Property="Icon" Value="Empty" />
<Setter Property="IsExpanded" Value="False" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:CardExpander}">
<!-- Top level border should not have padding or margin -->
<Border
x:Name="ContentBorder"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
Padding="0"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Top level controls always visible -->
<Grid
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Column="1"
Content="{TemplateBinding Header}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Grid>
<!-- Collapsed content to expand -->
<Border
x:Name="ContentPresenterBorder"
Grid.Row="1"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
Opacity="0.0">
<ContentPresenter
x:Name="ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}" />
<Border.LayoutTransform>
<ScaleTransform ScaleY="0" />
</Border.LayoutTransform>
</Border>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="ContentPresenterBorder" Property="BorderThickness" Value="0,1,0,0" />
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="0.0"
To="1.0"
Duration="00:00:00.167" />
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="0.0"
To="1.0"
Duration="00:00:00.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="1.0"
To="0"
Duration="00:00:00.167" />
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="1.0"
To="0.0"
Duration="00:00:00.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<!--<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />-->
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ControlFillColorDisabledBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
<Setter TargetName="HeaderContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Taken from https://github.com/icsharpcode/AvalonEdit/blob/30cad99ce905412ed5f5e847e3c00e72e69aee77/ICSharpCode.AvalonEdit/TextEditor.xaml -->
<Style x:Key="NewTextEditor" TargetType="{x:Type avalonedit:TextEditor}">
<Setter Property="Foreground" Value="{DynamicResource NewTextEditorForeground}" />
<Setter Property="Background" Value="{DynamicResource NewTextEditorBackground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type avalonedit:TextEditor}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ScrollViewer
Name="PART_ScrollViewer"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Top"
CanContentScroll="True"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TextArea}"
Focusable="False"
HorizontalScrollBarVisibility="{TemplateBinding HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding VerticalScrollBarVisibility}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="WordWrap" Value="True">
<Setter TargetName="PART_ScrollViewer" Property="HorizontalScrollBarVisibility" Value="Disabled" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,63 @@
<SyntaxDefinition name="XML" extensions=".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd;.wxs;.wxi;.wxl;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets;.xaml;.xpt;.xft;.map;.wsdl;.disco;.ps1xml;.nuspec" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color foreground="#529955" name="Comment" exampleText="&lt;!-- comment --&gt;" />
<Color foreground="White" name="CData" exampleText="&lt;![CDATA[data]]&gt;" />
<Color foreground="White" name="DocType" exampleText="&lt;!DOCTYPE rootElement&gt;" />
<Color foreground="White" name="XmlDeclaration" exampleText='&lt;?xml version="1.0"?&gt;' />
<Color foreground="#569CD6" name="XmlTag" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="#9CDCFE" name="AttributeName" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="#CE9178" name="AttributeValue" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="White" name="Entity" exampleText="index.aspx?a=1&amp;amp;b=2" />
<Color foreground="White" name="BrokenEntity" exampleText="index.aspx?a=1&amp;b=2" />
<RuleSet>
<Span color="Comment" multiline="true">
<Begin>&lt;!--</Begin>
<End>--&gt;</End>
</Span>
<Span color="CData" multiline="true">
<Begin>&lt;!\[CDATA\[</Begin>
<End>]]&gt;</End>
</Span>
<Span color="DocType" multiline="true">
<Begin>&lt;!DOCTYPE</Begin>
<End>&gt;</End>
</Span>
<Span color="XmlDeclaration" multiline="true">
<Begin>&lt;\?</Begin>
<End>\?&gt;</End>
</Span>
<Span color="XmlTag" multiline="true">
<Begin>&lt;</Begin>
<End>&gt;</End>
<RuleSet>
<!-- Treat the position before '<' as end, as that's not a valid character
in attribute names and indicates the user forgot a closing quote. -->
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>"</Begin>
<End>"|(?=&lt;)</End>
</Span>
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>'</Begin>
<End>'|(?=&lt;)</End>
</Span>
<Rule color="AttributeName">[\d\w_\-\.]+(?=(\s*=))</Rule>
<Rule color="AttributeValue">=</Rule>
</RuleSet>
</Span>
<Import ruleSet="EntitySet"/>
</RuleSet>
<RuleSet name="EntitySet">
<Rule color="Entity">
&amp;
[\w\d\#]+
;
</Rule>
<Rule color="BrokenEntity">
&amp;
[\w\d\#]*
#missing ;
</Rule>
</RuleSet>
</SyntaxDefinition>

View File

@ -0,0 +1,63 @@
<SyntaxDefinition name="XML" extensions=".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd;.wxs;.wxi;.wxl;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets;.xaml;.xpt;.xft;.map;.wsdl;.disco;.ps1xml;.nuspec" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color foreground="Green" name="Comment" exampleText="&lt;!-- comment --&gt;" />
<Color foreground="Blue" name="CData" exampleText="&lt;![CDATA[data]]&gt;" />
<Color foreground="Blue" name="DocType" exampleText="&lt;!DOCTYPE rootElement&gt;" />
<Color foreground="Blue" name="XmlDeclaration" exampleText='&lt;?xml version="1.0"?&gt;' />
<Color foreground="DarkMagenta" name="XmlTag" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="Red" name="AttributeName" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="Blue" name="AttributeValue" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="Teal" name="Entity" exampleText="index.aspx?a=1&amp;amp;b=2" />
<Color foreground="Olive" name="BrokenEntity" exampleText="index.aspx?a=1&amp;b=2" />
<RuleSet>
<Span color="Comment" multiline="true">
<Begin>&lt;!--</Begin>
<End>--&gt;</End>
</Span>
<Span color="CData" multiline="true">
<Begin>&lt;!\[CDATA\[</Begin>
<End>]]&gt;</End>
</Span>
<Span color="DocType" multiline="true">
<Begin>&lt;!DOCTYPE</Begin>
<End>&gt;</End>
</Span>
<Span color="XmlDeclaration" multiline="true">
<Begin>&lt;\?</Begin>
<End>\?&gt;</End>
</Span>
<Span color="XmlTag" multiline="true">
<Begin>&lt;</Begin>
<End>&gt;</End>
<RuleSet>
<!-- Treat the position before '<' as end, as that's not a valid character
in attribute names and indicates the user forgot a closing quote. -->
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>"</Begin>
<End>"|(?=&lt;)</End>
</Span>
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>'</Begin>
<End>'|(?=&lt;)</End>
</Span>
<Rule color="AttributeName">[\d\w_\-\.]+(?=(\s*=))</Rule>
<Rule color="AttributeValue">=</Rule>
</RuleSet>
</Span>
<Import ruleSet="EntitySet"/>
</RuleSet>
<RuleSet name="EntitySet">
<Rule color="Entity">
&amp;
[\w\d\#]+
;
</Rule>
<Rule color="BrokenEntity">
&amp;
[\w\d\#]*
#missing ;
</Rule>
</RuleSet>
</SyntaxDefinition>

View File

@ -0,0 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="NewTextEditorBackground" Color="White" />
<SolidColorBrush x:Key="NewTextEditorForeground" Color="Black" />
<SolidColorBrush x:Key="NewTextEditorLink" Color="Blue" />
</ResourceDictionary>

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Bloxstrap.UI.ViewModels.Dialogs
{
internal class AddCustomThemeViewModel : NotifyPropertyChangedViewModel
{
public static CustomThemeTemplate[] Templates => Enum.GetValues<CustomThemeTemplate>();
public CustomThemeTemplate Template { get; set; } = CustomThemeTemplate.Simple;
public string Name { get; set; } = "";
private string _filePath = "";
public string FilePath
{
get => _filePath;
set
{
if (_filePath != value)
{
_filePath = value;
OnPropertyChanged(nameof(FilePath));
OnPropertyChanged(nameof(FilePathVisibility));
}
}
}
public Visibility FilePathVisibility => string.IsNullOrEmpty(FilePath) ? Visibility.Collapsed : Visibility.Visible;
public int SelectedTab { get; set; } = 0;
private string _nameError = "";
public string NameError
{
get => _nameError;
set
{
if (_nameError != value)
{
_nameError = value;
OnPropertyChanged(nameof(NameError));
OnPropertyChanged(nameof(NameErrorVisibility));
}
}
}
public Visibility NameErrorVisibility => string.IsNullOrEmpty(NameError) ? Visibility.Collapsed : Visibility.Visible;
private string _fileError = "";
public string FileError
{
get => _fileError;
set
{
if (_fileError != value)
{
_fileError = value;
OnPropertyChanged(nameof(FileError));
OnPropertyChanged(nameof(FileErrorVisibility));
}
}
}
public Visibility FileErrorVisibility => string.IsNullOrEmpty(FileError) ? Visibility.Collapsed : Visibility.Visible;
}
}

View File

@ -0,0 +1,84 @@
using Bloxstrap.UI.Elements.Bootstrapper;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace Bloxstrap.UI.ViewModels.Editor
{
public class BootstrapperEditorWindowViewModel : NotifyPropertyChangedViewModel
{
private CustomDialog? _dialog = null;
public ICommand PreviewCommand => new RelayCommand(Preview);
public ICommand SaveCommand => new RelayCommand(Save);
public ICommand OpenThemeFolderCommand => new RelayCommand(OpenThemeFolder);
public Action<bool, string> ThemeSavedCallback { get; set; } = null!;
public string Directory { get; set; } = "";
public string Name { get; set; } = "";
public string Title { get; set; } = "Editing \"Custom Theme\"";
public string Code { get; set; } = "";
public bool CodeChanged { get; set; } = false;
private void Preview()
{
const string LOG_IDENT = "BootstrapperEditorWindowViewModel::Preview";
try
{
CustomDialog dialog = new CustomDialog();
dialog.ApplyCustomTheme(Name, Code);
_dialog?.CloseBootstrapper();
_dialog = dialog;
dialog.Message = Strings.Bootstrapper_StylePreview_TextCancel;
dialog.CancelEnabled = true;
dialog.ShowBootstrapper();
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to preview custom theme");
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox($"Failed to preview theme: {ex.Message}", MessageBoxImage.Error, MessageBoxButton.OK);
}
}
private void Save()
{
const string LOG_IDENT = "BootstrapperEditorWindowViewModel::Save";
string path = Path.Combine(Directory, "Theme.xml");
try
{
File.WriteAllText(path, Code);
CodeChanged = false;
ThemeSavedCallback.Invoke(true, "Your theme has been saved!");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to save custom theme");
App.Logger.WriteException(LOG_IDENT, ex);
//Frontend.ShowMessageBox($"Failed to save theme: {ex.Message}", MessageBoxImage.Error, MessageBoxButton.OK);
ThemeSavedCallback.Invoke(false, ex.Message);
}
}
private void OpenThemeFolder()
{
Process.Start("explorer.exe", Directory);
}
}
}

View File

@ -4,10 +4,13 @@ using System.Windows.Controls;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Win32;
using Bloxstrap.UI.Elements.Settings;
using Bloxstrap.UI.Elements.Editor;
using Bloxstrap.UI.Elements.Dialogs;
namespace Bloxstrap.UI.ViewModels.Settings
{
@ -18,6 +21,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper);
public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation);
public ICommand AddCustomThemeCommand => new RelayCommand(AddCustomTheme);
public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme);
public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme);
public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme);
public ICommand ExportCustomThemeCommand => new RelayCommand(ExportCustomTheme);
private void PreviewBootstrapper()
{
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
@ -51,6 +60,8 @@ namespace Bloxstrap.UI.ViewModels.Settings
foreach (var entry in BootstrapperIconEx.Selections)
Icons.Add(new BootstrapperIconEntry { IconType = entry });
PopulateCustomThemes();
}
public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>();
@ -78,9 +89,15 @@ namespace Bloxstrap.UI.ViewModels.Settings
public BootstrapperStyle Dialog
{
get => App.Settings.Prop.BootstrapperStyle;
set => App.Settings.Prop.BootstrapperStyle = value;
set
{
App.Settings.Prop.BootstrapperStyle = value;
OnPropertyChanged(nameof(CustomThemesExpanded)); // TODO: only fire when needed
}
}
public bool CustomThemesExpanded => App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.CustomDialog;
public ObservableCollection<BootstrapperIconEntry> Icons { get; set; } = new();
public BootstrapperIcon Icon
@ -116,5 +133,183 @@ namespace Bloxstrap.UI.ViewModels.Settings
OnPropertyChanged(nameof(Icons));
}
}
private void DeleteCustomThemeStructure(string name)
{
string dir = Path.Combine(Paths.CustomThemes, name);
Directory.Delete(dir, true);
}
private void RenameCustomThemeStructure(string oldName, string newName)
{
string oldDir = Path.Combine(Paths.CustomThemes, oldName);
string newDir = Path.Combine(Paths.CustomThemes, newName);
Directory.Move(oldDir, newDir);
}
private void AddCustomTheme()
{
var dialog = new AddCustomThemeDialog();
dialog.ShowDialog();
if (dialog.Created)
{
CustomThemes.Add(dialog.ThemeName);
SelectedCustomThemeIndex = CustomThemes.Count - 1;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
OnPropertyChanged(nameof(IsCustomThemeSelected));
if (dialog.OpenEditor)
EditCustomTheme();
}
}
private void DeleteCustomTheme()
{
if (SelectedCustomTheme is null)
return;
try
{
DeleteCustomThemeStructure(SelectedCustomTheme);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex);
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_DeleteFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
return;
}
CustomThemes.Remove(SelectedCustomTheme);
if (CustomThemes.Any())
{
SelectedCustomThemeIndex = CustomThemes.Count - 1;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
OnPropertyChanged(nameof(IsCustomThemeSelected));
}
private void RenameCustomTheme()
{
if (SelectedCustomTheme is null)
return;
if (SelectedCustomTheme == SelectedCustomThemeName)
return;
try
{
RenameCustomThemeStructure(SelectedCustomTheme, SelectedCustomThemeName);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_RenameFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
return;
}
int idx = CustomThemes.IndexOf(SelectedCustomTheme);
CustomThemes[idx] = SelectedCustomThemeName;
SelectedCustomThemeIndex = idx;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
private void EditCustomTheme()
{
if (SelectedCustomTheme is null)
return;
new BootstrapperEditorWindow(SelectedCustomTheme).ShowDialog();
}
private void ExportCustomTheme()
{
if (SelectedCustomTheme is null)
return;
var dialog = new SaveFileDialog
{
FileName = $"{SelectedCustomTheme}.zip",
Filter = $"{Strings.FileTypes_ZipArchive}|*.zip"
};
if (dialog.ShowDialog() != true)
return;
string themeDir = Path.Combine(Paths.CustomThemes, SelectedCustomTheme);
using var memStream = new MemoryStream();
using var zipStream = new ZipOutputStream(memStream);
foreach (var filePath in Directory.EnumerateFiles(themeDir, "*.*", SearchOption.AllDirectories))
{
string relativePath = filePath[(themeDir.Length + 1)..];
var entry = new ZipEntry(relativePath);
entry.DateTime = DateTime.Now;
zipStream.PutNextEntry(entry);
using var fileStream = File.OpenRead(filePath);
fileStream.CopyTo(zipStream);
}
zipStream.CloseEntry();
zipStream.Finish();
memStream.Position = 0;
using var outputStream = File.OpenWrite(dialog.FileName);
memStream.CopyTo(outputStream);
Process.Start("explorer.exe", $"/select,\"{dialog.FileName}\"");
}
private void PopulateCustomThemes()
{
string? selected = App.Settings.Prop.SelectedCustomTheme;
Directory.CreateDirectory(Paths.CustomThemes);
foreach (string directory in Directory.GetDirectories(Paths.CustomThemes))
{
if (!File.Exists(Path.Combine(directory, "Theme.xml")))
continue; // missing the main theme file, ignore
string name = Path.GetFileName(directory);
CustomThemes.Add(name);
}
if (selected != null)
{
int idx = CustomThemes.IndexOf(selected);
if (idx != -1)
{
SelectedCustomThemeIndex = idx;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
else
{
SelectedCustomTheme = null;
}
}
}
public string? SelectedCustomTheme
{
get => App.Settings.Prop.SelectedCustomTheme;
set => App.Settings.Prop.SelectedCustomTheme = value;
}
public string SelectedCustomThemeName { get; set; } = "";
public int SelectedCustomThemeIndex { get; set; }
public ObservableCollection<string> CustomThemes { get; set; } = new();
public bool IsCustomThemeSelected => SelectedCustomTheme is not null;
}
}

View File

@ -2,9 +2,6 @@
{
public class BehaviourViewModel : NotifyPropertyChangedViewModel
{
private string _oldPlayerVersionGuid = "";
private string _oldStudioVersionGuid = "";
public bool ConfirmLaunches
{
get => App.Settings.Prop.ConfirmLaunches;
@ -17,26 +14,18 @@
set => App.Settings.Prop.ForceRobloxLanguage = value;
}
public bool BackgroundUpdates
{
get => App.Settings.Prop.BackgroundUpdatesEnabled;
set => App.Settings.Prop.BackgroundUpdatesEnabled = value;
}
public bool IsRobloxInstallationMissing => String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid);
public bool ForceRobloxReinstallation
{
// wouldnt it be better to check old version guids?
// what about fresh installs?
get => String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
set
{
if (value)
{
_oldPlayerVersionGuid = App.State.Prop.Player.VersionGuid;
_oldStudioVersionGuid = App.State.Prop.Studio.VersionGuid;
App.State.Prop.Player.VersionGuid = "";
App.State.Prop.Studio.VersionGuid = "";
}
else
{
App.State.Prop.Player.VersionGuid = _oldPlayerVersionGuid;
App.State.Prop.Studio.VersionGuid = _oldStudioVersionGuid;
}
}
get => App.State.Prop.ForceReinstall || IsRobloxInstallationMissing;
set => App.State.Prop.ForceReinstall = value;
}
}
}

View File

@ -75,10 +75,24 @@ namespace Bloxstrap
}
}
public static string GetRobloxVersion(bool studio)
/// <summary>
/// Parses the input version string and prints if fails
/// </summary>
public static Version? ParseVersionSafe(string versionStr)
{
IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData();
const string LOG_IDENT = "Utilities::ParseVersionSafe";
if (!Version.TryParse(versionStr, out Version? version))
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to convert {versionStr} to a valid Version type.");
return version;
}
return version;
}
public static string GetRobloxVersionStr(IAppData data)
{
string playerLocation = data.ExecutablePath;
if (!File.Exists(playerLocation))
@ -92,6 +106,19 @@ namespace Bloxstrap
return versionInfo.ProductVersion.Replace(", ", ".");
}
public static string GetRobloxVersionStr(bool studio)
{
IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData();
return GetRobloxVersionStr(data);
}
public static Version? GetRobloxVersion(IAppData data)
{
string str = GetRobloxVersionStr(data);
return ParseVersionSafe(str);
}
public static Process[] GetProcessesSafe()
{
const string LOG_IDENT = "Utilities::GetProcessesSafe";
@ -107,5 +134,24 @@ namespace Bloxstrap
return Array.Empty<Process>(); // can we retry?
}
}
public static bool DoesMutexExist(string name)
{
try
{
Mutex.OpenExisting(name).Close();
return true;
}
catch
{
return false;
}
}
public static void KillBackgroundUpdater()
{
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent");
handle.Set();
}
}
}

View File

@ -31,5 +31,15 @@ namespace Bloxstrap.Utility
fileInfo.IsReadOnly = false;
App.Logger.WriteLine("Filesystem::AssertReadOnly", $"The following file was set as read-only: {filePath}");
}
internal static void AssertReadOnlyDirectory(string directoryPath)
{
var directory = new DirectoryInfo(directoryPath) { Attributes = FileAttributes.Normal };
foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories))
info.Attributes = FileAttributes.Normal;
App.Logger.WriteLine("Filesystem::AssertReadOnlyDirectory", $"The following directory was set as read-only: {directoryPath}");
}
}
}

View File

@ -25,6 +25,11 @@ namespace Bloxstrap.Utility
return FromStream(stream);
}
public static string FromString(string str)
{
return FromBytes(Encoding.UTF8.GetBytes(str));
}
public static string Stringify(byte[] hash)
{
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Utility
{
internal static class PathValidator
{
public enum ValidationResult
{
Ok,
IllegalCharacter,
ReservedFileName,
ReservedDirectoryName
}
private static readonly string[] _reservedNames = new string[]
{
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9"
};
private static readonly char[] _directorySeperatorDelimiters = new char[]
{
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar
};
private static readonly char[] _invalidPathChars = GetInvalidPathChars();
public static char[] GetInvalidPathChars()
{
char[] invalids = new char[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|' };
char[] otherInvalids = Path.GetInvalidPathChars();
char[] result = new char[invalids.Length + otherInvalids.Length];
invalids.CopyTo(result, 0);
otherInvalids.CopyTo(result, invalids.Length);
return result;
}
public static ValidationResult IsFileNameValid(string fileName)
{
if (fileName.IndexOfAny(_invalidPathChars) != -1)
return ValidationResult.IllegalCharacter;
string fileNameNoExt = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant();
if (_reservedNames.Contains(fileNameNoExt))
return ValidationResult.ReservedFileName;
return ValidationResult.Ok;
}
public static ValidationResult IsPathValid(string path)
{
string? pathRoot = Path.GetPathRoot(path);
string pathNoRoot = pathRoot != null ? path[pathRoot.Length..] : path;
string[] pathParts = pathNoRoot.Split(_directorySeperatorDelimiters);
foreach (var part in pathParts)
{
if (part.IndexOfAny(_invalidPathChars) != -1)
return ValidationResult.IllegalCharacter;
if (_reservedNames.Contains(part))
return ValidationResult.ReservedDirectoryName;
}
string fileName = Path.GetFileName(path);
if (fileName.IndexOfAny(_invalidPathChars) != -1)
return ValidationResult.IllegalCharacter;
string fileNameNoExt = Path.GetFileNameWithoutExtension(path).ToUpperInvariant();
if (_reservedNames.Contains(fileNameNoExt))
return ValidationResult.ReservedFileName;
return ValidationResult.Ok;
}
}
}

2
wpfui

@ -1 +1 @@
Subproject commit 9080158ba8d496501146d1167aae910898eff9af
Subproject commit dca423b724ec24bd3377da3a27f4055ae317b50a