using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; using ICSharpCode.SharpZipLib.Zip; using Microsoft.Win32; using Bloxstrap.UI.Elements.Settings; using Bloxstrap.UI.Elements.Editor; using Bloxstrap.UI.Elements.Dialogs; namespace Bloxstrap.UI.ViewModels.Settings { public class AppearanceViewModel : NotifyPropertyChangedViewModel { private readonly Page _page; public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper); public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation); public ICommand AddCustomThemeCommand => new RelayCommand(AddCustomTheme); public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme); public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme); public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme); public ICommand ExportCustomThemeCommand => new RelayCommand(ExportCustomTheme); private void PreviewBootstrapper() { IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew(); if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) dialog.Message = Strings.Bootstrapper_StylePreview_ImageCancel; else dialog.Message = Strings.Bootstrapper_StylePreview_TextCancel; dialog.CancelEnabled = true; dialog.ShowBootstrapper(); } private void BrowseCustomIconLocation() { var dialog = new OpenFileDialog { Filter = $"{Strings.Menu_IconFiles}|*.ico" }; if (dialog.ShowDialog() != true) return; CustomIconLocation = dialog.FileName; OnPropertyChanged(nameof(CustomIconLocation)); } public AppearanceViewModel(Page page) { _page = page; foreach (var entry in BootstrapperIconEx.Selections) Icons.Add(new BootstrapperIconEntry { IconType = entry }); PopulateCustomThemes(); } public IEnumerable Themes { get; } = Enum.GetValues(typeof(Theme)).Cast(); public Theme Theme { get => App.Settings.Prop.Theme; set { App.Settings.Prop.Theme = value; ((MainWindow)Window.GetWindow(_page)!).ApplyTheme(); } } public static List Languages => Locale.GetLanguages(); public string SelectedLanguage { get => Locale.SupportedLocales[App.Settings.Prop.Locale]; set => App.Settings.Prop.Locale = Locale.GetIdentifierFromName(value); } public IEnumerable Dialogs { get; } = BootstrapperStyleEx.Selections; public BootstrapperStyle Dialog { get => App.Settings.Prop.BootstrapperStyle; set { App.Settings.Prop.BootstrapperStyle = value; OnPropertyChanged(nameof(CustomThemesExpanded)); // TODO: only fire when needed } } public bool CustomThemesExpanded => App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.CustomDialog; public ObservableCollection Icons { get; set; } = new(); public BootstrapperIcon Icon { get => App.Settings.Prop.BootstrapperIcon; set => App.Settings.Prop.BootstrapperIcon = value; } public string Title { get => App.Settings.Prop.BootstrapperTitle; set => App.Settings.Prop.BootstrapperTitle = value; } public string CustomIconLocation { get => App.Settings.Prop.BootstrapperIconCustomLocation; set { if (String.IsNullOrEmpty(value)) { if (App.Settings.Prop.BootstrapperIcon == BootstrapperIcon.IconCustom) App.Settings.Prop.BootstrapperIcon = BootstrapperIcon.IconBloxstrap; } else { App.Settings.Prop.BootstrapperIcon = BootstrapperIcon.IconCustom; } App.Settings.Prop.BootstrapperIconCustomLocation = value; OnPropertyChanged(nameof(Icon)); OnPropertyChanged(nameof(Icons)); } } private void DeleteCustomThemeStructure(string name) { string dir = Path.Combine(Paths.CustomThemes, name); Directory.Delete(dir, true); } private void RenameCustomThemeStructure(string oldName, string newName) { string oldDir = Path.Combine(Paths.CustomThemes, oldName); string newDir = Path.Combine(Paths.CustomThemes, newName); Directory.Move(oldDir, newDir); } private void AddCustomTheme() { var dialog = new AddCustomThemeDialog(); dialog.ShowDialog(); if (dialog.Created) { CustomThemes.Add(dialog.ThemeName); SelectedCustomThemeIndex = CustomThemes.Count - 1; OnPropertyChanged(nameof(SelectedCustomThemeIndex)); OnPropertyChanged(nameof(IsCustomThemeSelected)); if (dialog.OpenEditor) EditCustomTheme(); } } private void DeleteCustomTheme() { if (SelectedCustomTheme is null) return; try { DeleteCustomThemeStructure(SelectedCustomTheme); } catch (Exception ex) { App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex); Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_DeleteFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error); return; } CustomThemes.Remove(SelectedCustomTheme); if (CustomThemes.Any()) { SelectedCustomThemeIndex = CustomThemes.Count - 1; OnPropertyChanged(nameof(SelectedCustomThemeIndex)); } OnPropertyChanged(nameof(IsCustomThemeSelected)); } private void RenameCustomTheme() { const string LOG_IDENT = "AppearanceViewModel::RenameCustomTheme"; if (SelectedCustomTheme is null || SelectedCustomTheme == SelectedCustomThemeName) return; if (string.IsNullOrEmpty(SelectedCustomThemeName)) { Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameEmpty, MessageBoxImage.Error); return; } var validationResult = PathValidator.IsFileNameValid(SelectedCustomThemeName); if (validationResult != PathValidator.ValidationResult.Ok) { switch (validationResult) { case PathValidator.ValidationResult.IllegalCharacter: Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameIllegalCharacters, MessageBoxImage.Error); break; case PathValidator.ValidationResult.ReservedFileName: Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameReserved, MessageBoxImage.Error); break; default: App.Logger.WriteLine(LOG_IDENT, $"Got unhandled PathValidator::ValidationResult {validationResult}"); Debug.Assert(false); Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_Unknown, MessageBoxImage.Error); break; } return; } // better to check for the file instead of the directory so broken themes can be overwritten string path = Path.Combine(Paths.CustomThemes, SelectedCustomThemeName, "Theme.xml"); if (File.Exists(path)) { Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameTaken, MessageBoxImage.Error); return; } try { RenameCustomThemeStructure(SelectedCustomTheme, SelectedCustomThemeName); } catch (Exception ex) { App.Logger.WriteException(LOG_IDENT, ex); Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_RenameFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error); return; } int idx = CustomThemes.IndexOf(SelectedCustomTheme); CustomThemes[idx] = SelectedCustomThemeName; SelectedCustomThemeIndex = idx; OnPropertyChanged(nameof(SelectedCustomThemeIndex)); } private void EditCustomTheme() { if (SelectedCustomTheme is null) return; new BootstrapperEditorWindow(SelectedCustomTheme).ShowDialog(); } private void ExportCustomTheme() { if (SelectedCustomTheme is null) return; var dialog = new SaveFileDialog { FileName = $"{SelectedCustomTheme}.zip", Filter = $"{Strings.FileTypes_ZipArchive}|*.zip" }; if (dialog.ShowDialog() != true) return; string themeDir = Path.Combine(Paths.CustomThemes, SelectedCustomTheme); using var memStream = new MemoryStream(); using var zipStream = new ZipOutputStream(memStream); foreach (var filePath in Directory.EnumerateFiles(themeDir, "*.*", SearchOption.AllDirectories)) { string relativePath = filePath[(themeDir.Length + 1)..]; var entry = new ZipEntry(relativePath); entry.DateTime = DateTime.Now; zipStream.PutNextEntry(entry); using var fileStream = File.OpenRead(filePath); fileStream.CopyTo(zipStream); } zipStream.CloseEntry(); zipStream.Finish(); memStream.Position = 0; using var outputStream = File.OpenWrite(dialog.FileName); memStream.CopyTo(outputStream); Process.Start("explorer.exe", $"/select,\"{dialog.FileName}\""); } private void PopulateCustomThemes() { string? selected = App.Settings.Prop.SelectedCustomTheme; Directory.CreateDirectory(Paths.CustomThemes); foreach (string directory in Directory.GetDirectories(Paths.CustomThemes)) { if (!File.Exists(Path.Combine(directory, "Theme.xml"))) continue; // missing the main theme file, ignore string name = Path.GetFileName(directory); CustomThemes.Add(name); } if (selected != null) { int idx = CustomThemes.IndexOf(selected); if (idx != -1) { SelectedCustomThemeIndex = idx; OnPropertyChanged(nameof(SelectedCustomThemeIndex)); } else { SelectedCustomTheme = null; } } } public string? SelectedCustomTheme { get => App.Settings.Prop.SelectedCustomTheme; set => App.Settings.Prop.SelectedCustomTheme = value; } public string SelectedCustomThemeName { get; set; } = ""; public int SelectedCustomThemeIndex { get; set; } public ObservableCollection CustomThemes { get; set; } = new(); public bool IsCustomThemeSelected => SelectedCustomTheme is not null; } }