mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-06-23 23:00:23 -07:00
Compare commits
20 Commits
ff8466f0f5
...
4a0aa7e265
Author | SHA1 | Date | |
---|---|---|---|
|
4a0aa7e265 | ||
|
0d04a36590 | ||
|
800f49b0c8 | ||
|
19c82c66d0 | ||
|
667fa5c1db | ||
|
f873b7785e | ||
|
3cfa9a0aad | ||
|
94ed521d31 | ||
|
a4a82e1057 | ||
|
71667c6c11 | ||
|
1b7c8b2b3a | ||
|
239a0e3e1d | ||
|
33243bfd0a | ||
|
2acd0162fb | ||
|
552f2a52a6 | ||
|
12af23b261 | ||
|
f0df6153e3 | ||
|
1258dc3589 | ||
|
7977dba498 | ||
|
4785464332 |
10
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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.
|
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:
|
validations:
|
||||||
required: true
|
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
|
- type: textarea
|
||||||
id: log
|
id: log
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -181,6 +181,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)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "App::OnStartup";
|
const string LOG_IDENT = "App::OnStartup";
|
||||||
@ -213,6 +229,8 @@ namespace Bloxstrap
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.WriteLine(LOG_IDENT, $"OSVersion: {Environment.OSVersion}");
|
||||||
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
|
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
|
||||||
Logger.WriteLine(LOG_IDENT, $"Temp path is {Paths.Temp}");
|
Logger.WriteLine(LOG_IDENT, $"Temp path is {Paths.Temp}");
|
||||||
Logger.WriteLine(LOG_IDENT, $"WindowsStartMenu path is {Paths.WindowsStartMenu}");
|
Logger.WriteLine(LOG_IDENT, $"WindowsStartMenu path is {Paths.WindowsStartMenu}");
|
||||||
@ -292,6 +310,7 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
Logger.Initialize(true);
|
Logger.Initialize(true);
|
||||||
Logger.WriteLine(LOG_IDENT, "Not installed, launching the installer");
|
Logger.WriteLine(LOG_IDENT, "Not installed, launching the installer");
|
||||||
|
AssertWindowsOSVersion(); // prevent new installs from unsupported operating systems
|
||||||
LaunchHandler.LaunchInstaller();
|
LaunchHandler.LaunchInstaller();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<UseWindowsForms>True</UseWindowsForms>
|
<UseWindowsForms>True</UseWindowsForms>
|
||||||
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
|
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
|
||||||
<Version>2.8.6</Version>
|
<Version>2.9.0</Version>
|
||||||
<FileVersion>2.8.6</FileVersion>
|
<FileVersion>2.9.0</FileVersion>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
@ -31,7 +31,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Resources\CustomBootstrapperSchema.json" />
|
<EmbeddedResource Include="Resources\CustomBootstrapperSchema.json" />
|
||||||
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate.xml" />
|
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate_Blank.xml" />
|
||||||
|
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate_Simple.xml" />
|
||||||
<EmbeddedResource Include="Resources\Icon2008.ico" />
|
<EmbeddedResource Include="Resources\Icon2008.ico" />
|
||||||
<EmbeddedResource Include="Resources\Icon2011.ico" />
|
<EmbeddedResource Include="Resources\Icon2011.ico" />
|
||||||
<EmbeddedResource Include="Resources\Icon2017.ico" />
|
<EmbeddedResource Include="Resources\Icon2017.ico" />
|
||||||
@ -54,10 +55,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
|
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Markdig" Version="0.37.0" />
|
<PackageReference Include="Markdig" Version="0.40.0" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
|
||||||
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
|
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -58,6 +58,7 @@ namespace Bloxstrap
|
|||||||
private double _taskbarProgressIncrement;
|
private double _taskbarProgressIncrement;
|
||||||
private double _taskbarProgressMaximum;
|
private double _taskbarProgressMaximum;
|
||||||
private long _totalDownloadedBytes = 0;
|
private long _totalDownloadedBytes = 0;
|
||||||
|
private bool _packageExtractionSuccess = true;
|
||||||
|
|
||||||
private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
|
private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
|
||||||
private bool _noConnection = false;
|
private bool _noConnection = false;
|
||||||
@ -78,7 +79,15 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
// https://github.com/icsharpcode/SharpZipLib/blob/master/src/ICSharpCode.SharpZipLib/Zip/FastZip.cs/#L669-L680
|
// 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. ¯\_(ツ)_/¯
|
// 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.DirectoryFailure += (_, e) => throw e.Exception;
|
||||||
_fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested;
|
_fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested;
|
||||||
|
|
||||||
@ -179,6 +188,8 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
App.AssertWindowsOSVersion();
|
||||||
|
|
||||||
// ensure only one instance of the bootstrapper is running at the time
|
// ensure only one instance of the bootstrapper is running at the time
|
||||||
// so that we don't have stuff like two updates happening simultaneously
|
// so that we don't have stuff like two updates happening simultaneously
|
||||||
|
|
||||||
@ -221,6 +232,8 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool allModificationsApplied = true;
|
||||||
|
|
||||||
if (!_noConnection)
|
if (!_noConnection)
|
||||||
{
|
{
|
||||||
if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)
|
if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)
|
||||||
@ -231,7 +244,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
// we require deployment details for applying modifications for a worst case scenario,
|
// 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
|
// 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
|
// check registry entries for every launch, just in case the stock bootstrapper changes it back
|
||||||
@ -245,7 +258,15 @@ namespace Bloxstrap
|
|||||||
await mutex.ReleaseAsync();
|
await mutex.ReleaseAsync();
|
||||||
|
|
||||||
if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested)
|
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();
|
StartRoblox();
|
||||||
|
}
|
||||||
|
|
||||||
await mutex.ReleaseAsync();
|
await mutex.ReleaseAsync();
|
||||||
|
|
||||||
@ -303,14 +324,6 @@ namespace Bloxstrap
|
|||||||
clientVersion = await Deployment.GetInfo();
|
clientVersion = await Deployment.GetInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
|
||||||
|
|
||||||
_latestVersionGuid = clientVersion.VersionGuid;
|
_latestVersionGuid = clientVersion.VersionGuid;
|
||||||
@ -649,7 +662,28 @@ namespace Bloxstrap
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Roblox Install
|
#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";
|
const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder";
|
||||||
|
|
||||||
@ -659,6 +693,13 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid)
|
if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid)
|
||||||
{
|
{
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(dir, true);
|
Directory.Delete(dir, true);
|
||||||
@ -927,10 +968,12 @@ namespace Bloxstrap
|
|||||||
_isInstalling = false;
|
_isInstalling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyModifications()
|
private async Task<bool> ApplyModifications()
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
|
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
SetStatus(Strings.Bootstrapper_Status_ApplyingModifications);
|
SetStatus(Strings.Bootstrapper_Status_ApplyingModifications);
|
||||||
|
|
||||||
// handle file mods
|
// handle file mods
|
||||||
@ -1006,7 +1049,7 @@ namespace Bloxstrap
|
|||||||
foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories))
|
foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories))
|
||||||
{
|
{
|
||||||
if (_cancelTokenSource.IsCancellationRequested)
|
if (_cancelTokenSource.IsCancellationRequested)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
// get relative directory path
|
// get relative directory path
|
||||||
string relativeFile = file.Substring(Paths.Modifications.Length + 1);
|
string relativeFile = file.Substring(Paths.Modifications.Length + 1);
|
||||||
@ -1038,10 +1081,18 @@ namespace Bloxstrap
|
|||||||
Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
|
||||||
|
|
||||||
Filesystem.AssertReadOnly(fileVersionFolder);
|
Filesystem.AssertReadOnly(fileVersionFolder);
|
||||||
File.Copy(fileModFolder, fileVersionFolder, true);
|
try
|
||||||
Filesystem.AssertReadOnly(fileVersionFolder);
|
{
|
||||||
|
File.Copy(fileModFolder, fileVersionFolder, true);
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder");
|
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
|
// the manifest is primarily here to keep track of what files have been
|
||||||
@ -1088,7 +1139,7 @@ namespace Bloxstrap
|
|||||||
if (package is not null)
|
if (package is not null)
|
||||||
{
|
{
|
||||||
if (_cancelTokenSource.IsCancellationRequested)
|
if (_cancelTokenSource.IsCancellationRequested)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
await DownloadPackage(package);
|
await DownloadPackage(package);
|
||||||
ExtractPackage(package, entry.Value);
|
ExtractPackage(package, entry.Value);
|
||||||
@ -1099,6 +1150,11 @@ namespace Bloxstrap
|
|||||||
App.State.Save();
|
App.State.Save();
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
|
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)
|
private async Task DownloadPackage(Package package)
|
||||||
|
8
Bloxstrap/Enums/CustomThemeTemplate.cs
Normal file
8
Bloxstrap/Enums/CustomThemeTemplate.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Bloxstrap.Enums
|
||||||
|
{
|
||||||
|
public enum CustomThemeTemplate
|
||||||
|
{
|
||||||
|
Blank,
|
||||||
|
Simple
|
||||||
|
}
|
||||||
|
}
|
60
Bloxstrap/Exceptions/CustomThemeException.cs
Normal file
60
Bloxstrap/Exceptions/CustomThemeException.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
Bloxstrap/Extensions/CustomThemeTemplateEx.cs
Normal file
10
Bloxstrap/Extensions/CustomThemeTemplateEx.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Bloxstrap.Extensions
|
||||||
|
{
|
||||||
|
static class CustomThemeTemplateEx
|
||||||
|
{
|
||||||
|
public static string GetFileName(this CustomThemeTemplate template)
|
||||||
|
{
|
||||||
|
return $"CustomBootstrapperTemplate_{template}.xml";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,5 @@
|
|||||||
public string BootstrapperVersion { get; set; } = null!;
|
public string BootstrapperVersion { get; set; } = null!;
|
||||||
|
|
||||||
public DateTime? Timestamp { get; set; }
|
public DateTime? Timestamp { get; set; }
|
||||||
|
|
||||||
public bool IsBehindDefaultChannel { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<BloxstrapCustomBootstrapper Version="0" Height="320" Width="500">
|
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="500">
|
||||||
<!-- Put UI elements here -->
|
<!-- Put UI elements here -->
|
||||||
<!-- Examples of custom bootstrappers can be found at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
|
<!-- Examples of custom bootstrappers can be found at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
|
||||||
</BloxstrapCustomBootstrapper>
|
</BloxstrapCustomBootstrapper>
|
@ -0,0 +1,9 @@
|
|||||||
|
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="520" IgnoreTitleBarInset="True" Theme="Default" Margin="30">
|
||||||
|
<!-- Find more custom bootstrapper examples at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
|
||||||
|
<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>
|
514
Bloxstrap/Resources/Strings.Designer.cs
generated
514
Bloxstrap/Resources/Strings.Designer.cs
generated
@ -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>
|
/// <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..
|
/// 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>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use.
|
/// Looks up a localized string similar to Bloxstrap tried to upgrade Roblox but can't because Roblox'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>
|
/// <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..
|
/// 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>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Custom.
|
/// Looks up a localized string similar to Custom.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Export.
|
/// Looks up a localized string similar to Export.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Import from file.
|
/// Looks up a localized string similar to Import from file.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Reset.
|
/// Looks up a localized string similar to Reset.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Value.
|
/// Looks up a localized string similar to Value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -802,6 +892,385 @@ 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 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 "{0}".
|
||||||
|
/// </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 it's 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 Fluent..
|
||||||
|
/// </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} {2}.
|
||||||
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Add Fast Flag.
|
/// Looks up a localized string similar to Add Fast Flag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1170,6 +1639,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Catmoji.
|
/// Looks up a localized string similar to Catmoji.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1923,6 +2410,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Configure how Bloxstrap should look..
|
/// Looks up a localized string similar to Configure how Bloxstrap should look..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1270,7 +1270,179 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
|
|||||||
<value>All Bloxstrap logs</value>
|
<value>All Bloxstrap logs</value>
|
||||||
<comment>Label that appears next to a checkbox</comment>
|
<comment>Label that appears next to a checkbox</comment>
|
||||||
</data>
|
</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">
|
<data name="Menu.About.Licenses.Apache" xml:space="preserve">
|
||||||
<value>Apache License 2.0</value>
|
<value>Apache License 2.0</value>
|
||||||
</data>
|
</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>
|
||||||
|
</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>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.VersionNotSet" xml:space="preserve">
|
||||||
|
<value>{0} version is not set</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.VersionNotNumber" xml:space="preserve">
|
||||||
|
<value>{0} version is not a number</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.VersionNotSupported" xml:space="preserve">
|
||||||
|
<value>{0} version {1} is no longer supported</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.VersionNotRecognised" xml:space="preserve">
|
||||||
|
<value>{0} version {1} is not recognised</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementInvalidChild" xml:space="preserve">
|
||||||
|
<value>{0} cannot have a child of {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.UnknownElement" xml:space="preserve">
|
||||||
|
<value>Unknown element {0}</value>
|
||||||
|
</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>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeMissing" xml:space="preserve">
|
||||||
|
<value>Element {0} is missing the {1} attribute</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeInvalidType" xml:space="preserve">
|
||||||
|
<value>{0} {1} is not a valid {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeMustBeLargerThanMin" xml:space="preserve">
|
||||||
|
<value>{0} {1} must be larger than {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax" xml:space="preserve">
|
||||||
|
<value>{0} {1} must be smaller than {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.UnknownEnumValue" xml:space="preserve">
|
||||||
|
<value>{0} Unknown {1} {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeMultipleDefinitions" xml:space="preserve">
|
||||||
|
<value>{0} can only have one {1} defined</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeMultipleChildren" xml:space="preserve">
|
||||||
|
<value>{0}.{1} can only have one child</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementMultipleChildren" xml:space="preserve">
|
||||||
|
<value>{0} can only have one child</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeMissingChild" xml:space="preserve">
|
||||||
|
<value>{0}.{1} is missing it's child</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeParseError" xml:space="preserve">
|
||||||
|
<value>{0}.{1} could not be parsed into a {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeParseErrorNull" xml:space="preserve">
|
||||||
|
<value>{0}.{1} {2} is null</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementAttributeBlacklistedUriScheme" xml:space="preserve">
|
||||||
|
<value>{0}.{1} uses blacklisted scheme {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CustomTheme.Errors.ElementTypeCreationFailed" xml:space="preserve">
|
||||||
|
<value>{0} failed to create {1}: {2}</value>
|
||||||
|
</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 Fluent.</value>
|
||||||
|
</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>
|
||||||
</root>
|
</root>
|
@ -176,16 +176,15 @@
|
|||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");
|
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
App.Logger.WriteException(LOG_IDENT, ex);
|
||||||
|
|
||||||
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettings.roblox.com" + path);
|
try
|
||||||
}
|
{
|
||||||
|
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettings.roblox.com" + path);
|
||||||
// check if channel is behind LIVE
|
}
|
||||||
if (!isDefaultChannel)
|
catch (HttpRequestException httpEx)
|
||||||
{
|
when (!isDefaultChannel && BadChannelCodes.Contains(httpEx.StatusCode))
|
||||||
var defaultClientVersion = await GetInfo(DefaultChannel);
|
{
|
||||||
|
throw new InvalidChannelException(httpEx.StatusCode);
|
||||||
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan)
|
}
|
||||||
clientVersion.IsBehindDefaultChannel = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientVersionCache[cacheKey] = clientVersion;
|
ClientVersionCache[cacheKey] = clientVersion;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace Bloxstrap.UI.Elements.Bootstrapper
|
namespace Bloxstrap.UI.Elements.Bootstrapper
|
||||||
@ -37,7 +38,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception($"{xmlElement.Name} has invalid {attributeName}: {ex.Message}", ex);
|
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementAttributeConversionError", xmlElement.Name, attributeName, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +83,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception($"{element.Name} has invalid {attributeName}: {ex.Message}", ex);
|
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementAttributeConversionError", element.Name, attributeName, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
{
|
{
|
||||||
public partial class CustomDialog
|
public partial class CustomDialog
|
||||||
{
|
{
|
||||||
const int Version = 0;
|
const int Version = 1;
|
||||||
|
|
||||||
private class DummyFrameworkElement : FrameworkElement { }
|
private class DummyFrameworkElement : FrameworkElement { }
|
||||||
|
|
||||||
@ -59,11 +59,11 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
private static T HandleXml<T>(CustomDialog dialog, XElement xmlElement) where T : class
|
private static T HandleXml<T>(CustomDialog dialog, XElement xmlElement) where T : class
|
||||||
{
|
{
|
||||||
if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString()))
|
if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString()))
|
||||||
throw new Exception($"Unknown element {xmlElement.Name}");
|
throw new CustomThemeException("CustomTheme.Errors.UnknownElement", xmlElement.Name);
|
||||||
|
|
||||||
var element = _elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement);
|
var element = _elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement);
|
||||||
if (element is not T)
|
if (element is not T)
|
||||||
throw new Exception($"{xmlElement.Parent!.Name} cannot have a child of {xmlElement.Name}");
|
throw new CustomThemeException("CustomTheme.Errors.ElementInvalidChild", xmlElement.Parent!.Name, xmlElement.Name);
|
||||||
|
|
||||||
return (T)element;
|
return (T)element;
|
||||||
}
|
}
|
||||||
@ -78,19 +78,37 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
dialog.ElementGrid.Children.Add(uiElement);
|
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)
|
private void HandleXmlBase(XElement xml)
|
||||||
{
|
{
|
||||||
if (_initialised)
|
if (_initialised)
|
||||||
throw new Exception("Custom dialog has already been initialised");
|
throw new CustomThemeException("CustomTheme.Errors.DialogAlreadyInitialised");
|
||||||
|
|
||||||
if (xml.Name != "BloxstrapCustomBootstrapper")
|
if (xml.Name != "BloxstrapCustomBootstrapper")
|
||||||
throw new Exception("XML root is not a BloxstrapCustomBootstrapper");
|
throw new CustomThemeException("CustomTheme.Errors.InvalidRoot", "BloxstrapCustomBootstrapper");
|
||||||
|
|
||||||
if (xml.Attribute("Version")?.Value != Version.ToString())
|
AssertThemeVersion(xml.Attribute("Version")?.Value);
|
||||||
throw new Exception("Unknown BloxstrapCustomBootstrapper version");
|
|
||||||
|
|
||||||
if (xml.Descendants().Count() > MaxElements)
|
if (xml.Descendants().Count() > MaxElements)
|
||||||
throw new Exception($"Custom bootstrappers can have a maximum of {MaxElements} elements");
|
throw new CustomThemeException("CustomTheme.Errors.TooManyElements", MaxElements, xml.Descendants().Count());
|
||||||
|
|
||||||
_initialised = true;
|
_initialised = true;
|
||||||
|
|
||||||
@ -116,7 +134,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception($"XML parse failed: {ex.Message}", ex);
|
throw new CustomThemeException(ex, "CustomTheme.Errors.XMLParseFailed", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleXmlBase(xml);
|
HandleXmlBase(xml);
|
||||||
|
@ -150,7 +150,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception($"ImageBrush Failed to create BitmapImage: {ex.Message}", ex);
|
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
imageBrush.ImageSource = bitmapImage;
|
imageBrush.ImageSource = bitmapImage;
|
||||||
@ -217,7 +217,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
|
|
||||||
var first = brushElement.FirstNode as XElement;
|
var first = brushElement.FirstNode as XElement;
|
||||||
if (first == null)
|
if (first == null)
|
||||||
throw new Exception($"{xmlElement.Name} {name} is missing the brush");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissingChild", xmlElement.Name, name);
|
||||||
|
|
||||||
var brush = HandleXml<Brush>(dialog, first);
|
var brush = HandleXml<Brush>(dialog, first);
|
||||||
uiElement.SetValue(dependencyProperty, brush);
|
uiElement.SetValue(dependencyProperty, brush);
|
||||||
@ -620,7 +620,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception($"Image Failed to create BitmapImage: {ex.Message}", ex);
|
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
image.Source = bitmapImage;
|
image.Source = bitmapImage;
|
||||||
@ -693,7 +693,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
if (element.Name == "Grid.RowDefinitions")
|
if (element.Name == "Grid.RowDefinitions")
|
||||||
{
|
{
|
||||||
if (rowsSet)
|
if (rowsSet)
|
||||||
throw new Exception("Grid can only have one RowDefinitions defined");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", "Grid", "RowDefinitions");
|
||||||
rowsSet = true;
|
rowsSet = true;
|
||||||
|
|
||||||
HandleXmlElement_Grid_RowDefinitions(grid, dialog, element);
|
HandleXmlElement_Grid_RowDefinitions(grid, dialog, element);
|
||||||
@ -701,7 +701,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
else if (element.Name == "Grid.ColumnDefinitions")
|
else if (element.Name == "Grid.ColumnDefinitions")
|
||||||
{
|
{
|
||||||
if (columnsSet)
|
if (columnsSet)
|
||||||
throw new Exception("Grid can only have one ColumnDefinitions defined");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", "Grid", "ColumnDefinitions");
|
||||||
columnsSet = true;
|
columnsSet = true;
|
||||||
|
|
||||||
HandleXmlElement_Grid_ColumnDefinitions(grid, dialog, element);
|
HandleXmlElement_Grid_ColumnDefinitions(grid, dialog, element);
|
||||||
@ -760,7 +760,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
if (children.Any())
|
if (children.Any())
|
||||||
{
|
{
|
||||||
if (children.Count() > 1)
|
if (children.Count() > 1)
|
||||||
throw new Exception("Border can only have one child");
|
throw new CustomThemeException("CustomTheme.Errors.ElementMultipleChildren", "Border");
|
||||||
|
|
||||||
border.Child = HandleXml<UIElement>(dialog, children.First());
|
border.Child = HandleXml<UIElement>(dialog, children.First());
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
if (defaultValue != null)
|
if (defaultValue != null)
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
|
||||||
throw new Exception($"Element {element.Name} is missing the {attributeName} attribute");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissing", element.Name, attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return attribute.Value.ToString();
|
return attribute.Value.ToString();
|
||||||
@ -41,12 +41,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
if (defaultValue != null)
|
if (defaultValue != null)
|
||||||
return (T)defaultValue;
|
return (T)defaultValue;
|
||||||
|
|
||||||
throw new Exception($"Element {element.Name} is missing the {attributeName} attribute");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissing", element.Name, attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
T? parsed = ConvertValue<T>(attribute.Value);
|
T? parsed = ConvertValue<T>(attribute.Value);
|
||||||
if (parsed == null)
|
if (parsed == null)
|
||||||
throw new Exception($"{element.Name} {attributeName} is not a valid {typeof(T).Name}");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name);
|
||||||
|
|
||||||
return (T)parsed;
|
return (T)parsed;
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
|
|
||||||
T? parsed = ConvertValue<T>(attribute.Value);
|
T? parsed = ConvertValue<T>(attribute.Value);
|
||||||
if (parsed == null)
|
if (parsed == null)
|
||||||
throw new Exception($"{element.Name} {attributeName} is not a valid {typeof(T).Name}");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name);
|
||||||
|
|
||||||
return (T)parsed;
|
return (T)parsed;
|
||||||
}
|
}
|
||||||
@ -71,17 +71,17 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
private static void ValidateXmlElement(string elementName, string attributeName, int value, int? min = null, int? max = null)
|
private static void ValidateXmlElement(string elementName, string attributeName, int value, int? min = null, int? max = null)
|
||||||
{
|
{
|
||||||
if (min != null && value < min)
|
if (min != null && value < min)
|
||||||
throw new Exception($"{elementName} {attributeName} must be larger than {min}");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min);
|
||||||
if (max != null && value > max)
|
if (max != null && value > max)
|
||||||
throw new Exception($"{elementName} {attributeName} must be smaller than {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)
|
private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null)
|
||||||
{
|
{
|
||||||
if (min != null && value < min)
|
if (min != null && value < min)
|
||||||
throw new Exception($"{elementName} {attributeName} must be larger than {min}");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min);
|
||||||
if (max != null && value > max)
|
if (max != null && value > max)
|
||||||
throw new Exception($"{elementName} {attributeName} must be smaller than {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+.
|
// You can't do numeric only generics in .NET 6. The feature is exclusive to .NET 7+.
|
||||||
@ -136,7 +136,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
return FontWeights.UltraBlack;
|
return FontWeights.UltraBlack;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Exception($"{element.Name} Unknown FontWeight {value}");
|
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "FontWeight", value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
return FontStyles.Oblique;
|
return FontStyles.Oblique;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Exception($"{element.Name} Unknown FontStyle {value}");
|
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "FontStyle", value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
return TextDecorations.Underline;
|
return TextDecorations.Underline;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Exception($"{element.Name} Unknown TextDecorations {value}");
|
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "TextDecorations", value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +205,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
if (sourcePath == null)
|
if (sourcePath == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
// TODO: this is bad :(
|
||||||
return sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\");
|
return sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,13 +219,13 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
path = GetFullPath(dialog, path)!;
|
path = GetFullPath(dialog, path)!;
|
||||||
|
|
||||||
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out Uri? result))
|
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out Uri? result))
|
||||||
throw new Exception($"{xmlElement.Name} failed to parse {name} as Uri");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeParseError", xmlElement.Name, name, "Uri");
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
throw new Exception($"{xmlElement.Name} {name} Uri is null");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeParseErrorNull", xmlElement.Name, name, "Uri");
|
||||||
|
|
||||||
if (result.Scheme != "file")
|
if (result.Scheme != "file")
|
||||||
throw new Exception($"{xmlElement.Name} {name} uses blacklisted scheme {result.Scheme}");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeBlacklistedUriScheme", xmlElement.Name, name, result.Scheme);
|
||||||
|
|
||||||
return new GetImageSourceDataResult { Uri = result };
|
return new GetImageSourceDataResult { Uri = result };
|
||||||
}
|
}
|
||||||
@ -234,7 +235,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
var contentAttr = xmlElement.Attribute("Content");
|
var contentAttr = xmlElement.Attribute("Content");
|
||||||
var contentElement = xmlElement.Element($"{xmlElement.Name}.Content");
|
var contentElement = xmlElement.Element($"{xmlElement.Name}.Content");
|
||||||
if (contentAttr != null && contentElement != null)
|
if (contentAttr != null && contentElement != null)
|
||||||
throw new Exception($"{xmlElement.Name} can only have one Content defined");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", xmlElement.Name, "Content");
|
||||||
|
|
||||||
if (contentAttr != null)
|
if (contentAttr != null)
|
||||||
return GetTranslatedText(contentAttr.Value);
|
return GetTranslatedText(contentAttr.Value);
|
||||||
@ -244,11 +245,11 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
|
|
||||||
var children = contentElement.Elements();
|
var children = contentElement.Elements();
|
||||||
if (children.Count() > 1)
|
if (children.Count() > 1)
|
||||||
throw new Exception($"{xmlElement.Name}.Content can only have one child");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleChildren", xmlElement.Name, "Content");
|
||||||
|
|
||||||
var first = contentElement.FirstNode as XElement;
|
var first = contentElement.FirstNode as XElement;
|
||||||
if (first == null)
|
if (first == null)
|
||||||
throw new Exception($"{xmlElement.Name} Content is missing the content");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissingChild", xmlElement.Name, "Content");
|
||||||
|
|
||||||
var uiElement = HandleXml<UIElement>(dialog, first);
|
var uiElement = HandleXml<UIElement>(dialog, first);
|
||||||
return uiElement;
|
return uiElement;
|
||||||
@ -262,7 +263,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
|
|
||||||
var children = effectElement.Elements();
|
var children = effectElement.Elements();
|
||||||
if (children.Count() > 1)
|
if (children.Count() > 1)
|
||||||
throw new Exception($"{xmlElement.Name}.Effect can only have one child");
|
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleChildren", xmlElement.Name, "Effect");
|
||||||
|
|
||||||
var child = children.FirstOrDefault();
|
var child = children.FirstOrDefault();
|
||||||
if (child == null)
|
if (child == null)
|
||||||
|
165
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml
Normal file
165
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml
Normal 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>
|
230
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
Normal file
230
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
string name = $"Custom Theme {count + 1}";
|
||||||
|
|
||||||
|
// TODO: this sucks
|
||||||
|
if (File.Exists(GetThemePath(name)))
|
||||||
|
name += " " + Random.Shared.Next(1, 100000).ToString(); // 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 = Encoding.UTF8.GetString(Resource.Get(template.GetFileName()).Result);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@
|
|||||||
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Editor"
|
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Editor"
|
||||||
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Editor"
|
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Editor"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||||
Title="{Binding Path=Title, Mode=OneTime}"
|
Title="{Binding Path=Title, Mode=OneTime}"
|
||||||
Width="1000"
|
Width="1000"
|
||||||
@ -48,7 +49,7 @@
|
|||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
Command="{Binding Path=OpenThemeFolderCommand, Mode=OneTime}"
|
Command="{Binding Path=OpenThemeFolderCommand, Mode=OneTime}"
|
||||||
Content="Open Theme Folder" />
|
Content="{x:Static resources:Strings.CustomTheme_Editor_OpenThemeDirectory}" />
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
@ -63,14 +64,14 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="0,0,4,0"
|
Margin="0,0,4,0"
|
||||||
Command="{Binding Path=PreviewCommand, Mode=OneTime}"
|
Command="{Binding Path=PreviewCommand, Mode=OneTime}"
|
||||||
Content="Preview" />
|
Content="{x:Static resources:Strings.CustomTheme_Editor_Preview}" />
|
||||||
|
|
||||||
<ui:Button
|
<ui:Button
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="4,0,0,0"
|
Margin="4,0,0,0"
|
||||||
Appearance="Primary"
|
Appearance="Primary"
|
||||||
Command="{Binding Path=SaveCommand, Mode=OneTime}"
|
Command="{Binding Path=SaveCommand, Mode=OneTime}"
|
||||||
Content="Save" />
|
Content="{x:Static resources:Strings.CustomTheme_Editor_Save}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ui:Snackbar
|
<ui:Snackbar
|
||||||
|
@ -147,7 +147,7 @@ namespace Bloxstrap.UI.Elements.Editor
|
|||||||
_viewModel.ThemeSavedCallback = ThemeSavedCallback;
|
_viewModel.ThemeSavedCallback = ThemeSavedCallback;
|
||||||
_viewModel.Directory = directory;
|
_viewModel.Directory = directory;
|
||||||
_viewModel.Name = name;
|
_viewModel.Name = name;
|
||||||
_viewModel.Title = $"Editing \"{name}\"";
|
_viewModel.Title = string.Format(Strings.CustomTheme_Editor_Title, name);
|
||||||
_viewModel.Code = themeContents;
|
_viewModel.Code = themeContents;
|
||||||
|
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
@ -173,9 +173,9 @@ namespace Bloxstrap.UI.Elements.Editor
|
|||||||
private void ThemeSavedCallback(bool success, string message)
|
private void ThemeSavedCallback(bool success, string message)
|
||||||
{
|
{
|
||||||
if (success)
|
if (success)
|
||||||
Snackbar.Show("Settings saved!", message, Wpf.Ui.Common.SymbolRegular.CheckmarkCircle32, Wpf.Ui.Common.ControlAppearance.Success);
|
Snackbar.Show(Strings.CustomTheme_Editor_Save_Success, message, Wpf.Ui.Common.SymbolRegular.CheckmarkCircle32, Wpf.Ui.Common.ControlAppearance.Success);
|
||||||
else
|
else
|
||||||
Snackbar.Show("Error", message, Wpf.Ui.Common.SymbolRegular.ErrorCircle24, Wpf.Ui.Common.ControlAppearance.Danger);
|
Snackbar.Show(Strings.CustomTheme_Editor_Save_Error, message, Wpf.Ui.Common.SymbolRegular.ErrorCircle24, Wpf.Ui.Common.ControlAppearance.Danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ToCRLF(string text)
|
private static string ToCRLF(string text)
|
||||||
@ -194,7 +194,7 @@ namespace Bloxstrap.UI.Elements.Editor
|
|||||||
if (!_viewModel.CodeChanged)
|
if (!_viewModel.CodeChanged)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var result = Frontend.ShowMessageBox($"Save changes to {_viewModel.Name}?", MessageBoxImage.Information, MessageBoxButton.YesNoCancel);
|
var result = Frontend.ShowMessageBox(string.Format(Strings.CustomTheme_Editor_ConfirmSave, _viewModel.Name), MessageBoxImage.Information, MessageBoxButton.YesNoCancel);
|
||||||
if (result == MessageBoxResult.Cancel)
|
if (result == MessageBoxResult.Cancel)
|
||||||
{
|
{
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
|
@ -95,16 +95,19 @@
|
|||||||
</StackPanel.Style>
|
</StackPanel.Style>
|
||||||
<TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
<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}" />
|
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomThemeName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
<Grid Margin="0,4,0,0">
|
<Grid Margin="0,8,0,0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<ui:Button Grid.Column="0" Margin="0,0,2,0" Icon="Add28" Content="Rename" HorizontalAlignment="Stretch" Command="{Binding RenameCustomThemeCommand, Mode=OneTime}" />
|
|
||||||
<ui:Button Grid.Column="1" Margin="2,0,0,0" Icon="Add28" Content="Edit" HorizontalAlignment="Stretch" Command="{Binding EditCustomThemeCommand, Mode=OneTime}" />
|
<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>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="No custom theme selected." TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">
|
<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>
|
<TextBlock.Style>
|
||||||
<Style>
|
<Style>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
|
@ -67,7 +67,7 @@ namespace Bloxstrap.UI
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (App.Settings.Prop.SelectedCustomTheme == null)
|
if (App.Settings.Prop.SelectedCustomTheme == null)
|
||||||
throw new Exception("No custom theme selected");
|
throw new CustomThemeException("CustomTheme.Errors.NoThemeSelected");
|
||||||
|
|
||||||
CustomDialog dialog = new CustomDialog();
|
CustomDialog dialog = new CustomDialog();
|
||||||
dialog.ApplyCustomTheme(App.Settings.Prop.SelectedCustomTheme);
|
dialog.ApplyCustomTheme(App.Settings.Prop.SelectedCustomTheme);
|
||||||
@ -78,7 +78,7 @@ namespace Bloxstrap.UI
|
|||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
App.Logger.WriteException(LOG_IDENT, ex);
|
||||||
|
|
||||||
if (!App.LaunchSettings.QuietFlag.Active)
|
if (!App.LaunchSettings.QuietFlag.Active)
|
||||||
Frontend.ShowMessageBox($"Failed to setup custom bootstrapper: {ex.Message}.\nDefaulting to Fluent.", MessageBoxImage.Error);
|
ShowMessageBox(string.Format(Strings.CustomTheme_Errors_SetupFailed, ex.Message), MessageBoxImage.Error);
|
||||||
|
|
||||||
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
|
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
|
||||||
}
|
}
|
||||||
@ -110,5 +110,17 @@ namespace Bloxstrap.UI
|
|||||||
return messagebox.Result;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
68
Bloxstrap/UI/ViewModels/Dialogs/AddCustomThemeViewModel.cs
Normal file
68
Bloxstrap/UI/ViewModels/Dialogs/AddCustomThemeViewModel.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,13 @@ using System.Windows.Controls;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
using Bloxstrap.UI.Elements.Settings;
|
using Bloxstrap.UI.Elements.Settings;
|
||||||
using Bloxstrap.UI.Elements.Editor;
|
using Bloxstrap.UI.Elements.Editor;
|
||||||
|
using Bloxstrap.UI.Elements.Dialogs;
|
||||||
|
|
||||||
namespace Bloxstrap.UI.ViewModels.Settings
|
namespace Bloxstrap.UI.ViewModels.Settings
|
||||||
{
|
{
|
||||||
@ -23,6 +25,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme);
|
public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme);
|
||||||
public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme);
|
public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme);
|
||||||
public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme);
|
public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme);
|
||||||
|
public ICommand ExportCustomThemeCommand => new RelayCommand(ExportCustomTheme);
|
||||||
|
|
||||||
private void PreviewBootstrapper()
|
private void PreviewBootstrapper()
|
||||||
{
|
{
|
||||||
@ -131,31 +134,6 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CreateCustomThemeName()
|
|
||||||
{
|
|
||||||
int count = Directory.GetDirectories(Paths.CustomThemes).Count();
|
|
||||||
|
|
||||||
string name = $"Custom Theme {count + 1}";
|
|
||||||
|
|
||||||
// TODO: this sucks
|
|
||||||
if (Directory.Exists(Path.Combine(Paths.CustomThemes, name))) // DUCK
|
|
||||||
name += " " + Random.Shared.Next(1, 100000).ToString(); // easy
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateCustomThemeStructure(string name)
|
|
||||||
{
|
|
||||||
string dir = Path.Combine(Paths.CustomThemes, name);
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
|
|
||||||
string themeFilePath = Path.Combine(dir, "Theme.xml");
|
|
||||||
|
|
||||||
string templateContent = Encoding.UTF8.GetString(Resource.Get("CustomBootstrapperTemplate.xml").Result);
|
|
||||||
|
|
||||||
File.WriteAllText(themeFilePath, templateContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteCustomThemeStructure(string name)
|
private void DeleteCustomThemeStructure(string name)
|
||||||
{
|
{
|
||||||
string dir = Path.Combine(Paths.CustomThemes, name);
|
string dir = Path.Combine(Paths.CustomThemes, name);
|
||||||
@ -171,24 +149,20 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
|
|
||||||
private void AddCustomTheme()
|
private void AddCustomTheme()
|
||||||
{
|
{
|
||||||
string name = CreateCustomThemeName();
|
var dialog = new AddCustomThemeDialog();
|
||||||
|
dialog.ShowDialog();
|
||||||
|
|
||||||
try
|
if (dialog.Created)
|
||||||
{
|
{
|
||||||
CreateCustomThemeStructure(name);
|
CustomThemes.Add(dialog.ThemeName);
|
||||||
}
|
SelectedCustomThemeIndex = CustomThemes.Count - 1;
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException("AppearanceViewModel::AddCustomTheme", ex);
|
|
||||||
Frontend.ShowMessageBox($"Failed to create custom theme: {ex.Message}", MessageBoxImage.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomThemes.Add(name);
|
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
|
||||||
SelectedCustomThemeIndex = CustomThemes.Count - 1;
|
OnPropertyChanged(nameof(IsCustomThemeSelected));
|
||||||
|
|
||||||
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
|
if (dialog.OpenEditor)
|
||||||
OnPropertyChanged(nameof(IsCustomThemeSelected));
|
EditCustomTheme();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteCustomTheme()
|
private void DeleteCustomTheme()
|
||||||
@ -203,7 +177,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex);
|
App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex);
|
||||||
Frontend.ShowMessageBox($"Failed to delete custom theme {SelectedCustomTheme}: {ex.Message}", MessageBoxImage.Error);
|
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_DeleteFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +207,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
|
App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
|
||||||
Frontend.ShowMessageBox($"Failed to rename custom theme {SelectedCustomTheme}: {ex.Message}", MessageBoxImage.Error);
|
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_RenameFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +226,48 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
new BootstrapperEditorWindow(SelectedCustomTheme).ShowDialog();
|
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()
|
private void PopulateCustomThemes()
|
||||||
{
|
{
|
||||||
string? selected = App.Settings.Prop.SelectedCustomTheme;
|
string? selected = App.Settings.Prop.SelectedCustomTheme;
|
||||||
|
@ -31,5 +31,15 @@ namespace Bloxstrap.Utility
|
|||||||
fileInfo.IsReadOnly = false;
|
fileInfo.IsReadOnly = false;
|
||||||
App.Logger.WriteLine("Filesystem::AssertReadOnly", $"The following file was set as read-only: {filePath}");
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
104
Bloxstrap/Utility/PathValidator.cs
Normal file
104
Bloxstrap/Utility/PathValidator.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user