add custom bootstrappers

This commit is contained in:
bluepilledgreat 2024-10-19 21:05:37 +01:00
parent 2e63da5779
commit 8ee6beb3de
18 changed files with 1238 additions and 3 deletions

View File

@ -28,6 +28,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate.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" />
@ -49,6 +50,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<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.37.0" />

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
<BloxstrapCustomBootstrapper Version="0" Height="200" Width="500">
<!-- Put UI elements here -->
<!-- Examples of custom bootstrappers can be found at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
</BloxstrapCustomBootstrapper>

View File

@ -1084,6 +1084,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Custom.
/// </summary>
public static string Enums_BootstrapperStyle_CustomDialog {
get {
return ResourceManager.GetString("Enums.BootstrapperStyle.CustomDialog", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Bloxstrap (Glass). /// Looks up a localized string similar to Bloxstrap (Glass).
/// </summary> /// </summary>

View File

@ -1239,4 +1239,7 @@ Would you like to enable test mode?</value>
<data name="Dialog.Exception.Version" xml:space="preserve"> <data name="Dialog.Exception.Version" xml:space="preserve">
<value>Version {0}</value> <value>Version {0}</value>
</data> </data>
<data name="Enums.BootstrapperStyle.CustomDialog" xml:space="preserve">
<value>Custom</value>
</data>
</root> </root>

View File

@ -0,0 +1,602 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml.Linq;
using Wpf.Ui.Markup;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
private const int MaxElements = 50;
private static ThicknessConverter? _thicknessConverter = null;
private static ThicknessConverter ThicknessConverter { get => _thicknessConverter ??= new ThicknessConverter(); }
private static BrushConverter? _brushConverter = null;
private static BrushConverter BrushConverter { get => _brushConverter ??= new BrushConverter(); }
private bool _initialised = false;
// prevent users from creating elements with the same name multiple times
private List<string> UsedNames { get; } = new List<string>();
private string ThemeDir { get; set; } = "";
delegate void HandleXmlElementDelegate(CustomDialog dialog, XElement xmlElement);
delegate void HandleXmlTransformationElementDelegate(TransformGroup group, XElement xmlElement);
private static Dictionary<string, HandleXmlElementDelegate> _elementHandlerMap = new Dictionary<string, HandleXmlElementDelegate>()
{
//["BloxstrapCustomBootstrapper"] = HandleXmlElement_BloxstrapCustomBootstrapper,
["TitleBar"] = HandleXmlElement_TitleBar,
["Button"] = HandleXmlElement_Button,
["ProgressBar"] = HandleXmlElement_ProgressBar,
["TextBlock"] = HandleXmlElement_TextBlock,
["Image"] = HandleXmlElement_Image
};
private static Dictionary<string, HandleXmlTransformationElementDelegate> _transformationHandlerMap = new Dictionary<string, HandleXmlTransformationElementDelegate>()
{
["ScaleTransform"] = HandleXmlTransformationElement_ScaleTransform,
["SkewTransform"] = HandleXmlTransformationElement_SkewTransform,
["RotateTransform"] = HandleXmlTransformationElement_RotateTransform,
["TranslateTransform"] = HandleXmlTransformationElement_TranslateTransform
};
#region Utilities
// https://stackoverflow.com/a/2961702
private static T? ConvertValue<T>(string input) where T : struct
{
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
// Cast ConvertFromString(string text) : object to (T)
//return (T?)converter.ConvertFromString(input);
return (T?)converter.ConvertFromInvariantString(input);
}
return default;
}
catch (NotSupportedException)
{
return default;
}
}
private static string GetXmlAttribute(XElement element, string attributeName, string? defaultValue = null)
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
{
if (defaultValue != null)
return defaultValue;
throw new Exception($"Element {element.Name} is missing the {attributeName} attribute");
}
return attribute.Value.ToString();
}
private static T ParseXmlAttribute<T>(XElement element, string attributeName, T? defaultValue = null) where T : struct
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
{
if (defaultValue != null)
return (T)defaultValue;
throw new Exception($"Element {element.Name} is missing the {attributeName} attribute");
}
T? parsed = ConvertValue<T>(attribute.Value);
if (parsed == null)
throw new Exception($"{element.Name} height is not a valid {typeof(T).Name}");
return (T)parsed;
}
private static void ValidateXmlElement(string elementName, string attributeName, int value, int? min = null, int? max = null)
{
if (min != null && value < min)
throw new Exception($"{elementName} {attributeName} must be larger than {min}");
if (max != null && value > max)
throw new Exception($"{elementName} {attributeName} must be smaller than {max}");
}
private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null)
{
if (min != null && value < min)
throw new Exception($"{elementName} {attributeName} must be larger than {min}");
if (max != null && value > max)
throw new Exception($"{elementName} {attributeName} must be smaller than {max}");
}
// You can't do numeric only generics in .NET 6. The feature is exclusive to .NET 7+.
private static double ParseXmlAttributeClamped(XElement element, string attributeName, double? defaultValue = null, double? min = null, double? max = null)
{
double value = ParseXmlAttribute<double>(element, attributeName, defaultValue);
ValidateXmlElement(element.Name.ToString(), attributeName, value, min, max);
return value;
}
private static int ParseXmlAttributeClamped(XElement element, string attributeName, int? defaultValue = null, int? min = null, int? max = null)
{
int value = ParseXmlAttribute<int>(element, attributeName, defaultValue);
ValidateXmlElement(element.Name.ToString(), attributeName, value, min, max);
return value;
}
private static object? GetThicknessFromXElement(XElement xmlElement, string attributeName)
{
string? attributeValue = xmlElement.Attribute(attributeName)?.Value?.ToString();
if (attributeValue == null)
return null;
object? thickness;
try
{
thickness = ThicknessConverter.ConvertFromInvariantString(attributeValue);
}
catch (Exception ex)
{
throw new Exception($"{xmlElement.Name} has invalid {attributeName}: {ex.Message}", ex);
}
if (thickness == null)
throw new Exception($"{xmlElement.Name} has invalid {attributeName}");
return thickness;
}
private static FontWeight GetFontWeightFromXElement(XElement element)
{
string? value = element.Attribute("FontWeight")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
value = "Normal";
// bruh
// https://learn.microsoft.com/en-us/dotnet/api/system.windows.fontweights?view=windowsdesktop-6.0
switch (value)
{
case "Thin":
return FontWeights.Thin;
case "ExtraLight":
case "UltraLight":
return FontWeights.ExtraLight;
case "Medium":
return FontWeights.Medium;
case "Normal":
case "Regular":
return FontWeights.Normal;
case "DemiBold":
case "SemiBold":
return FontWeights.DemiBold;
case "Bold":
return FontWeights.Bold;
case "ExtraBold":
case "UltraBold":
return FontWeights.ExtraBold;
case "Black":
case "Heavy":
return FontWeights.Black;
case "ExtraBlack":
case "UltraBlack":
return FontWeights.UltraBlack;
default:
throw new Exception($"{element.Name} Unknown FontWeight {value}");
}
}
private static FontStyle GetFontStyleFromXElement(XElement element)
{
string? value = element.Attribute("FontStyle")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
value = "Normal";
switch (value)
{
case "Normal":
return FontStyles.Normal;
case "Italic":
return FontStyles.Italic;
case "Oblique":
return FontStyles.Oblique;
default:
throw new Exception($"{element.Name} Unknown FontStyle {value}");
}
}
/// <summary>
/// Return type of string = Name of DynamicResource
/// Return type of brush = ... The Brush!!!
/// </summary>
private static object? GetBrushFromXElement(XElement element, string attributeName)
{
string? value = element.Attribute(attributeName)?.Value?.ToString();
if (value == null)
return null;
// dynamic resource name
if (value.StartsWith('{') && value.EndsWith('}'))
return value[1..^1];
try
{
return BrushConverter.ConvertFromInvariantString(value);
}
catch (Exception ex)
{
throw new Exception($"{element.Name} has invalid {attributeName}: {ex.Message}", ex);
}
}
#endregion
#region Transformation Elements
private static void HandleXmlTransformationElement_ScaleTransform(TransformGroup group, XElement xmlElement)
{
var st = new ScaleTransform();
st.ScaleX = ParseXmlAttribute<double>(xmlElement, "ScaleX", 1);
st.ScaleY = ParseXmlAttribute<double>(xmlElement, "ScaleY", 1);
st.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
st.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
group.Children.Add(st);
}
private static void HandleXmlTransformationElement_SkewTransform(TransformGroup group, XElement xmlElement)
{
var st = new SkewTransform();
st.AngleX = ParseXmlAttribute<double>(xmlElement, "AngleX", 0);
st.AngleY = ParseXmlAttribute<double>(xmlElement, "AngleY", 0);
st.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
st.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
group.Children.Add(st);
}
private static void HandleXmlTransformationElement_RotateTransform(TransformGroup group, XElement xmlElement)
{
var rt = new RotateTransform();
rt.Angle = ParseXmlAttribute<double>(xmlElement, "Angle", 0);
rt.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
rt.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
group.Children.Add(rt);
}
private static void HandleXmlTransformationElement_TranslateTransform(TransformGroup group, XElement xmlElement)
{
var tt = new TranslateTransform();
tt.X = ParseXmlAttribute<double>(xmlElement, "X", 0);
tt.Y = ParseXmlAttribute<double>(xmlElement, "Y", 0);
group.Children.Add(tt);
}
private static void HandleXmlTransformation(TransformGroup group, XElement xmlElement)
{
if (!_transformationHandlerMap.ContainsKey(xmlElement.Name.ToString()))
throw new Exception($"Unknown transformation {xmlElement.Name}");
_transformationHandlerMap[xmlElement.Name.ToString()](group, xmlElement);
}
private static void ApplyTransformations_UIElement(UIElement uiElement, XElement xmlElement)
{
if (!xmlElement.HasElements)
return;
var tg = new TransformGroup();
foreach (var child in xmlElement.Elements())
HandleXmlTransformation(tg, child);
if (tg.Children.Any())
uiElement.RenderTransform = tg;
}
#endregion
#region Elements
private static void HandleXmlElement_FrameworkElement(CustomDialog dialog, FrameworkElement uiElement, XElement xmlElement)
{
// prevent two elements from having the same name
string? name = xmlElement.Attribute("Name")?.Value?.ToString();
if (name != null)
{
if (dialog.UsedNames.Contains(name))
throw new Exception($"{xmlElement.Name} has duplicate name {name}");
dialog.UsedNames.Add(name);
}
uiElement.Name = name;
uiElement.Visibility = ParseXmlAttribute<Visibility>(xmlElement, "Visibility", Visibility.Visible);
object? margin = GetThicknessFromXElement(xmlElement, "Margin");
if (margin != null)
uiElement.Margin = (Thickness)margin;
uiElement.Height = ParseXmlAttributeClamped(xmlElement, "Height", defaultValue: 100.0, min: 0, max: 1000);
uiElement.Width = ParseXmlAttributeClamped(xmlElement, "Width", defaultValue: 100.0, min: 0, max: 1000);
// default values of these were originally Stretch but that was no good
uiElement.HorizontalAlignment = ParseXmlAttribute<HorizontalAlignment>(xmlElement, "HorizontalAlignment", HorizontalAlignment.Left);
uiElement.VerticalAlignment = ParseXmlAttribute<VerticalAlignment>(xmlElement, "VerticalAlignment", VerticalAlignment.Top);
int zIndex = ParseXmlAttributeClamped(xmlElement, "ZIndex", defaultValue: 0, min: 0, max: 1000);
Panel.SetZIndex(uiElement, zIndex);
}
private static void HandleXmlElement_Control(CustomDialog dialog, Control uiElement, XElement xmlElement)
{
HandleXmlElement_FrameworkElement(dialog, uiElement, xmlElement);
object? padding = GetThicknessFromXElement(xmlElement, "Padding");
if (padding != null)
uiElement.Padding = (Thickness)padding;
object? borderThickness = GetThicknessFromXElement(xmlElement, "BorderThickness");
if (borderThickness != null)
uiElement.BorderThickness = (Thickness)borderThickness;
// TODO: this isn't working for BloxstrapCustomBootstrapper. likely because of wpf.ui's themeservice.
object? foregroundBrush = GetBrushFromXElement(xmlElement, "Background");
if (foregroundBrush is Brush)
uiElement.Background = (Brush)foregroundBrush;
else if (foregroundBrush is string)
uiElement.SetResourceReference(Control.BackgroundProperty, foregroundBrush);
object? borderBrush = GetBrushFromXElement(xmlElement, "BorderBrush");
if (borderBrush is Brush)
uiElement.BorderBrush = (Brush)borderBrush;
else if (borderBrush is string)
uiElement.SetResourceReference(Control.BorderBrushProperty, borderBrush);
}
private static void HandleXmlElement_BloxstrapCustomBootstrapper(CustomDialog dialog, XElement xmlElement)
{
xmlElement.SetAttributeValue("Visibility", "Collapsed"); // don't show the bootstrapper yet!!!
HandleXmlElement_Control(dialog, dialog, xmlElement);
var theme = ParseXmlAttribute<Theme>(xmlElement, "Theme", Theme.Default);
dialog.Resources.MergedDictionaries.Clear();
dialog.Resources.MergedDictionaries.Add(new ThemesDictionary() { Theme = theme.GetFinal() == Theme.Dark ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light });
// set the margin & padding on the element grid
dialog.ElementGrid.Margin = dialog.Margin;
// TODO: put elementgrid inside a border?
dialog.Margin = new Thickness(0, 0, 0, 0);
dialog.Padding = new Thickness(0, 0, 0, 0);
dialog.MaxHeight = dialog.Height;
dialog.MaxWidth = dialog.Width;
}
private static void HandleXmlElement_TitleBar(CustomDialog dialog, XElement xmlElement)
{
xmlElement.SetAttributeValue("Name", "TitleBar"); // prevent two titlebars from existing
HandleXmlElement_Control(dialog, dialog.RootTitleBar, xmlElement);
Panel.SetZIndex(dialog.RootTitleBar, 1001); // always show above others
// properties we dont want modifiable
dialog.RootTitleBar.Height = double.NaN;
dialog.RootTitleBar.Width = double.NaN;
dialog.RootTitleBar.HorizontalAlignment = HorizontalAlignment.Stretch;
dialog.RootTitleBar.Margin = new Thickness(0, 0, 0, 0);
dialog.RootTitleBar.ShowMinimize = ParseXmlAttribute<bool>(xmlElement, "ShowMinimize", true);
dialog.RootTitleBar.ShowClose = ParseXmlAttribute<bool>(xmlElement, "ShowClose", true);
string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap";
dialog.Title = title;
dialog.RootTitleBar.Title = title;
}
private static void HandleXmlElement_Button(CustomDialog dialog, XElement xmlElement)
{
var button = new Button();
HandleXmlElement_Control(dialog, button, xmlElement);
button.Content = xmlElement.Attribute("Text")?.Value?.ToString();
if (xmlElement.Attribute("Name")?.Value == "CancelButton")
{
Binding cancelEnabledBinding = new Binding("CancelEnabled") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(button, Button.IsEnabledProperty, cancelEnabledBinding);
Binding cancelCommandBinding = new Binding("CancelInstallCommand");
BindingOperations.SetBinding(button, Button.CommandProperty, cancelCommandBinding);
}
ApplyTransformations_UIElement(button, xmlElement);
dialog.ElementGrid.Children.Add(button);
}
private static void HandleXmlElement_ProgressBar(CustomDialog dialog, XElement xmlElement)
{
var progressBar = new ProgressBar();
HandleXmlElement_Control(dialog, progressBar, xmlElement);
progressBar.IsIndeterminate = ParseXmlAttribute<bool>(xmlElement, "IsIndeterminate", false);
progressBar.Value = ParseXmlAttribute<double>(xmlElement, "Value", 0);
progressBar.Maximum = ParseXmlAttribute<double>(xmlElement, "Maximum", 100);
if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressBar")
{
Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, ProgressBar.IsIndeterminateProperty, isIndeterminateBinding);
Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, ProgressBar.MaximumProperty, maximumBinding);
Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(progressBar, ProgressBar.ValueProperty, valueBinding);
}
ApplyTransformations_UIElement(progressBar, xmlElement);
dialog.ElementGrid.Children.Add(progressBar);
}
private static void HandleXmlElement_TextBlock(CustomDialog dialog, XElement xmlElement)
{
var textBlock = new TextBlock();
HandleXmlElement_FrameworkElement(dialog, textBlock, xmlElement);
textBlock.Text = xmlElement.Attribute("Text")?.Value;
object? foregroundBrush = GetBrushFromXElement(xmlElement, "Foreground");
if (foregroundBrush is Brush)
textBlock.Foreground = (Brush)foregroundBrush;
else if (foregroundBrush is string)
textBlock.SetResourceReference(TextBlock.ForegroundProperty, foregroundBrush);
textBlock.FontSize = ParseXmlAttribute<double>(xmlElement, "FontSize", 12);
textBlock.FontWeight = GetFontWeightFromXElement(xmlElement);
textBlock.FontStyle = GetFontStyleFromXElement(xmlElement);
textBlock.TextAlignment = ParseXmlAttribute<TextAlignment>(xmlElement, "TextAlignment", TextAlignment.Center);
if (xmlElement.Attribute("Name")?.Value == "StatusText")
{
Binding textBinding = new Binding("Message") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, textBinding);
}
ApplyTransformations_UIElement(textBlock, xmlElement);
dialog.ElementGrid.Children.Add(textBlock);
}
private static void HandleXmlElement_Image(CustomDialog dialog, XElement xmlElement)
{
var image = new Image();
HandleXmlElement_FrameworkElement(dialog, image, xmlElement);
string sourcePath = GetXmlAttribute(xmlElement, "Source");
sourcePath = sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\");
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); // should this be modifiable by the user?
if (sourcePath == "{Icon}")
{
// bind the icon property
Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(image, Image.SourceProperty, binding);
}
else
{
if (!Uri.TryCreate(sourcePath, UriKind.RelativeOrAbsolute, out Uri? result))
throw new Exception("Image failed to parse Source as Uri");
if (result == null)
throw new Exception("Image Source uri is null");
BitmapImage bitmapImage;
try
{
bitmapImage = new BitmapImage(result);
}
catch (Exception ex)
{
throw new Exception($"Image Failed to create BitmapImage: {ex.Message}", ex);
}
image.Source = bitmapImage;
}
ApplyTransformations_UIElement(image, xmlElement);
dialog.ElementGrid.Children.Add(image);
}
private void HandleXml(CustomDialog dialog, XElement xmlElement)
{
if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString()))
throw new Exception($"Unknown element {xmlElement.Name}");
_elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement);
}
private void HandleXmlBase(XElement xml)
{
if (_initialised)
throw new Exception("Custom dialog has already been initialised");
if (xml.Name != "BloxstrapCustomBootstrapper")
throw new Exception("XML root is not a BloxstrapCustomBootstrapper");
if (xml.Attribute("Version")?.Value != "0")
throw new Exception("Unknown BloxstrapCustomBootstrapper version");
if (xml.Elements().Count() > MaxElements)
throw new Exception($"Custom bootstrappers can have a maximum of {MaxElements} elements");
_initialised = true;
// handle root
HandleXmlElement_BloxstrapCustomBootstrapper(this, xml);
// handle everything else
foreach (var child in xml.Elements())
HandleXml(this, child);
}
#endregion
#region Public APIs
public void ApplyCustomTheme(string name, string contents)
{
ThemeDir = Path.Combine(Paths.CustomThemes, name);
XElement xml;
try
{
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
xml = XElement.Load(ms);
}
catch (Exception ex)
{
throw new Exception($"XML parse failed: {ex.Message}", ex);
}
HandleXmlBase(xml);
}
public void ApplyCustomTheme(string name)
{
string path = Path.Combine(Paths.CustomThemes, name, "Theme.xml");
ApplyCustomTheme(name, File.ReadAllText(path));
}
#endregion
}
}

View File

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

View File

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

View File

@ -0,0 +1,69 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Editor.BootstrapperEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Editor"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Editor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="{Binding Path=Title, Mode=OneTime}"
Width="1000"
Height="500"
d:DataContext="{d:DesignInstance dmodels:BootstrapperEditorWindowViewModel,
IsDesignTimeCreatable=True}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar
x:Name="RootTitleBar"
Title="{Binding Path=Title, Mode=OneTime}"
Grid.Row="0"
Padding="8"
ForceShutdown="False"
Icon="pack://application:,,,/Bloxstrap.ico"
MinimizeToTray="False"
UseSnapLayout="True" />
<avalonedit:TextEditor
x:Name="UIXML"
Grid.Row="1"
Margin="10,10,10,0"
ShowLineNumbers="True"
SyntaxHighlighting="XML"
TextChanged="OnCodeChanged" />
<Grid
Grid.Row="2"
Margin="10"
HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:Button
Grid.Column="0"
Margin="0,0,5,0"
HorizontalAlignment="Center"
Command="{Binding Path=PreviewCommand, Mode=OneTime}"
Content="Preview" />
<ui:Button
Grid.Column="1"
Margin="5,0,0,0"
HorizontalAlignment="Center"
Command="{Binding Path=SaveCommand, Mode=OneTime}"
Content="Save" />
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,31 @@
using Bloxstrap.UI.Elements.Base;
using Bloxstrap.UI.ViewModels.Editor;
namespace Bloxstrap.UI.Elements.Editor
{
/// <summary>
/// Interaction logic for BootstrapperEditorWindow.xaml
/// </summary>
public partial class BootstrapperEditorWindow : WpfUiWindow
{
public BootstrapperEditorWindow(string name)
{
var viewModel = new BootstrapperEditorWindowViewModel();
viewModel.Name = name;
viewModel.Title = $"Editing \"{name}\"";
viewModel.Code = File.ReadAllText(Path.Combine(Paths.CustomThemes, name, "Theme.xml"));
DataContext = viewModel;
InitializeComponent();
UIXML.Text = viewModel.Code;
}
private void OnCodeChanged(object sender, EventArgs e)
{
BootstrapperEditorWindowViewModel viewModel = (BootstrapperEditorWindowViewModel)DataContext;
viewModel.Code = UIXML.Text;
viewModel.OnPropertyChanged(nameof(viewModel.Code));
}
}
}

View File

@ -8,7 +8,7 @@
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls" xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:resources="clr-namespace:Bloxstrap.Resources" xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="640" d:DesignWidth="800" d:DesignHeight="900" d:DesignWidth="800"
Title="AppearancePage" Title="AppearancePage"
Scrollable="True"> Scrollable="True">
<StackPanel Margin="0,0,14,14"> <StackPanel Margin="0,0,14,14">
@ -114,5 +114,59 @@
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,4,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Appearance_CustomisationIcon_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" /> <TextBlock Grid.Row="3" Grid.Column="1" Margin="0,4,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Appearance_CustomisationIcon_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</Grid> </Grid>
</ui:CardExpander> </ui:CardExpander>
<TextBlock Text="Custom Themes" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Text="pizzaboxer help me make this look better" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="CustomThemesListBox" Height="200" Grid.Row="0" Grid.Column="0" Margin="0,0,4,0" ItemsSource="{Binding CustomThemes, Mode=OneWay}" SelectionChanged="CustomThemeSelection" SelectedIndex="{Binding SelectedCustomThemeIndex, Mode=TwoWay}" />
<Grid Grid.Row="1" Grid.Column="0" Margin="0,8,4,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0" Margin="0,0,4,0" Icon="Add28" Content="{x:Static resources:Strings.Common_New}" HorizontalAlignment="Stretch" Command="{Binding AddCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="1" Margin="4,0,0,0" Icon="Delete28" Content="{x:Static resources:Strings.Common_Delete}" HorizontalAlignment="Stretch" Appearance="Danger" IsEnabled="{Binding IsCustomThemeSelected, Mode=OneWay}" Command="{Binding DeleteCustomThemeCommand, Mode=OneTime}" />
</Grid>
<StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Margin="4,0,0,0">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCustomThemeSelected}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomThemeName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</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}" />
</Grid>
</StackPanel>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="No custom theme selected." TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCustomThemeSelected}" Value="True">
<Setter Property="TextBlock.Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</StackPanel> </StackPanel>
</ui:UiPage> </ui:UiPage>

View File

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

View File

@ -54,6 +54,32 @@ namespace Bloxstrap.UI
}); });
} }
private static IBootstrapperDialog GetCustomBootstrapper()
{
const string LOG_IDENT = "Frontend::GetCustomBootstrapper";
Directory.CreateDirectory(Paths.CustomThemes);
try
{
if (App.Settings.Prop.SelectedCustomTheme == null)
throw new Exception("No custom theme selected");
CustomDialog dialog = new CustomDialog();
dialog.ApplyCustomTheme(App.Settings.Prop.SelectedCustomTheme);
return dialog;
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
if (!App.LaunchSettings.QuietFlag.Active)
Frontend.ShowMessageBox($"Failed to setup custom bootstrapper: {ex.Message}.\nDefaulting to Fluent.", MessageBoxImage.Error);
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
}
}
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style) public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
{ {
return style switch return style switch
@ -66,6 +92,7 @@ namespace Bloxstrap.UI
BootstrapperStyle.ByfronDialog => new ByfronDialog(), BootstrapperStyle.ByfronDialog => new ByfronDialog(),
BootstrapperStyle.FluentDialog => new FluentDialog(false), BootstrapperStyle.FluentDialog => new FluentDialog(false),
BootstrapperStyle.FluentAeroDialog => new FluentDialog(true), BootstrapperStyle.FluentAeroDialog => new FluentDialog(true),
BootstrapperStyle.CustomDialog => GetCustomBootstrapper(),
_ => new FluentDialog(false) _ => new FluentDialog(false)
}; };
} }

View File

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

View File

@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32; using Microsoft.Win32;
using Bloxstrap.UI.Elements.Settings; using Bloxstrap.UI.Elements.Settings;
using Bloxstrap.UI.Elements.Editor;
namespace Bloxstrap.UI.ViewModels.Settings namespace Bloxstrap.UI.ViewModels.Settings
{ {
@ -18,6 +19,11 @@ namespace Bloxstrap.UI.ViewModels.Settings
public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper); public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper);
public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation); public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation);
public ICommand AddCustomThemeCommand => new RelayCommand(AddCustomTheme);
public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme);
public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme);
public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme);
private void PreviewBootstrapper() private void PreviewBootstrapper()
{ {
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew(); IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
@ -51,6 +57,8 @@ namespace Bloxstrap.UI.ViewModels.Settings
foreach (var entry in BootstrapperIconEx.Selections) foreach (var entry in BootstrapperIconEx.Selections)
Icons.Add(new BootstrapperIconEntry { IconType = entry }); Icons.Add(new BootstrapperIconEntry { IconType = entry });
PopulateCustomThemes();
} }
public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>(); public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>();
@ -116,5 +124,170 @@ namespace Bloxstrap.UI.ViewModels.Settings
OnPropertyChanged(nameof(Icons)); OnPropertyChanged(nameof(Icons));
} }
} }
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)
{
string dir = Path.Combine(Paths.CustomThemes, name);
Directory.Delete(dir, true);
}
private void RenameCustomThemeStructure(string oldName, string newName)
{
string oldDir = Path.Combine(Paths.CustomThemes, oldName);
string newDir = Path.Combine(Paths.CustomThemes, newName);
Directory.Move(oldDir, newDir);
}
private void AddCustomTheme()
{
string name = CreateCustomThemeName();
try
{
CreateCustomThemeStructure(name);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::AddCustomTheme", ex);
Frontend.ShowMessageBox($"Failed to create custom theme: {ex.Message}", MessageBoxImage.Error);
return;
}
CustomThemes.Add(name);
SelectedCustomThemeIndex = CustomThemes.Count - 1;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
OnPropertyChanged(nameof(IsCustomThemeSelected));
}
private void DeleteCustomTheme()
{
if (SelectedCustomTheme is null)
return;
try
{
DeleteCustomThemeStructure(SelectedCustomTheme);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex);
Frontend.ShowMessageBox($"Failed to delete custom theme {SelectedCustomTheme}: {ex.Message}", MessageBoxImage.Error);
return;
}
CustomThemes.Remove(SelectedCustomTheme);
if (CustomThemes.Any())
{
SelectedCustomThemeIndex = CustomThemes.Count - 1;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
OnPropertyChanged(nameof(IsCustomThemeSelected));
}
private void RenameCustomTheme()
{
if (SelectedCustomTheme is null)
return;
if (SelectedCustomTheme == SelectedCustomThemeName)
return;
try
{
RenameCustomThemeStructure(SelectedCustomTheme, SelectedCustomThemeName);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
Frontend.ShowMessageBox($"Failed to rename custom theme {SelectedCustomTheme}: {ex.Message}", MessageBoxImage.Error);
return;
}
int idx = CustomThemes.IndexOf(SelectedCustomTheme);
CustomThemes[idx] = SelectedCustomThemeName;
SelectedCustomThemeIndex = idx;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
private void EditCustomTheme()
{
if (SelectedCustomTheme is null)
return;
new BootstrapperEditorWindow(SelectedCustomTheme).ShowDialog();
}
private void PopulateCustomThemes()
{
string? selected = App.Settings.Prop.SelectedCustomTheme;
Directory.CreateDirectory(Paths.CustomThemes);
foreach (string directory in Directory.GetDirectories(Paths.CustomThemes))
{
if (!File.Exists(Path.Combine(directory, "Theme.xml")))
continue; // missing the main theme file, ignore
string name = Path.GetFileName(directory);
CustomThemes.Add(name);
}
if (selected != null)
{
int idx = CustomThemes.IndexOf(selected);
if (idx != -1)
{
SelectedCustomThemeIndex = idx;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
else
{
SelectedCustomTheme = null;
}
}
}
public string? SelectedCustomTheme
{
get => App.Settings.Prop.SelectedCustomTheme;
set => App.Settings.Prop.SelectedCustomTheme = value;
}
public string SelectedCustomThemeName { get; set; } = "";
public int SelectedCustomThemeIndex { get; set; }
public ObservableCollection<string> CustomThemes { get; set; } = new();
public bool IsCustomThemeSelected => SelectedCustomTheme is not null;
} }
} }