VRCMelonAssistant/ModAssistant/Classes/Themes.cs

392 lines
17 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.IO;
using System.Windows.Media;
2020-02-08 12:55:01 +13:00
using ModAssistant.Pages;
2020-02-14 16:49:43 +13:00
using System.Reflection;
using Microsoft.Win32;
2020-02-18 18:21:21 +13:00
using System.Windows.Media.Imaging;
2020-02-19 16:00:02 +13:00
using System.IO.Compression;
using System.Windows.Markup;
namespace ModAssistant
{
public class Themes
{
2020-02-14 18:18:59 +13:00
public static string LoadedTheme { get; private set; } //Currently loaded theme
public static List<string> LoadedThemes { get => loadedThemes.Keys.ToList(); } //String of themes that can be loaded
public static string ThemeDirectory => $"{Environment.CurrentDirectory}/Themes"; //Self explanatory.
2020-02-14 18:18:59 +13:00
//Local dictionary of ResourceDictionarys mapped by their names.
private static Dictionary<string, Theme> loadedThemes = new Dictionary<string, Theme>();
2020-02-17 14:12:38 +13:00
private static List<string> preInstalledThemes = new List<string> { "Light", "Dark", "Light Pink" }; //These themes will always be available to use.
2020-02-08 14:09:25 +13:00
/// <summary>
/// Load all themes from local Themes subfolder and from embedded resources.
/// This also refreshes the Themes dropdown in the Options screen.
/// </summary>
public static void LoadThemes()
{
loadedThemes.Clear();
2020-02-21 18:03:00 +13:00
//Begin by loading local themes. We should always load these first.
//I am doing loading here to prevent the LoadTheme function from becoming too crazy.
foreach (string localTheme in preInstalledThemes)
{
string location = $"Themes/{localTheme}.xaml";
Uri local = new Uri(location, UriKind.Relative);
ResourceDictionary localDictionary = new ResourceDictionary();
localDictionary.Source = local;
Theme theme = new Theme(localTheme, localDictionary);
loadedThemes.Add(localTheme, theme);
}
2020-02-08 14:09:25 +13:00
if (Directory.Exists(ThemeDirectory)) //Load themes from Themes subfolder if it exists.
{
2020-02-21 18:03:00 +13:00
//We then load each zipped theme.
2020-02-19 16:00:02 +13:00
foreach (string file in Directory.EnumerateFiles(ThemeDirectory))
{
FileInfo info = new FileInfo(file);
string name = Path.GetFileNameWithoutExtension(info.Name);
//Look for zip files with ".mat" extension.
2020-02-21 18:03:00 +13:00
if (info.Extension.ToLower().Equals(".mat"))
2020-02-19 16:00:02 +13:00
{
Theme theme = LoadZipTheme(ThemeDirectory, name, ".mat");
if (theme is null) continue;
AddOrModifyTheme(name, theme);
2020-02-19 16:00:02 +13:00
}
}
2020-02-21 18:03:00 +13:00
//Finally load any loose theme files in subfolders.
foreach (string directory in Directory.EnumerateDirectories(ThemeDirectory))
2020-02-19 16:00:02 +13:00
{
string name = directory.Split('\\').Last();
Theme theme = LoadTheme(directory, name);
if (theme is null) continue;
AddOrModifyTheme(name, theme);
2020-02-19 16:00:02 +13:00
}
}
2020-02-08 14:09:25 +13:00
if (Options.Instance != null && Options.Instance.ApplicationThemeComboBox != null) //Refresh Themes dropdown in Options screen.
{
2020-02-08 12:55:01 +13:00
Options.Instance.ApplicationThemeComboBox.ItemsSource = LoadedThemes;
Options.Instance.ApplicationThemeComboBox.SelectedIndex = LoadedThemes.IndexOf(LoadedTheme);
}
}
2020-02-17 13:39:52 +13:00
/// <summary>
/// Runs once at the start of the program, performs settings checking.
/// </summary>
/// <param name="savedTheme">Theme name retrieved from the settings file.</param>
public static void FirstLoad(string savedTheme)
{
2020-02-18 18:21:21 +13:00
if (string.IsNullOrEmpty(savedTheme))
2020-02-17 13:39:52 +13:00
{
Themes.ApplyWindowsTheme();
return;
}
try
{
Themes.ApplyTheme(savedTheme, false);
}
catch (ArgumentException)
{
Themes.ApplyWindowsTheme();
2020-02-17 15:16:22 +13:00
MainWindow.Instance.MainText = (string)Application.Current.FindResource("Themes:ThemeNotFound");
2020-02-17 13:39:52 +13:00
}
}
2020-02-08 14:09:25 +13:00
/// <summary>
/// Applies a loaded theme to ModAssistant.
/// </summary>
/// <param name="theme">Name of the theme.</param>
/// <param name="sendMessage">Send message to MainText (default: true).</param>
public static void ApplyTheme(string theme, bool sendMessage = true)
{
if (loadedThemes.TryGetValue(theme, out Theme newTheme))
{
2020-02-21 18:03:00 +13:00
//First, pause our video and hide it.
MainWindow.Instance.BackgroundVideo.Pause();
MainWindow.Instance.BackgroundVideo.Visibility = Visibility.Hidden;
Application.Current.Resources.MergedDictionaries.RemoveAt(2); //We might want to change this to a static integer or search by name.
LoadedTheme = theme;
Application.Current.Resources.MergedDictionaries.Insert(2, newTheme.ThemeDictionary); //Insert our new theme into the same spot as last time.
Properties.Settings.Default.SelectedTheme = theme;
Properties.Settings.Default.Save();
if (sendMessage)
{
2020-02-17 15:16:22 +13:00
MainWindow.Instance.MainText = string.Format((string)Application.Current.FindResource("Themes:ThemeSet"), theme);
}
ApplyWaifus();
2020-02-21 18:03:00 +13:00
if (File.Exists(newTheme.VideoLocation)) //Load our video if it exists.
{
Uri videoUri = new Uri(newTheme.VideoLocation, UriKind.Absolute);
MainWindow.Instance.BackgroundVideo.Visibility = Visibility.Visible;
2020-02-21 18:03:00 +13:00
//Load the source video if it's not the same as what's playing, or if the theme is loading for the first time.
if (!sendMessage || MainWindow.Instance.BackgroundVideo.Source?.AbsoluteUri != videoUri.AbsoluteUri)
{
MainWindow.Instance.BackgroundVideo.Stop();
MainWindow.Instance.BackgroundVideo.Source = videoUri;
}
MainWindow.Instance.BackgroundVideo.Play();
}
2020-02-08 14:09:25 +13:00
ReloadIcons();
}
2020-02-17 15:16:22 +13:00
else
{
throw new ArgumentException(string.Format((string)Application.Current.FindResource("Themes:ThemeMissing"), theme));
}
}
2020-02-08 14:09:25 +13:00
/// <summary>
2020-02-21 18:03:00 +13:00
/// Writes an Embedded Resource theme to disk. You cannot write an outside theme to disk.
2020-02-08 14:09:25 +13:00
/// </summary>
/// <param name="themeName">Name of local theme.</param>
2020-02-08 13:46:20 +13:00
public static void WriteThemeToDisk(string themeName)
{
if (!Directory.Exists(ThemeDirectory))
{
Directory.CreateDirectory(ThemeDirectory);
}
if (!File.Exists($@"{ThemeDirectory}\\{themeName}.xaml"))
{
2020-02-14 18:18:59 +13:00
/*
* Any theme that you want to write must be set as an Embedded Resource instead of the default Page.
2020-02-14 18:18:59 +13:00
* This is so that we can grab its exact content from Manifest, shown below.
* Writing it as is instead of using XAMLWriter keeps the source as is with comments, spacing, and organization.
* Using XAMLWriter would compress it into an unorganized mess.
*/
2020-02-14 16:49:43 +13:00
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream($"ModAssistant.Themes.{themeName}.xaml"))
2020-02-21 18:04:37 +13:00
using (FileStream writer = new FileStream($@"{ThemeDirectory}\\{themeName}\\{themeName}.xaml", FileMode.Create))
2020-02-14 16:49:43 +13:00
{
byte[] buffer = new byte[s.Length];
int read = s.Read(buffer, 0, (int)s.Length);
writer.Write(buffer, 0, buffer.Length);
}
2020-02-17 15:16:22 +13:00
MainWindow.Instance.MainText = string.Format((string)Application.Current.FindResource("Themes:SavedTemplateTheme"), themeName);
}
else
{
MessageBox.Show((string)Application.Current.FindResource("Themes:TemplateThemeExists"));
2020-02-08 13:46:20 +13:00
}
}
2020-02-08 14:09:25 +13:00
/// <summary>
/// Finds the theme set on Windows and applies it.
/// </summary>
public static void ApplyWindowsTheme()
{
using (RegistryKey key = Registry.CurrentUser
.OpenSubKey("Software").OpenSubKey("Microsoft")
.OpenSubKey("Windows").OpenSubKey("CurrentVersion")
.OpenSubKey("Themes").OpenSubKey("Personalize"))
{
object registryValueObject = key?.GetValue("AppsUseLightTheme");
if (registryValueObject != null)
{
if ((int)registryValueObject <= 0)
{
ApplyTheme("Dark", false);
return;
}
}
ApplyTheme("Light", false);
}
}
2020-02-08 14:09:25 +13:00
/// <summary>
/// Loads a Theme from a directory location.
2020-02-08 14:09:25 +13:00
/// </summary>
/// <param name="directory">The full directory path to the theme.</param>
/// <param name="name">Name of the containing folder.</param>
2020-02-08 14:09:25 +13:00
/// <returns></returns>
private static Theme LoadTheme(string directory, string name)
2020-02-08 14:09:25 +13:00
{
Theme theme = new Theme(name, null);
theme.Waifus = new Waifus();
foreach (string file in Directory.EnumerateFiles(directory))
2020-02-08 14:26:06 +13:00
{
FileInfo info = new FileInfo(file);
if (info.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) &&
!info.Name.EndsWith(".side.png", StringComparison.OrdinalIgnoreCase))
{
theme.Waifus.Background = new BitmapImage(new Uri(info.FullName));
}
if (info.Name.EndsWith(".side.png", StringComparison.OrdinalIgnoreCase))
{
theme.Waifus.Sidebar = new BitmapImage(new Uri(info.FullName));
}
if (info.Name.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase))
{
Uri resourceSource = new Uri(info.FullName);
ResourceDictionary dictionary = new ResourceDictionary();
dictionary.Source = resourceSource;
theme.ThemeDictionary = dictionary;
}
if (info.Name.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase))
{
theme.VideoLocation = info.FullName;
}
2020-02-08 14:26:06 +13:00
}
return theme;
}
private static void AddOrModifyTheme(string name, Theme theme)
{
if (loadedThemes.TryGetValue(name, out _))
{
if (theme.ThemeDictionary != null)
{
loadedThemes[name].ThemeDictionary = theme.ThemeDictionary;
}
if (theme.Waifus != null)
{
loadedThemes[name].Waifus = theme.Waifus;
}
if (!string.IsNullOrEmpty(theme.VideoLocation))
{
loadedThemes[name].VideoLocation = theme.VideoLocation;
}
}
else loadedThemes.Add(name, theme);
2020-02-08 14:09:25 +13:00
}
2020-02-18 19:26:54 +13:00
/// <summary>
2020-02-19 16:00:02 +13:00
/// Loads themes from pre-packged zips.
/// </summary>
/// <param name="directory">Theme directory</param>
/// <param name="name">Theme name</param>
/// <param name="extension">Theme extension</param>
private static Theme LoadZipTheme(string directory, string name, string extension)
2020-02-19 16:00:02 +13:00
{
Waifus waifus = new Waifus();
ResourceDictionary dictionary = null;
2020-02-19 16:00:02 +13:00
using (FileStream stream = new FileStream(Path.Combine(directory, name + extension), FileMode.Open))
using (ZipArchive archive = new ZipArchive(stream))
{
foreach (ZipArchiveEntry file in archive.Entries)
{
if (file.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) &&
!file.Name.EndsWith(".side.png", StringComparison.OrdinalIgnoreCase))
{
waifus.Background = GetImageFromStream(Utils.StreamToArray(file.Open()));
}
if (file.Name.EndsWith(".side.png", StringComparison.OrdinalIgnoreCase))
{
waifus.Sidebar = GetImageFromStream(Utils.StreamToArray(file.Open()));
}
if (file.Name.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase))
{
if (!loadedThemes.ContainsKey(name))
{
try
{
dictionary = (ResourceDictionary)XamlReader.Load(file.Open());
2020-02-19 16:00:02 +13:00
}
catch (Exception ex)
{
MessageBox.Show($"Could not load {name}.\n\n{ex.Message}\n\nIgnoring...");
}
}
}
}
}
Theme theme = new Theme(name, dictionary);
theme.Waifus = waifus;
return theme;
2020-02-19 16:00:02 +13:00
}
/// <summary>
/// Returns a BeatmapImage from a memory stream.
/// </summary>
/// <param name="stream">memory stream containing an image.</param>
/// <returns></returns>
private static BitmapImage GetImageFromStream(byte[] array)
{
using (var mStream = new MemoryStream(array))
{
BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = mStream;
image.EndInit();
if (image.CanFreeze)
image.Freeze();
return image;
}
}
/// <summary>
/// Applies waifus from currently loaded Theme.
2020-02-19 16:00:02 +13:00
/// </summary>
private static void ApplyWaifus()
{
Waifus waifus = loadedThemes[LoadedTheme].Waifus;
if (waifus?.Background is null)
{
MainWindow.Instance.BackgroundImage.Opacity = 0;
}
else
{
MainWindow.Instance.BackgroundImage.Opacity = 1;
MainWindow.Instance.BackgroundImage.ImageSource = waifus.Background;
}
if (waifus?.Sidebar is null)
{
MainWindow.Instance.SideImage.Visibility = Visibility.Hidden;
}
else
{
MainWindow.Instance.SideImage.Visibility = Visibility.Visible;
MainWindow.Instance.SideImage.Source = waifus.Sidebar;
}
2020-02-19 16:00:02 +13:00
}
2020-02-08 14:09:25 +13:00
/// <summary>
/// Reload the icon colors for the About, Info, Options, and Mods buttons from the currently loaded theme.
/// </summary>
private static void ReloadIcons()
{
2020-02-19 16:00:02 +13:00
ResourceDictionary icons = Application.Current.Resources.MergedDictionaries.First(x => x.Source?.ToString() == "Resources/Icons.xaml");
2020-02-08 14:09:25 +13:00
ChangeColor(icons, "AboutIconColor", "heartDrawingGroup");
ChangeColor(icons, "InfoIconColor", "info_circleDrawingGroup");
ChangeColor(icons, "OptionsIconColor", "cogDrawingGroup");
ChangeColor(icons, "ModsIconColor", "microchipDrawingGroup");
}
/// <summary>
/// Change the color of an image from the loaded theme.
/// </summary>
/// <param name="icons">ResourceDictionary that contains the image.</param>
/// <param name="ResourceColorName">Resource name of the color to change.</param>
/// <param name="DrawingGroupName">DrawingGroup name for the image.</param>
private static void ChangeColor(ResourceDictionary icons, string ResourceColorName, string DrawingGroupName)
{
Application.Current.Resources[ResourceColorName] = loadedThemes[LoadedTheme].ThemeDictionary[ResourceColorName];
2020-02-08 14:09:25 +13:00
((GeometryDrawing)((DrawingGroup)icons[DrawingGroupName]).Children[0]).Brush = (Brush)Application.Current.Resources[ResourceColorName];
}
2020-02-19 16:00:02 +13:00
private class Waifus
{
public BitmapImage Background = null;
public BitmapImage Sidebar = null;
}
private class Theme
{
public string Name;
public ResourceDictionary ThemeDictionary;
public Waifus Waifus = null;
public string VideoLocation = null;
public Theme(string name, ResourceDictionary dictionary)
{
Name = name;
ThemeDictionary = dictionary;
}
}
}
}