2020-02-03 18:26:28 +13:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Windows ;
using System.IO ;
2020-02-08 12:38:38 +13:00
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 ;
2020-02-17 12:20:27 +13:00
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 ;
2020-02-03 18:26:28 +13:00
2020-02-03 19:48:03 +13:00
namespace ModAssistant
2020-02-03 18:26:28 +13:00
{
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-03 18:26:28 +13:00
2020-02-14 18:18:59 +13:00
//Local dictionary of ResourceDictionarys mapped by their names.
2020-02-20 17:19:38 +13:00
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-03 18:26:28 +13:00
2020-02-23 18:00:39 +13:00
private static readonly int LOADEDTHEME_INDEX = 4 ;
2020-02-23 17:48:25 +13:00
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>
2020-02-03 18:26:28 +13:00
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 )
2020-02-21 17:54:25 +13:00
{
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-03 18:53:59 +13:00
{
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
{
2020-02-20 17:19:38 +13:00
Theme theme = LoadZipTheme ( ThemeDirectory , name , ".mat" ) ;
2020-02-21 17:54:25 +13:00
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.
2020-02-21 17:54:25 +13:00
foreach ( string directory in Directory . EnumerateDirectories ( ThemeDirectory ) )
2020-02-19 16:00:02 +13:00
{
2020-02-21 17:54:25 +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-03 18:53:59 +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 13:13:27 +13:00
{
2020-02-08 12:55:01 +13:00
Options . Instance . ApplicationThemeComboBox . ItemsSource = LoadedThemes ;
2020-02-08 13:13:27 +13:00
Options . Instance . ApplicationThemeComboBox . SelectedIndex = LoadedThemes . IndexOf ( LoadedTheme ) ;
}
2020-02-03 18:26:28 +13:00
}
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>
2020-02-17 14:48:42 +13:00
/// <param name="sendMessage">Send message to MainText (default: true).</param>
public static void ApplyTheme ( string theme , bool sendMessage = true )
2020-02-03 18:26:28 +13:00
{
2020-02-20 17:19:38 +13:00
if ( loadedThemes . TryGetValue ( theme , out Theme newTheme ) )
2020-02-03 18:26:28 +13:00
{
2020-02-21 18:03:00 +13:00
//First, pause our video and hide it.
2020-02-22 14:04:16 +13:00
LoadedTheme = theme ;
2020-02-20 17:19:38 +13:00
MainWindow . Instance . BackgroundVideo . Pause ( ) ;
MainWindow . Instance . BackgroundVideo . Visibility = Visibility . Hidden ;
2020-02-22 14:04:16 +13:00
if ( newTheme . ThemeDictionary ! = null )
{
2020-02-23 17:48:25 +13:00
Application . Current . Resources . MergedDictionaries . RemoveAt ( LOADEDTHEME_INDEX ) ; //We might want to change this to a static integer or search by name.
Application . Current . Resources . MergedDictionaries . Insert ( LOADEDTHEME_INDEX , newTheme . ThemeDictionary ) ; //Insert our new theme into the same spot as last time.
2020-02-22 14:04:16 +13:00
}
2020-02-08 14:13:41 +13:00
Properties . Settings . Default . SelectedTheme = theme ;
Properties . Settings . Default . Save ( ) ;
2020-02-17 12:20:27 +13:00
if ( sendMessage )
{
2020-02-17 15:16:22 +13:00
MainWindow . Instance . MainText = string . Format ( ( string ) Application . Current . FindResource ( "Themes:ThemeSet" ) , theme ) ;
2020-02-17 12:20:27 +13:00
}
2020-02-20 17:19:38 +13:00
ApplyWaifus ( ) ;
2020-02-21 18:03:00 +13:00
if ( File . Exists ( newTheme . VideoLocation ) ) //Load our video if it exists.
2020-02-20 17:19:38 +13:00
{
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.
2020-02-20 17:19:38 +13:00
if ( ! sendMessage | | MainWindow . Instance . BackgroundVideo . Source ? . AbsoluteUri ! = videoUri . AbsoluteUri )
{
MainWindow . Instance . BackgroundVideo . Stop ( ) ;
2020-02-22 14:10:21 +13:00
MainWindow . Instance . BackgroundVideo . Source = videoUri ;
2020-02-20 17:19:38 +13:00
}
MainWindow . Instance . BackgroundVideo . Play ( ) ;
}
2020-02-08 14:09:25 +13:00
ReloadIcons ( ) ;
2020-02-03 18:26:28 +13:00
}
2020-02-17 15:16:22 +13:00
else
{
throw new ArgumentException ( string . Format ( ( string ) Application . Current . FindResource ( "Themes:ThemeMissing" ) , theme ) ) ;
}
2020-02-03 18:26:28 +13:00
}
2020-02-08 12:38:38 +13:00
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 ) ;
}
2020-02-23 15:19:26 +13:00
if ( ! Directory . Exists ( $"{ThemeDirectory}\\{themeName}" ) )
{
Directory . CreateDirectory ( $"{ThemeDirectory}\\{themeName}" ) ;
}
2020-02-08 13:46:20 +13:00
if ( ! File . Exists ( $@"{ThemeDirectory}\\{themeName}.xaml" ) )
{
2020-02-14 18:18:59 +13:00
/ *
2020-02-21 17:54:25 +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
2020-02-17 12:20:27 +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>
2020-02-21 17:54:25 +13:00
/// Loads a Theme from a directory location.
2020-02-08 14:09:25 +13:00
/// </summary>
2020-02-21 17:54:25 +13:00
/// <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>
2020-02-21 17:54:25 +13:00
private static Theme LoadTheme ( string directory , string name )
2020-02-08 14:09:25 +13:00
{
2020-02-21 17:54:25 +13:00
Theme theme = new Theme ( name , null ) ;
theme . Waifus = new Waifus ( ) ;
2020-02-23 15:33:44 +13:00
foreach ( string file in Directory . EnumerateFiles ( directory ) . OrderBy ( x = > x ) )
2020-02-08 14:26:06 +13:00
{
2020-02-21 17:54:25 +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
}
2020-02-21 17:54:25 +13:00
return theme ;
}
2020-02-23 17:48:25 +13:00
/// <summary>
/// Modifies an already existing theme, or adds the theme if it doesn't exist
/// </summary>
/// <param name="name">Name of the theme.</param>
/// <param name="theme">Theme to modify/apply</param>
2020-02-21 17:54:25 +13:00
private static void AddOrModifyTheme ( string name , Theme theme )
{
if ( loadedThemes . TryGetValue ( name , out _ ) )
2020-02-20 17:19:38 +13:00
{
2020-02-21 17:54:25 +13:00
if ( theme . ThemeDictionary ! = null )
{
loadedThemes [ name ] . ThemeDictionary = theme . ThemeDictionary ;
}
2020-02-23 17:48:25 +13:00
if ( theme . Waifus ? . Background ! = null )
{
loadedThemes [ name ] . Waifus . Background = theme . Waifus . Background ;
}
if ( theme . Waifus ? . Sidebar ! = null )
2020-02-21 17:54:25 +13:00
{
2020-02-23 17:48:25 +13:00
loadedThemes [ name ] . Waifus . Sidebar = theme . Waifus . Sidebar ;
2020-02-21 17:54:25 +13:00
}
if ( ! string . IsNullOrEmpty ( theme . VideoLocation ) )
{
loadedThemes [ name ] . VideoLocation = theme . VideoLocation ;
}
2020-02-20 17:19:38 +13:00
}
2020-02-21 17:54:25 +13:00
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>
2020-02-20 17:19:38 +13:00
private static Theme LoadZipTheme ( string directory , string name , string extension )
2020-02-19 16:00:02 +13:00
{
2020-02-20 17:19:38 +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 ( ) ) ) ;
}
2020-02-23 15:19:26 +13:00
if ( file . Name . EndsWith ( ".mp4" , StringComparison . OrdinalIgnoreCase ) )
{
if ( ! Directory . Exists ( $"{ThemeDirectory}\\{name}" ) )
{
Directory . CreateDirectory ( $"{ThemeDirectory}\\{name}" ) ;
}
if ( ! File . Exists ( $"{ThemeDirectory}\\{name}\\_{name}.mp4" ) )
{
file . ExtractToFile ( $"{ThemeDirectory}\\{name}\\_{name}.mp4" , false ) ;
}
2020-02-23 15:33:44 +13:00
else
{
//Check to see if the lengths of each file are different. If they are, overwrite what currently exists.
2020-02-23 18:07:28 +13:00
//The reason we are also checking LoadedTheme against the name variable is to prevent overwriting a file that's
//already being used by ModAssistant and causing a System.IO.IOException.
2020-02-23 15:33:44 +13:00
FileInfo existingInfo = new FileInfo ( $"{ThemeDirectory}\\{name}\\_{name}.mp4" ) ;
2020-02-23 15:50:44 +13:00
if ( existingInfo . Length ! = file . Length & & LoadedTheme ! = name )
2020-02-23 15:33:44 +13:00
{
file . ExtractToFile ( $"{ThemeDirectory}\\{name}\\_{name}.mp4" , true ) ;
}
}
2020-02-23 15:19:26 +13:00
}
2020-02-19 16:00:02 +13:00
if ( file . Name . EndsWith ( ".xaml" , StringComparison . OrdinalIgnoreCase ) )
{
if ( ! loadedThemes . ContainsKey ( name ) )
{
try
{
2020-02-20 17:19:38 +13:00
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..." ) ;
}
}
}
}
}
2020-02-20 17:19:38 +13:00
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>
2020-02-20 17:19:38 +13:00
/// Applies waifus from currently loaded Theme.
2020-02-19 16:00:02 +13:00
/// </summary>
2020-02-20 17:19:38 +13:00
private static void ApplyWaifus ( )
{
Waifus waifus = loadedThemes [ LoadedTheme ] . Waifus ;
2020-02-20 17:30:06 +13:00
if ( waifus ? . Background is null )
{
MainWindow . Instance . BackgroundImage . Opacity = 0 ;
}
else
{
MainWindow . Instance . BackgroundImage . Opacity = 1 ;
MainWindow . Instance . BackgroundImage . ImageSource = waifus . Background ;
}
2020-02-20 17:19:38 +13:00
2020-02-20 17:30:06 +13:00
if ( waifus ? . Sidebar is null )
2020-02-20 17:19:38 +13:00
{
2020-02-20 17:30:06 +13:00
MainWindow . Instance . SideImage . Visibility = Visibility . Hidden ;
2020-02-20 17:19:38 +13:00
}
else
{
2020-02-20 17:30:06 +13:00
MainWindow . Instance . SideImage . Visibility = Visibility . Visible ;
MainWindow . Instance . SideImage . Source = waifus . Sidebar ;
2020-02-20 17:19:38 +13:00
}
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 )
{
2020-02-20 17:19:38 +13:00
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 ;
}
2020-02-20 17:19:38 +13:00
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 ;
}
}
2020-02-03 18:26:28 +13:00
}
}