diff --git a/.hgignore b/.hgignore
index 6fd0798519..c8162e4c87 100644
--- a/.hgignore
+++ b/.hgignore
@@ -29,6 +29,8 @@ syntax: glob
obj/
[Rr]elease*/
ProgramData*/
+ProgramData-Server*/
+ProgramData-UI*/
_ReSharper*/
[Tt]humbs.db
[Tt]est[Rr]esult*
diff --git a/MediaBrowser.Api/Drawing/DrawingUtils.cs b/MediaBrowser.Api/Drawing/DrawingUtils.cs
new file mode 100644
index 0000000000..f76a74218f
--- /dev/null
+++ b/MediaBrowser.Api/Drawing/DrawingUtils.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Drawing;
+
+namespace MediaBrowser.Api.Drawing
+{
+ public static class DrawingUtils
+ {
+ ///
+ /// Resizes a set of dimensions
+ ///
+ public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
+ {
+ return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
+ }
+
+ ///
+ /// Resizes a set of dimensions
+ ///
+ /// The original size object
+ /// A new fixed width, if desired
+ /// A new fixed neight, if desired
+ /// A max fixed width, if desired
+ /// A max fixed height, if desired
+ /// A new size object
+ public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
+ {
+ decimal newWidth = size.Width;
+ decimal newHeight = size.Height;
+
+ if (width.HasValue && height.HasValue)
+ {
+ newWidth = width.Value;
+ newHeight = height.Value;
+ }
+
+ else if (height.HasValue)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, height.Value);
+ newHeight = height.Value;
+ }
+
+ else if (width.HasValue)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, width.Value);
+ newWidth = width.Value;
+ }
+
+ if (maxHeight.HasValue && maxHeight < newHeight)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
+ newHeight = maxHeight.Value;
+ }
+
+ if (maxWidth.HasValue && maxWidth < newWidth)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
+ newWidth = maxWidth.Value;
+ }
+
+ return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
+ }
+
+ private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
+ {
+ decimal scaleFactor = newHeight;
+ scaleFactor /= currentHeight;
+ scaleFactor *= currentWidth;
+
+ return scaleFactor;
+ }
+
+ private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
+ {
+ decimal scaleFactor = newWidth;
+ scaleFactor /= currentWidth;
+ scaleFactor *= currentHeight;
+
+ return scaleFactor;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Drawing/ImageProcessor.cs b/MediaBrowser.Api/Drawing/ImageProcessor.cs
new file mode 100644
index 0000000000..1a471acf54
--- /dev/null
+++ b/MediaBrowser.Api/Drawing/ImageProcessor.cs
@@ -0,0 +1,148 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Api.Drawing
+{
+ public static class ImageProcessor
+ {
+ ///
+ /// Processes an image by resizing to target dimensions
+ ///
+ /// The entity that owns the image
+ /// The image type
+ /// The image index (currently only used with backdrops)
+ /// The stream to save the new image to
+ /// Use if a fixed width is required. Aspect ratio will be preserved.
+ /// Use if a fixed height is required. Aspect ratio will be preserved.
+ /// Use if a max width is required. Aspect ratio will be preserved.
+ /// Use if a max height is required. Aspect ratio will be preserved.
+ /// Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.
+ public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
+ {
+ Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
+
+ // Determine the output size based on incoming parameters
+ Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
+
+ Bitmap thumbnail;
+
+ // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+ if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
+ {
+ thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
+ }
+ else
+ {
+ thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
+ }
+
+ thumbnail.MakeTransparent();
+
+ // Preserve the original resolution
+ thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+
+ Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
+
+ thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+ thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+ thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+ thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+
+ thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
+
+ ImageFormat outputFormat = originalImage.RawFormat;
+
+ // Write to the output stream
+ SaveImage(outputFormat, thumbnail, toStream, quality);
+
+ thumbnailGraph.Dispose();
+ thumbnail.Dispose();
+ originalImage.Dispose();
+ }
+
+ public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
+ {
+ var item = entity as BaseItem;
+
+ if (item != null)
+ {
+ if (imageType == ImageType.Logo)
+ {
+ return item.LogoImagePath;
+ }
+ if (imageType == ImageType.Backdrop)
+ {
+ return item.BackdropImagePaths.ElementAt(imageIndex);
+ }
+ if (imageType == ImageType.Banner)
+ {
+ return item.BannerImagePath;
+ }
+ if (imageType == ImageType.Art)
+ {
+ return item.ArtImagePath;
+ }
+ if (imageType == ImageType.Thumbnail)
+ {
+ return item.ThumbnailImagePath;
+ }
+ }
+
+ return entity.PrimaryImagePath;
+ }
+
+ public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
+ {
+ // Use special save methods for jpeg and png that will result in a much higher quality image
+ // All other formats use the generic Image.Save
+ if (ImageFormat.Jpeg.Equals(outputFormat))
+ {
+ SaveJpeg(newImage, toStream, quality);
+ }
+ else if (ImageFormat.Png.Equals(outputFormat))
+ {
+ newImage.Save(toStream, ImageFormat.Png);
+ }
+ else
+ {
+ newImage.Save(toStream, outputFormat);
+ }
+ }
+
+ public static void SaveJpeg(Image image, Stream target, int? quality)
+ {
+ if (!quality.HasValue)
+ {
+ quality = 90;
+ }
+
+ using (var encoderParameters = new EncoderParameters(1))
+ {
+ encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
+ image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
+ }
+ }
+
+ public static ImageCodecInfo GetImageCodecInfo(string mimeType)
+ {
+ ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
+
+ for (int i = 0; i < info.Length; i++)
+ {
+ ImageCodecInfo ici = info[i];
+ if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
+ {
+ return ici;
+ }
+ }
+ return info[1];
+ }
+ }
+}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index 44b58852b8..1af7e71bd0 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -105,7 +105,7 @@
- xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData\Plugins\" /y
+ xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.png
new file mode 100644
index 0000000000..f272ed92a1
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.png differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.png
new file mode 100644
index 0000000000..93a06e3083
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.png differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.png
new file mode 100644
index 0000000000..b9b6765c7f
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.png differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.png
new file mode 100644
index 0000000000..2e526f8953
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.png differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.png
new file mode 100644
index 0000000000..94131ed2d1
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.png differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.png
new file mode 100644
index 0000000000..2a51cd5446
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.png differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.png
new file mode 100644
index 0000000000..f413a2ed73
Binary files /dev/null and b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.png differ
diff --git a/MediaBrowser.ServerApplication/App.config b/MediaBrowser.ServerApplication/App.config
index 4b2ffa81d2..a5c9453388 100644
--- a/MediaBrowser.ServerApplication/App.config
+++ b/MediaBrowser.ServerApplication/App.config
@@ -1,7 +1,7 @@
-
+
diff --git a/MediaBrowser.Plugins.sln b/MediaBrowser.UI.sln
similarity index 71%
rename from MediaBrowser.Plugins.sln
rename to MediaBrowser.UI.sln
index cd237ebce2..65130e3d9d 100644
--- a/MediaBrowser.Plugins.sln
+++ b/MediaBrowser.UI.sln
@@ -1,11 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.DefaultTheme", "MediaBrowser.Plugins.DefaultTheme\MediaBrowser.Plugins.DefaultTheme.csproj", "{6E892999-711D-4E24-8BAC-DACF5BFA783A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.UI", "..\MediaBrowserUI\MediaBrowser.UI\MediaBrowser.UI.csproj", "{B5ECE1FB-618E-420B-9A99-8E972D76920A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.UI", "MediaBrowser.UI\MediaBrowser.UI.csproj", "{B5ECE1FB-618E-420B-9A99-8E972D76920A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
EndProject
@@ -13,36 +9,48 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Model", "Media
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.ApiInteraction", "MediaBrowser.ApiInteraction\MediaBrowser.ApiInteraction.csproj", "{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.DefaultTheme", "MediaBrowser.Plugins.DefaultTheme\MediaBrowser.Plugins.DefaultTheme.csproj", "{6E892999-711D-4E24-8BAC-DACF5BFA783A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|Any CPU.Build.0 = Release|Any CPU
- {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU
{B5ECE1FB-618E-420B-9A99-8E972D76920A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5ECE1FB-618E-420B-9A99-8E972D76920A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Debug|x86.ActiveCfg = Debug|Any CPU
{B5ECE1FB-618E-420B-9A99-8E972D76920A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5ECE1FB-618E-420B-9A99-8E972D76920A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Release|x86.ActiveCfg = Release|Any CPU
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|x86.ActiveCfg = Debug|Any CPU
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.ActiveCfg = Release|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.ActiveCfg = Debug|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.ActiveCfg = Release|Any CPU
{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Debug|x86.ActiveCfg = Debug|Any CPU
{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Release|Any CPU.ActiveCfg = Release|Any CPU
{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Release|Any CPU.Build.0 = Release|Any CPU
+ {921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Release|x86.ActiveCfg = Release|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|x86.ActiveCfg = Release|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MediaBrowser.UI/App.config b/MediaBrowser.UI/App.config
new file mode 100644
index 0000000000..018d3790f9
--- /dev/null
+++ b/MediaBrowser.UI/App.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/App.xaml b/MediaBrowser.UI/App.xaml
new file mode 100644
index 0000000000..75318985ce
--- /dev/null
+++ b/MediaBrowser.UI/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/App.xaml.cs b/MediaBrowser.UI/App.xaml.cs
new file mode 100644
index 0000000000..6f2afa91cc
--- /dev/null
+++ b/MediaBrowser.UI/App.xaml.cs
@@ -0,0 +1,213 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.UI;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Weather;
+using MediaBrowser.UI.Controller;
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+
+namespace MediaBrowser.UI
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : BaseApplication, IApplication
+ {
+ private Timer ClockTimer { get; set; }
+ private Timer ServerConfigurationTimer { get; set; }
+
+ public static App Instance
+ {
+ get
+ {
+ return Application.Current as App;
+ }
+ }
+
+ public DtoUser CurrentUser
+ {
+ get
+ {
+ return UIKernel.Instance.CurrentUser;
+ }
+ set
+ {
+ UIKernel.Instance.CurrentUser = value;
+ OnPropertyChanged("CurrentUser");
+ }
+ }
+
+ public ServerConfiguration ServerConfiguration
+ {
+ get
+ {
+ return UIKernel.Instance.ServerConfiguration;
+ }
+ set
+ {
+ UIKernel.Instance.ServerConfiguration = value;
+ OnPropertyChanged("ServerConfiguration");
+ }
+ }
+
+ private DateTime _currentTime = DateTime.Now;
+ public DateTime CurrentTime
+ {
+ get
+ {
+ return _currentTime;
+ }
+ private set
+ {
+ _currentTime = value;
+ OnPropertyChanged("CurrentTime");
+ }
+ }
+
+ private WeatherInfo _currentWeather;
+ public WeatherInfo CurrentWeather
+ {
+ get
+ {
+ return _currentWeather;
+ }
+ private set
+ {
+ _currentWeather = value;
+ OnPropertyChanged("CurrentWeather");
+ }
+ }
+
+ private BaseTheme _currentTheme;
+ public BaseTheme CurrentTheme
+ {
+ get
+ {
+ return _currentTheme;
+ }
+ private set
+ {
+ _currentTheme = value;
+ OnPropertyChanged("CurrentTheme");
+ }
+ }
+
+ [STAThread]
+ public static void Main()
+ {
+ RunApplication("MediaBrowserUI");
+ }
+
+ #region BaseApplication Overrides
+ protected override IKernel InstantiateKernel()
+ {
+ return new UIKernel();
+ }
+
+ protected override Window InstantiateMainWindow()
+ {
+ return new MainWindow();
+ }
+
+ protected override void OnKernelLoaded()
+ {
+ base.OnKernelLoaded();
+
+ PropertyChanged += AppPropertyChanged;
+
+ // Update every 10 seconds
+ ClockTimer = new Timer(ClockTimerCallback, null, 0, 10000);
+
+ // Update every 30 minutes
+ ServerConfigurationTimer = new Timer(ServerConfigurationTimerCallback, null, 0, 1800000);
+
+ CurrentTheme = UIKernel.Instance.Plugins.OfType().First();
+
+ foreach (var resource in CurrentTheme.GlobalResources)
+ {
+ Resources.MergedDictionaries.Add(resource);
+ }
+ }
+ #endregion
+
+ async void AppPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName.Equals("ServerConfiguration"))
+ {
+ if (string.IsNullOrEmpty(ServerConfiguration.WeatherZipCode))
+ {
+ CurrentWeather = null;
+ }
+ else
+ {
+ CurrentWeather = await UIKernel.Instance.ApiClient.GetWeatherInfoAsync(ServerConfiguration.WeatherZipCode);
+ }
+ }
+ }
+
+ private void ClockTimerCallback(object stateInfo)
+ {
+ CurrentTime = DateTime.Now;
+ }
+
+ private async void ServerConfigurationTimerCallback(object stateInfo)
+ {
+ ServerConfiguration = await UIKernel.Instance.ApiClient.GetServerConfigurationAsync();
+ }
+
+ public async Task GetImage(string url)
+ {
+ var image = new Image();
+
+ image.Source = await GetBitmapImage(url);
+
+ return image;
+ }
+
+ public async Task GetBitmapImage(string url)
+ {
+ Stream stream = await UIKernel.Instance.ApiClient.GetImageStreamAsync(url);
+
+ BitmapImage bitmap = new BitmapImage();
+
+ bitmap.CacheOption = BitmapCacheOption.Default;
+
+ bitmap.BeginInit();
+ bitmap.StreamSource = stream;
+ bitmap.EndInit();
+
+ return bitmap;
+ }
+
+ public async Task LogoutUser()
+ {
+ CurrentUser = null;
+
+ if (ServerConfiguration.EnableUserProfiles)
+ {
+ Navigate(CurrentTheme.LoginPageUri);
+ }
+ else
+ {
+ DtoUser defaultUser = await UIKernel.Instance.ApiClient.GetDefaultUserAsync();
+ CurrentUser = defaultUser;
+
+ Navigate(new Uri("/Pages/HomePage.xaml", UriKind.Relative));
+ }
+ }
+
+ public void Navigate(Uri uri)
+ {
+ (MainWindow as MainWindow).Navigate(uri);
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs b/MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs
new file mode 100644
index 0000000000..59c6251786
--- /dev/null
+++ b/MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.UI.Configuration
+{
+ ///
+ /// This is the UI's device configuration that applies regardless of which user is logged in.
+ ///
+ public class UIApplicationConfiguration : BaseApplicationConfiguration
+ {
+ ///
+ /// Gets or sets the server host name (myserver or 192.168.x.x)
+ ///
+ public string ServerHostName { get; set; }
+
+ ///
+ /// Gets or sets the port number used by the API
+ ///
+ public int ServerApiPort { get; set; }
+
+ public UIApplicationConfiguration()
+ : base()
+ {
+ ServerHostName = "localhost";
+ ServerApiPort = 8096;
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Configuration/UIApplicationPaths.cs b/MediaBrowser.UI/Configuration/UIApplicationPaths.cs
new file mode 100644
index 0000000000..07cb54fc1b
--- /dev/null
+++ b/MediaBrowser.UI/Configuration/UIApplicationPaths.cs
@@ -0,0 +1,8 @@
+using MediaBrowser.Common.Kernel;
+
+namespace MediaBrowser.UI.Configuration
+{
+ public class UIApplicationPaths : BaseApplicationPaths
+ {
+ }
+}
diff --git a/MediaBrowser.UI/Controller/PluginUpdater.cs b/MediaBrowser.UI/Controller/PluginUpdater.cs
new file mode 100644
index 0000000000..d9fa48749a
--- /dev/null
+++ b/MediaBrowser.UI/Controller/PluginUpdater.cs
@@ -0,0 +1,231 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.ComponentModel.Composition.Hosting;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.UI.Controller
+{
+ ///
+ /// This keeps ui plugin assemblies in sync with plugins installed on the server
+ ///
+ public class PluginUpdater
+ {
+ ///
+ /// Gets the list of currently installed UI plugins
+ ///
+ [ImportMany(typeof(BasePlugin))]
+ private IEnumerable CurrentPlugins { get; set; }
+
+ private CompositionContainer CompositionContainer { get; set; }
+
+ public async Task UpdatePlugins()
+ {
+ // First load the plugins that are currently installed
+ ReloadComposableParts();
+
+ Logger.LogInfo("Downloading list of installed plugins");
+ PluginInfo[] allInstalledPlugins = await UIKernel.Instance.ApiClient.GetInstalledPluginsAsync().ConfigureAwait(false);
+
+ IEnumerable uiPlugins = allInstalledPlugins.Where(p => p.DownloadToUI);
+
+ PluginUpdateResult result = new PluginUpdateResult();
+
+ result.DeletedPlugins = DeleteUninstalledPlugins(uiPlugins);
+
+ await DownloadPluginAssemblies(uiPlugins, result).ConfigureAwait(false);
+
+ // If any new assemblies were downloaded we'll have to reload the CurrentPlugins list
+ if (result.NewlyInstalledPlugins.Any())
+ {
+ ReloadComposableParts();
+ }
+
+ result.UpdatedConfigurations = await DownloadPluginConfigurations(uiPlugins).ConfigureAwait(false);
+
+ CompositionContainer.Dispose();
+
+ return result;
+ }
+
+ ///
+ /// Downloads plugin assemblies from the server, if they need to be installed or updated.
+ ///
+ private async Task DownloadPluginAssemblies(IEnumerable uiPlugins, PluginUpdateResult result)
+ {
+ List newlyInstalledPlugins = new List();
+ List updatedPlugins = new List();
+
+ // Loop through the list of plugins that are on the server
+ foreach (PluginInfo pluginInfo in uiPlugins)
+ {
+ // See if it is already installed in the UI
+ BasePlugin installedPlugin = CurrentPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(pluginInfo.AssemblyFileName, StringComparison.OrdinalIgnoreCase));
+
+ // Download the plugin if it is not present, or if the current version is out of date
+ bool downloadPlugin = installedPlugin == null;
+
+ if (installedPlugin != null)
+ {
+ Version serverVersion = Version.Parse(pluginInfo.Version);
+
+ downloadPlugin = serverVersion > installedPlugin.Version;
+ }
+
+ if (downloadPlugin)
+ {
+ await DownloadPlugin(pluginInfo).ConfigureAwait(false);
+
+ if (installedPlugin == null)
+ {
+ newlyInstalledPlugins.Add(pluginInfo);
+ }
+ else
+ {
+ updatedPlugins.Add(pluginInfo);
+ }
+ }
+ }
+
+ result.NewlyInstalledPlugins = newlyInstalledPlugins;
+ result.UpdatedPlugins = updatedPlugins;
+ }
+
+ ///
+ /// Downloads plugin configurations from the server.
+ ///
+ private async Task> DownloadPluginConfigurations(IEnumerable uiPlugins)
+ {
+ List updatedPlugins = new List();
+
+ // Loop through the list of plugins that are on the server
+ foreach (PluginInfo pluginInfo in uiPlugins)
+ {
+ // See if it is already installed in the UI
+ BasePlugin installedPlugin = CurrentPlugins.First(p => p.AssemblyFileName.Equals(pluginInfo.AssemblyFileName, StringComparison.OrdinalIgnoreCase));
+
+ if (installedPlugin.ConfigurationDateLastModified < pluginInfo.ConfigurationDateLastModified)
+ {
+ await DownloadPluginConfiguration(installedPlugin, pluginInfo).ConfigureAwait(false);
+
+ updatedPlugins.Add(pluginInfo);
+ }
+ }
+
+ return updatedPlugins;
+ }
+
+ ///
+ /// Downloads a plugin assembly from the server
+ ///
+ private async Task DownloadPlugin(PluginInfo plugin)
+ {
+ Logger.LogInfo("Downloading {0} Plugin", plugin.Name);
+
+ string path = Path.Combine(UIKernel.Instance.ApplicationPaths.PluginsPath, plugin.AssemblyFileName);
+
+ // First download to a MemoryStream. This way if the download is cut off, we won't be left with a partial file
+ using (MemoryStream memoryStream = new MemoryStream())
+ {
+ Stream assemblyStream = await UIKernel.Instance.ApiClient.GetPluginAssemblyAsync(plugin).ConfigureAwait(false);
+
+ await assemblyStream.CopyToAsync(memoryStream).ConfigureAwait(false);
+
+ memoryStream.Position = 0;
+
+ using (FileStream fileStream = new FileStream(path, FileMode.Create))
+ {
+ await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+ }
+ }
+
+ ///
+ /// Downloads the latest configuration for a plugin
+ ///
+ private async Task DownloadPluginConfiguration(BasePlugin plugin, PluginInfo pluginInfo)
+ {
+ Logger.LogInfo("Downloading {0} Configuration", plugin.Name);
+
+ object config = await UIKernel.Instance.ApiClient.GetPluginConfigurationAsync(pluginInfo, plugin.ConfigurationType).ConfigureAwait(false);
+
+ XmlSerializer.SerializeToFile(config, plugin.ConfigurationFilePath);
+
+ File.SetLastWriteTimeUtc(plugin.ConfigurationFilePath, pluginInfo.ConfigurationDateLastModified);
+ }
+
+ ///
+ /// Deletes any plugins that have been uninstalled from the server
+ ///
+ private IEnumerable DeleteUninstalledPlugins(IEnumerable uiPlugins)
+ {
+ var deletedPlugins = new List();
+
+ foreach (BasePlugin plugin in CurrentPlugins)
+ {
+ PluginInfo latest = uiPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(plugin.AssemblyFileName, StringComparison.OrdinalIgnoreCase));
+
+ if (latest == null)
+ {
+ DeletePlugin(plugin);
+
+ deletedPlugins.Add(plugin.Name);
+ }
+ }
+
+ return deletedPlugins;
+ }
+
+ ///
+ /// Deletes an installed ui plugin.
+ /// Leaves config and data behind in the event it is later re-installed
+ ///
+ private void DeletePlugin(BasePlugin plugin)
+ {
+ Logger.LogInfo("Deleting {0} Plugin", plugin.Name);
+
+ string path = plugin.AssemblyFilePath;
+
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
+
+ ///
+ /// Re-uses MEF within the kernel to discover installed plugins
+ ///
+ private void ReloadComposableParts()
+ {
+ if (CompositionContainer != null)
+ {
+ CompositionContainer.Dispose();
+ }
+
+ CompositionContainer = UIKernel.Instance.GetCompositionContainer();
+
+ CompositionContainer.ComposeParts(this);
+
+ CompositionContainer.Catalog.Dispose();
+
+ foreach (BasePlugin plugin in CurrentPlugins)
+ {
+ plugin.Initialize(UIKernel.Instance, false);
+ }
+ }
+ }
+
+ public class PluginUpdateResult
+ {
+ public IEnumerable DeletedPlugins { get; set; }
+ public IEnumerable NewlyInstalledPlugins { get; set; }
+ public IEnumerable UpdatedPlugins { get; set; }
+ public IEnumerable UpdatedConfigurations { get; set; }
+ }
+}
diff --git a/MediaBrowser.UI/Controller/UIKernel.cs b/MediaBrowser.UI/Controller/UIKernel.cs
new file mode 100644
index 0000000000..ca24b7852a
--- /dev/null
+++ b/MediaBrowser.UI/Controller/UIKernel.cs
@@ -0,0 +1,97 @@
+using MediaBrowser.ApiInteraction;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Progress;
+using MediaBrowser.UI.Configuration;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.UI.Controller
+{
+ ///
+ /// This controls application logic as well as server interaction within the UI.
+ ///
+ public class UIKernel : BaseKernel
+ {
+ public static UIKernel Instance { get; private set; }
+
+ public ApiClient ApiClient { get; private set; }
+ public DtoUser CurrentUser { get; set; }
+ public ServerConfiguration ServerConfiguration { get; set; }
+
+ public UIKernel()
+ : base()
+ {
+ Instance = this;
+ }
+
+ public override KernelContext KernelContext
+ {
+ get { return KernelContext.Ui; }
+ }
+
+ ///
+ /// Give the UI a different url prefix so that they can share the same port, in case they are installed on the same machine.
+ ///
+ protected override string HttpServerUrlPrefix
+ {
+ get
+ {
+ return "http://+:" + Configuration.HttpServerPortNumber + "/mediabrowser/ui/";
+ }
+ }
+
+ ///
+ /// Performs initializations that can be reloaded at anytime
+ ///
+ protected override async Task ReloadInternal(IProgress progress)
+ {
+ ReloadApiClient();
+
+ await new PluginUpdater().UpdatePlugins().ConfigureAwait(false);
+
+ await base.ReloadInternal(progress).ConfigureAwait(false);
+ }
+
+ ///
+ /// Updates and installs new plugin assemblies and configurations from the server
+ ///
+ protected async Task UpdatePlugins()
+ {
+ return await new PluginUpdater().UpdatePlugins().ConfigureAwait(false);
+ }
+
+ ///
+ /// Disposes the current ApiClient and creates a new one
+ ///
+ private void ReloadApiClient()
+ {
+ DisposeApiClient();
+
+ ApiClient = new ApiClient
+ {
+ ServerHostName = Configuration.ServerHostName,
+ ServerApiPort = Configuration.ServerApiPort
+ };
+ }
+
+ ///
+ /// Disposes the current ApiClient
+ ///
+ private void DisposeApiClient()
+ {
+ if (ApiClient != null)
+ {
+ ApiClient.Dispose();
+ }
+ }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ DisposeApiClient();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Controls/EnhancedScrollViewer.cs b/MediaBrowser.UI/Controls/EnhancedScrollViewer.cs
new file mode 100644
index 0000000000..188715e1e5
--- /dev/null
+++ b/MediaBrowser.UI/Controls/EnhancedScrollViewer.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace MediaBrowser.UI.Controls
+{
+ ///
+ /// Provides a ScrollViewer that can be scrolled by dragging the mouse
+ ///
+ public class EnhancedScrollViewer : ScrollViewer
+ {
+ private Point _scrollTarget;
+ private Point _scrollStartPoint;
+ private Point _scrollStartOffset;
+ private const int PixelsToMoveToBeConsideredScroll = 5;
+
+ protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
+ {
+ if (IsMouseOver)
+ {
+ // Save starting point, used later when determining how much to scroll.
+ _scrollStartPoint = e.GetPosition(this);
+ _scrollStartOffset.X = HorizontalOffset;
+ _scrollStartOffset.Y = VerticalOffset;
+
+ // Update the cursor if can scroll or not.
+ Cursor = (ExtentWidth > ViewportWidth) ||
+ (ExtentHeight > ViewportHeight) ?
+ Cursors.ScrollAll : Cursors.Arrow;
+
+ CaptureMouse();
+ }
+
+ base.OnPreviewMouseDown(e);
+ }
+
+ protected override void OnPreviewMouseMove(MouseEventArgs e)
+ {
+ if (IsMouseCaptured)
+ {
+ Point currentPoint = e.GetPosition(this);
+
+ // Determine the new amount to scroll.
+ var delta = new Point(_scrollStartPoint.X - currentPoint.X, _scrollStartPoint.Y - currentPoint.Y);
+
+ if (Math.Abs(delta.X) < PixelsToMoveToBeConsideredScroll &&
+ Math.Abs(delta.Y) < PixelsToMoveToBeConsideredScroll)
+ return;
+
+ _scrollTarget.X = _scrollStartOffset.X + delta.X;
+ _scrollTarget.Y = _scrollStartOffset.Y + delta.Y;
+
+ // Scroll to the new position.
+ ScrollToHorizontalOffset(_scrollTarget.X);
+ ScrollToVerticalOffset(_scrollTarget.Y);
+ }
+
+ base.OnPreviewMouseMove(e);
+ }
+
+ protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
+ {
+ if (IsMouseCaptured)
+ {
+ Cursor = Cursors.Arrow;
+ ReleaseMouseCapture();
+ }
+
+ base.OnPreviewMouseUp(e);
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Controls/ExtendedImage.cs b/MediaBrowser.UI/Controls/ExtendedImage.cs
new file mode 100644
index 0000000000..9d6ee3a7ae
--- /dev/null
+++ b/MediaBrowser.UI/Controls/ExtendedImage.cs
@@ -0,0 +1,92 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace MediaBrowser.UI.Controls
+{
+ ///
+ /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
+ ///
+ /// Step 1a) Using this custom control in a XAML file that exists in the current project.
+ /// Add this XmlNamespace attribute to the root element of the markup file where it is
+ /// to be used:
+ ///
+ /// xmlns:MyNamespace="clr-namespace:MediaBrowser.UI.Controls"
+ ///
+ ///
+ /// Step 1b) Using this custom control in a XAML file that exists in a different project.
+ /// Add this XmlNamespace attribute to the root element of the markup file where it is
+ /// to be used:
+ ///
+ /// xmlns:MyNamespace="clr-namespace:MediaBrowser.UI.Controls;assembly=MediaBrowser.UI.Controls"
+ ///
+ /// You will also need to add a project reference from the project where the XAML file lives
+ /// to this project and Rebuild to avoid compilation errors:
+ ///
+ /// Right click on the target project in the Solution Explorer and
+ /// "Add Reference"->"Projects"->[Browse to and select this project]
+ ///
+ ///
+ /// Step 2)
+ /// Go ahead and use your control in the XAML file.
+ ///
+ ///
+ ///
+ ///
+ public class ExtendedImage : Control
+ {
+ public static readonly DependencyProperty HasImageProperty = DependencyProperty.Register(
+ "HasImage",
+ typeof (bool),
+ typeof (ExtendedImage),
+ new PropertyMetadata(default(bool)));
+
+ public bool HasImage
+ {
+ get { return (bool)GetValue(HasImageProperty); }
+ set { SetValue(HasImageProperty, value); }
+ }
+
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
+ "Source",
+ typeof(ImageSource),
+ typeof(ExtendedImage),
+ new PropertyMetadata(default(ImageBrush)));
+
+ public ImageSource Source
+ {
+ get { return (ImageSource)GetValue(SourceProperty); }
+ set { SetValue(SourceProperty, value); }
+ }
+
+ public static readonly DependencyProperty StretchProperty = DependencyProperty.Register(
+ "Stretch",
+ typeof (Stretch),
+ typeof (ExtendedImage),
+ new PropertyMetadata(default(Stretch)));
+
+ public Stretch Stretch
+ {
+ get { return (Stretch) GetValue(StretchProperty); }
+ set { SetValue(StretchProperty, value); }
+ }
+
+ public static readonly DependencyProperty PlaceHolderSourceProperty = DependencyProperty.Register(
+ "PlaceHolderSource",
+ typeof(ImageSource),
+ typeof(ExtendedImage),
+ new PropertyMetadata(default(ImageBrush)));
+
+ public ImageSource PlaceHolderSource
+ {
+ get { return (ImageSource)GetValue(PlaceHolderSourceProperty); }
+ set { SetValue(PlaceHolderSourceProperty, value); }
+ }
+
+ static ExtendedImage()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedImage),
+ new FrameworkPropertyMetadata(typeof(ExtendedImage)));
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Controls/TreeHelper.cs b/MediaBrowser.UI/Controls/TreeHelper.cs
new file mode 100644
index 0000000000..bbe4895727
--- /dev/null
+++ b/MediaBrowser.UI/Controls/TreeHelper.cs
@@ -0,0 +1,226 @@
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Media;
+
+namespace MediaBrowser.UI.Controls
+{
+ ///
+ /// Helper methods for UI-related tasks.
+ ///
+ public static class TreeHelper
+ {
+ ///
+ /// Finds a Child of a given item in the visual tree.
+ ///
+ /// A direct parent of the queried item.
+ /// The type of the queried item.
+ /// x:Name or Name of child.
+ /// The first parent item that matches the submitted type parameter.
+ /// If not matching item can be found,
+ /// a null parent is being returned.
+ public static T FindChild(DependencyObject parent, string childName)
+ where T : DependencyObject
+ {
+ // Confirm parent and childName are valid.
+ if (parent == null) return null;
+
+ T foundChild = null;
+
+ int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < childrenCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(parent, i);
+ // If the child is not of the request child type child
+ T childType = child as T;
+ if (childType == null)
+ {
+ // recursively drill down the tree
+ foundChild = FindChild(child, childName);
+
+ // If the child is found, break so we do not overwrite the found child.
+ if (foundChild != null) break;
+ }
+ else if (!string.IsNullOrEmpty(childName))
+ {
+ var frameworkElement = child as FrameworkElement;
+ // If the child's name is set for search
+ if (frameworkElement != null && frameworkElement.Name == childName)
+ {
+ // if the child's name is of the request name
+ foundChild = (T)child;
+ break;
+ }
+ }
+ else
+ {
+ // child element found.
+ foundChild = (T)child;
+ break;
+ }
+ }
+
+ return foundChild;
+ }
+
+ #region find parent
+
+ ///
+ /// Finds a parent of a given item on the visual tree.
+ ///
+ /// The type of the queried item.
+ /// A direct or indirect child of the
+ /// queried item.
+ /// The first parent item that matches the submitted
+ /// type parameter. If not matching item can be found, a null
+ /// reference is being returned.
+ public static T TryFindParent(this DependencyObject child)
+ where T : DependencyObject
+ {
+ //get parent item
+ DependencyObject parentObject = GetParentObject(child);
+
+ //we've reached the end of the tree
+ if (parentObject == null) return null;
+
+ //check if the parent matches the type we're looking for
+ T parent = parentObject as T;
+ if (parent != null)
+ {
+ return parent;
+ }
+
+ //use recursion to proceed with next level
+ return TryFindParent(parentObject);
+ }
+
+ ///
+ /// This method is an alternative to WPF's
+ /// method, which also
+ /// supports content elements. Keep in mind that for content element,
+ /// this method falls back to the logical tree of the element!
+ ///
+ /// The item to be processed.
+ /// The submitted item's parent, if available. Otherwise
+ /// null.
+ public static DependencyObject GetParentObject(this DependencyObject child)
+ {
+ if (child == null) return null;
+
+ //handle content elements separately
+ ContentElement contentElement = child as ContentElement;
+ if (contentElement != null)
+ {
+ DependencyObject parent = ContentOperations.GetParent(contentElement);
+ if (parent != null) return parent;
+
+ FrameworkContentElement fce = contentElement as FrameworkContentElement;
+ return fce != null ? fce.Parent : null;
+ }
+
+ //also try searching for parent in framework elements (such as DockPanel, etc)
+ FrameworkElement frameworkElement = child as FrameworkElement;
+ if (frameworkElement != null)
+ {
+ DependencyObject parent = frameworkElement.Parent;
+ if (parent != null) return parent;
+ }
+
+ //if it's not a ContentElement/FrameworkElement, rely on VisualTreeHelper
+ return VisualTreeHelper.GetParent(child);
+ }
+
+ #endregion
+
+ #region find children
+
+ ///
+ /// Analyzes both visual and logical tree in order to find all elements of a given
+ /// type that are descendants of the item.
+ ///
+ /// The type of the queried items.
+ /// The root element that marks the source of the search. If the
+ /// source is already of the requested type, it will not be included in the result.
+ /// All descendants of that match the requested type.
+ public static IEnumerable FindChildren(this DependencyObject source) where T : DependencyObject
+ {
+ if (source != null)
+ {
+ var childs = GetChildObjects(source);
+ foreach (DependencyObject child in childs)
+ {
+ //analyze if children match the requested type
+ if (child is T)
+ {
+ yield return (T)child;
+ }
+
+ //recurse tree
+ foreach (T descendant in FindChildren(child))
+ {
+ yield return descendant;
+ }
+ }
+ }
+ }
+
+
+ ///
+ /// This method is an alternative to WPF's
+ /// method, which also
+ /// supports content elements. Keep in mind that for content elements,
+ /// this method falls back to the logical tree of the element.
+ ///
+ /// The item to be processed.
+ /// The submitted item's child elements, if available.
+ public static IEnumerable GetChildObjects(this DependencyObject parent)
+ {
+ if (parent == null) yield break;
+
+ if (parent is ContentElement || parent is FrameworkElement)
+ {
+ //use the logical tree for content / framework elements
+ foreach (object obj in LogicalTreeHelper.GetChildren(parent))
+ {
+ var depObj = obj as DependencyObject;
+ if (depObj != null) yield return (DependencyObject)obj;
+ }
+ }
+ else
+ {
+ //use the visual tree per default
+ int count = VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < count; i++)
+ {
+ yield return VisualTreeHelper.GetChild(parent, i);
+ }
+ }
+ }
+
+ #endregion
+
+ #region find from point
+
+ ///
+ /// Tries to locate a given item within the visual tree,
+ /// starting with the dependency object at a given position.
+ ///
+ /// The type of the element to be found
+ /// on the visual tree of the element at the given location.
+ /// The main element which is used to perform
+ /// hit testing.
+ /// The position to be evaluated on the origin.
+ public static T TryFindFromPoint(UIElement reference, Point point)
+ where T : DependencyObject
+ {
+ DependencyObject element = reference.InputHitTest(point) as DependencyObject;
+
+ if (element == null) return null;
+
+ if (element is T) return (T)element;
+
+ return TryFindParent(element);
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.UI/Controls/WindowCommands.xaml b/MediaBrowser.UI/Controls/WindowCommands.xaml
new file mode 100644
index 0000000000..9209549185
--- /dev/null
+++ b/MediaBrowser.UI/Controls/WindowCommands.xaml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MediaBrowser.UI/Controls/WindowCommands.xaml.cs b/MediaBrowser.UI/Controls/WindowCommands.xaml.cs
new file mode 100644
index 0000000000..1810c5bf3f
--- /dev/null
+++ b/MediaBrowser.UI/Controls/WindowCommands.xaml.cs
@@ -0,0 +1,50 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace MediaBrowser.UI.Controls
+{
+ ///
+ /// Interaction logic for WindowCommands.xaml
+ ///
+ public partial class WindowCommands : UserControl
+ {
+ public Window ParentWindow
+ {
+ get { return TreeHelper.TryFindParent(this); }
+ }
+
+ public WindowCommands()
+ {
+ InitializeComponent();
+ Loaded += WindowCommandsLoaded;
+ }
+
+ void WindowCommandsLoaded(object sender, RoutedEventArgs e)
+ {
+ CloseApplicationButton.Click += CloseApplicationButtonClick;
+ MinimizeApplicationButton.Click += MinimizeApplicationButtonClick;
+ MaximizeApplicationButton.Click += MaximizeApplicationButtonClick;
+ UndoMaximizeApplicationButton.Click += UndoMaximizeApplicationButtonClick;
+ }
+
+ void UndoMaximizeApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.WindowState = WindowState.Normal;
+ }
+
+ void MaximizeApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.WindowState = WindowState.Maximized;
+ }
+
+ void MinimizeApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.WindowState = WindowState.Minimized;
+ }
+
+ void CloseApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.Close();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs b/MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs
new file mode 100644
index 0000000000..a5dd5013b4
--- /dev/null
+++ b/MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class CurrentUserVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (App.Instance.ServerConfiguration == null || !App.Instance.ServerConfiguration.EnableUserProfiles)
+ {
+ return Visibility.Collapsed;
+ }
+
+ return value == null ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/DateTimeToStringConverter.cs b/MediaBrowser.UI/Converters/DateTimeToStringConverter.cs
new file mode 100644
index 0000000000..6c568c0612
--- /dev/null
+++ b/MediaBrowser.UI/Converters/DateTimeToStringConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class DateTimeToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var date = (DateTime)value;
+
+ string format = parameter as string;
+
+ if (string.IsNullOrEmpty(format))
+ {
+ return date.ToString();
+ }
+
+ if (format.Equals("shorttime", StringComparison.OrdinalIgnoreCase))
+ {
+ return date.ToShortTimeString();
+ }
+
+ return date.ToString(format);
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/LastSeenTextConverter.cs b/MediaBrowser.UI/Converters/LastSeenTextConverter.cs
new file mode 100644
index 0000000000..7462602100
--- /dev/null
+++ b/MediaBrowser.UI/Converters/LastSeenTextConverter.cs
@@ -0,0 +1,86 @@
+using MediaBrowser.Model.DTO;
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class LastSeenTextConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var user = value as DtoUser;
+
+ if (user != null)
+ {
+ if (user.LastActivityDate.HasValue)
+ {
+ DateTime date = user.LastActivityDate.Value.ToLocalTime();
+
+ return "Last seen " + GetRelativeTimeText(date);
+ }
+ }
+
+ return null;
+ }
+
+ private static string GetRelativeTimeText(DateTime date)
+ {
+ TimeSpan ts = DateTime.Now - date;
+
+ const int second = 1;
+ const int minute = 60 * second;
+ const int hour = 60 * minute;
+ const int day = 24 * hour;
+ const int month = 30 * day;
+
+ int delta = System.Convert.ToInt32(ts.TotalSeconds);
+
+ if (delta < 0)
+ {
+ return "not yet";
+ }
+ if (delta < 1 * minute)
+ {
+ return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";
+ }
+ if (delta < 2 * minute)
+ {
+ return "a minute ago";
+ }
+ if (delta < 45 * minute)
+ {
+ return ts.Minutes + " minutes ago";
+ }
+ if (delta < 90 * minute)
+ {
+ return "an hour ago";
+ }
+ if (delta < 24 * hour)
+ {
+ return ts.Hours + " hours ago";
+ }
+ if (delta < 48 * hour)
+ {
+ return "yesterday";
+ }
+ if (delta < 30 * day)
+ {
+ return ts.Days + " days ago";
+ }
+ if (delta < 12 * month)
+ {
+ int months = System.Convert.ToInt32(Math.Floor((double)ts.Days / 30));
+ return months <= 1 ? "one month ago" : months + " months ago";
+ }
+
+ int years = System.Convert.ToInt32(Math.Floor((double)ts.Days / 365));
+ return years <= 1 ? "one year ago" : years + " years ago";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/UserImageConverter.cs b/MediaBrowser.UI/Converters/UserImageConverter.cs
new file mode 100644
index 0000000000..a9ef4b8620
--- /dev/null
+++ b/MediaBrowser.UI/Converters/UserImageConverter.cs
@@ -0,0 +1,60 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.UI.Controller;
+using System;
+using System.Globalization;
+using System.Net.Cache;
+using System.Windows.Data;
+using System.Windows.Media.Imaging;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class UserImageConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var user = value as DtoUser;
+
+ if (user != null && user.HasImage)
+ {
+ var config = parameter as string;
+
+ int? maxWidth = null;
+ int? maxHeight = null;
+ int? width = null;
+ int? height = null;
+
+ if (!string.IsNullOrEmpty(config))
+ {
+ var vals = config.Split(',');
+
+ width = GetSize(vals[0]);
+ height = GetSize(vals[1]);
+ maxWidth = GetSize(vals[2]);
+ maxHeight = GetSize(vals[3]);
+ }
+
+ var uri = UIKernel.Instance.ApiClient.GetUserImageUrl(user.Id, width, height, maxWidth, maxHeight, 100);
+
+ return new BitmapImage(new Uri(uri), new RequestCachePolicy(RequestCacheLevel.Revalidate));
+ }
+
+ return null;
+ }
+
+ private int? GetSize(string val)
+ {
+ if (string.IsNullOrEmpty(val) || val == "0")
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs b/MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs
new file mode 100644
index 0000000000..cab4c595ca
--- /dev/null
+++ b/MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs
@@ -0,0 +1,31 @@
+using MediaBrowser.Model.Weather;
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class WeatherTemperatureConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var weather = value as WeatherInfo;
+
+ if (weather != null)
+ {
+ if (App.Instance.ServerConfiguration.WeatherUnit == WeatherUnits.Celsius)
+ {
+ return weather.CurrentWeather.TemperatureCelsius + "°C";
+ }
+
+ return weather.CurrentWeather.TemperatureFahrenheit + "°F";
+ }
+ return null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs b/MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs
new file mode 100644
index 0000000000..5706ecec9e
--- /dev/null
+++ b/MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class WeatherVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value == null ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/MainWindow.xaml b/MediaBrowser.UI/MainWindow.xaml
new file mode 100644
index 0000000000..b3c36915e8
--- /dev/null
+++ b/MediaBrowser.UI/MainWindow.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MediaBrowser.UI/MainWindow.xaml.cs b/MediaBrowser.UI/MainWindow.xaml.cs
new file mode 100644
index 0000000000..07e8e94331
--- /dev/null
+++ b/MediaBrowser.UI/MainWindow.xaml.cs
@@ -0,0 +1,368 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.UI.Controller;
+using MediaBrowser.UI.Controls;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Imaging;
+
+namespace MediaBrowser.UI
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window, INotifyPropertyChanged
+ {
+ private Timer MouseIdleTimer { get; set; }
+ private Timer BackdropTimer { get; set; }
+ private Image BackdropImage { get; set; }
+ private string[] CurrentBackdrops { get; set; }
+ private int CurrentBackdropIndex { get; set; }
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ BackButton.Click += BtnApplicationBackClick;
+ ExitButton.Click += ExitButtonClick;
+ ForwardButton.Click += ForwardButtonClick;
+ DragBar.MouseDown += DragableGridMouseDown;
+ Loaded += MainWindowLoaded;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(String info)
+ {
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs(info));
+ }
+ }
+
+ private bool _isMouseIdle = true;
+ public bool IsMouseIdle
+ {
+ get { return _isMouseIdle; }
+ set
+ {
+ _isMouseIdle = value;
+ OnPropertyChanged("IsMouseIdle");
+ }
+ }
+
+ void MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ DataContext = App.Instance;
+
+ if (App.Instance.ServerConfiguration == null)
+ {
+ App.Instance.PropertyChanged += ApplicationPropertyChanged;
+ }
+ else
+ {
+ LoadInitialPage();
+ }
+ }
+
+ void ForwardButtonClick(object sender, RoutedEventArgs e)
+ {
+ NavigateForward();
+ }
+
+ void ExitButtonClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ void ApplicationPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName.Equals("ServerConfiguration"))
+ {
+ App.Instance.PropertyChanged -= ApplicationPropertyChanged;
+ LoadInitialPage();
+ }
+ }
+
+ private async void LoadInitialPage()
+ {
+ await App.Instance.LogoutUser().ConfigureAwait(false);
+ }
+
+ private void DragableGridMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ClickCount == 2)
+ {
+ WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ }
+ else if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ DragMove();
+ }
+ }
+
+ void BtnApplicationBackClick(object sender, RoutedEventArgs e)
+ {
+ NavigateBack();
+ }
+
+ private Frame PageFrame
+ {
+ get
+ {
+ // Finding the grid that is generated by the ControlTemplate of the Button
+ return TreeHelper.FindChild(PageContent, "PageFrame");
+ }
+ }
+
+ public void Navigate(Uri uri)
+ {
+ PageFrame.Navigate(uri);
+ }
+
+ ///
+ /// Sets the backdrop based on an ApiBaseItemWrapper
+ ///
+ public void SetBackdrops(DtoBaseItem item)
+ {
+ SetBackdrops(UIKernel.Instance.ApiClient.GetBackdropImageUrls(item, null, null, 1920, 1080));
+ }
+
+ ///
+ /// Sets the backdrop based on a list of image files
+ ///
+ public async void SetBackdrops(string[] backdrops)
+ {
+ // Don't reload the same backdrops
+ if (CurrentBackdrops != null && backdrops.SequenceEqual(CurrentBackdrops))
+ {
+ return;
+ }
+
+ if (BackdropTimer != null)
+ {
+ BackdropTimer.Dispose();
+ }
+
+ BackdropGrid.Children.Clear();
+
+ if (backdrops.Length == 0)
+ {
+ CurrentBackdrops = null;
+ return;
+ }
+
+ CurrentBackdropIndex = GetFirstBackdropIndex();
+
+ Image image = await App.Instance.GetImage(backdrops.ElementAt(CurrentBackdropIndex));
+ image.SetResourceReference(Image.StyleProperty, "BackdropImage");
+
+ BackdropGrid.Children.Add(image);
+
+ CurrentBackdrops = backdrops;
+ BackdropImage = image;
+
+ const int backdropRotationTime = 7000;
+
+ if (backdrops.Count() > 1)
+ {
+ BackdropTimer = new Timer(BackdropTimerCallback, null, backdropRotationTime, backdropRotationTime);
+ }
+ }
+
+ public void ClearBackdrops()
+ {
+ if (BackdropTimer != null)
+ {
+ BackdropTimer.Dispose();
+ }
+
+ BackdropGrid.Children.Clear();
+
+ CurrentBackdrops = null;
+ }
+
+ private void BackdropTimerCallback(object stateInfo)
+ {
+ // Need to do this on the UI thread
+ Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ var animFadeOut = new Storyboard();
+ animFadeOut.Completed += AnimFadeOutCompleted;
+
+ var fadeOut = new DoubleAnimation();
+ fadeOut.From = 1.0;
+ fadeOut.To = 0.5;
+ fadeOut.Duration = new Duration(TimeSpan.FromSeconds(1));
+
+ animFadeOut.Children.Add(fadeOut);
+ Storyboard.SetTarget(fadeOut, BackdropImage);
+ Storyboard.SetTargetProperty(fadeOut, new PropertyPath(Image.OpacityProperty));
+
+ animFadeOut.Begin(this);
+ });
+ }
+
+ async void AnimFadeOutCompleted(object sender, System.EventArgs e)
+ {
+ if (CurrentBackdrops == null)
+ {
+ return;
+ }
+
+ int backdropIndex = GetNextBackdropIndex();
+
+ BitmapImage image = await App.Instance.GetBitmapImage(CurrentBackdrops[backdropIndex]);
+ CurrentBackdropIndex = backdropIndex;
+
+ // Need to do this on the UI thread
+ BackdropImage.Source = image;
+ Storyboard imageFadeIn = new Storyboard();
+
+ DoubleAnimation fadeIn = new DoubleAnimation();
+
+ fadeIn.From = 0.25;
+ fadeIn.To = 1.0;
+ fadeIn.Duration = new Duration(TimeSpan.FromSeconds(1));
+
+ imageFadeIn.Children.Add(fadeIn);
+ Storyboard.SetTarget(fadeIn, BackdropImage);
+ Storyboard.SetTargetProperty(fadeIn, new PropertyPath(Image.OpacityProperty));
+ imageFadeIn.Begin(this);
+ }
+
+ private int GetFirstBackdropIndex()
+ {
+ return 0;
+ }
+
+ private int GetNextBackdropIndex()
+ {
+ if (CurrentBackdropIndex < CurrentBackdrops.Length - 1)
+ {
+ return CurrentBackdropIndex + 1;
+ }
+
+ return 0;
+ }
+
+ public void NavigateBack()
+ {
+ if (PageFrame.NavigationService.CanGoBack)
+ {
+ PageFrame.NavigationService.GoBack();
+ }
+ }
+
+ public void NavigateForward()
+ {
+ if (PageFrame.NavigationService.CanGoForward)
+ {
+ PageFrame.NavigationService.GoForward();
+ }
+ }
+
+ ///
+ /// Shows the control bar then starts a timer to hide it
+ ///
+ private void StartMouseIdleTimer()
+ {
+ IsMouseIdle = false;
+
+ const int duration = 10000;
+
+ // Start the timer if it's null, otherwise reset it
+ if (MouseIdleTimer == null)
+ {
+ MouseIdleTimer = new Timer(MouseIdleTimerCallback, null, duration, Timeout.Infinite);
+ }
+ else
+ {
+ MouseIdleTimer.Change(duration, Timeout.Infinite);
+ }
+ }
+
+ ///
+ /// This is the Timer callback method to hide the control bar
+ ///
+ private void MouseIdleTimerCallback(object stateInfo)
+ {
+ IsMouseIdle = true;
+
+ if (MouseIdleTimer != null)
+ {
+ MouseIdleTimer.Dispose();
+ MouseIdleTimer = null;
+ }
+ }
+
+ ///
+ /// Handles OnMouseMove to show the control box
+ ///
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ base.OnMouseMove(e);
+
+ StartMouseIdleTimer();
+ }
+
+ ///
+ /// Handles OnKeyUp to provide keyboard based navigation
+ ///
+ protected override void OnKeyUp(KeyEventArgs e)
+ {
+ base.OnKeyUp(e);
+
+ if (IsBackPress(e))
+ {
+ NavigateBack();
+ }
+
+ else if (IsForwardPress(e))
+ {
+ NavigateForward();
+ }
+ }
+
+ ///
+ /// Determines if a keypress should be treated as a backward press
+ ///
+ private bool IsBackPress(KeyEventArgs e)
+ {
+ if (e.Key == Key.BrowserBack || e.Key == Key.Back)
+ {
+ return true;
+ }
+
+ if (e.SystemKey == Key.Left && e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Alt))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines if a keypress should be treated as a forward press
+ ///
+ private bool IsForwardPress(KeyEventArgs e)
+ {
+ if (e.Key == Key.BrowserForward)
+ {
+ return true;
+ }
+
+ if (e.SystemKey == Key.RightAlt && e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Alt))
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.UI/MediaBrowser.UI.csproj b/MediaBrowser.UI/MediaBrowser.UI.csproj
new file mode 100644
index 0000000000..b099d0f837
--- /dev/null
+++ b/MediaBrowser.UI/MediaBrowser.UI.csproj
@@ -0,0 +1,196 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}
+ WinExe
+ Properties
+ MediaBrowser.UI
+ MediaBrowser.UI
+ v4.5
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ MediaBrowser.UI.App
+
+
+ Resources\Images\Icon.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WindowCommands.xaml
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+
+
+ MainWindow.xaml
+ Code
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+ Designer
+
+
+
+
+ {921c0f64-fda7-4e9f-9e73-0cb0eedb2422}
+ MediaBrowser.ApiInteraction
+
+
+ {9142eefa-7570-41e1-bfcc-468bb571af2f}
+ MediaBrowser.Common
+
+
+ {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}
+ MediaBrowser.Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/Pages/BaseLoginPage.cs b/MediaBrowser.UI/Pages/BaseLoginPage.cs
new file mode 100644
index 0000000000..cd3151df03
--- /dev/null
+++ b/MediaBrowser.UI/Pages/BaseLoginPage.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.UI.Controller;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.UI.Pages
+{
+ public class BaseLoginPage : BasePage
+ {
+ private DtoUser[] _users;
+ public DtoUser[] Users
+ {
+ get { return _users; }
+
+ set
+ {
+ _users = value;
+ OnPropertyChanged("Users");
+ }
+ }
+
+ protected override async Task LoadData()
+ {
+ Users = await UIKernel.Instance.ApiClient.GetAllUsersAsync().ConfigureAwait(false);
+ }
+
+ protected void UserClicked(DtoUser user)
+ {
+ App.Instance.CurrentUser = user;
+ //App.Instance.Navigate(new Uri("/Pages/HomePage.xaml", UriKind.Relative));
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Pages/BasePage.cs b/MediaBrowser.UI/Pages/BasePage.cs
new file mode 100644
index 0000000000..800f6e215f
--- /dev/null
+++ b/MediaBrowser.UI/Pages/BasePage.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Web;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace MediaBrowser.UI.Pages
+{
+ public abstract class BasePage : Page, INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(String info)
+ {
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs(info));
+ }
+ }
+
+ protected Uri Uri
+ {
+ get
+ {
+ return NavigationService.CurrentSource;
+ }
+ }
+
+ protected MainWindow MainWindow
+ {
+ get
+ {
+ return App.Instance.MainWindow as MainWindow;
+ }
+ }
+
+ private NameValueCollection _queryString;
+ protected NameValueCollection QueryString
+ {
+ get
+ {
+ if (_queryString == null)
+ {
+ string url = Uri.ToString();
+
+ int index = url.IndexOf('?');
+
+ if (index == -1)
+ {
+ _queryString = new NameValueCollection();
+ }
+ else
+ {
+ _queryString = HttpUtility.ParseQueryString(url.Substring(index + 1));
+ }
+ }
+
+ return _queryString;
+ }
+ }
+
+ protected BasePage()
+ : base()
+ {
+ Loaded += BasePageLoaded;
+ }
+
+ async void BasePageLoaded(object sender, RoutedEventArgs e)
+ {
+ await LoadData();
+
+ DataContext = this;
+ }
+
+ protected abstract Task LoadData();
+ }
+}
diff --git a/MediaBrowser.UI/Properties/AssemblyInfo.cs b/MediaBrowser.UI/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..565b1801e6
--- /dev/null
+++ b/MediaBrowser.UI/Properties/AssemblyInfo.cs
@@ -0,0 +1,53 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.UI")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.UI")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/MediaBrowser.UI/Properties/Resources.Designer.cs b/MediaBrowser.UI/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..b9d7426209
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17626
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.UI.Properties
+{
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MediaBrowser.UI.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Properties/Resources.resx b/MediaBrowser.UI/Properties/Resources.resx
new file mode 100644
index 0000000000..ffecec851a
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/Properties/Settings.Designer.cs b/MediaBrowser.UI/Properties/Settings.Designer.cs
new file mode 100644
index 0000000000..4d9ddf50d1
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17626
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.UI.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Properties/Settings.settings b/MediaBrowser.UI/Properties/Settings.settings
new file mode 100644
index 0000000000..8f2fd95d62
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/Resources/AppResources.xaml b/MediaBrowser.UI/Resources/AppResources.xaml
new file mode 100644
index 0000000000..8d4f36d4fd
--- /dev/null
+++ b/MediaBrowser.UI/Resources/AppResources.xaml
@@ -0,0 +1,122 @@
+
+
+
+ Segoe UI, Lucida Sans Unicode, Verdana
+ Thin
+ Black
+ 36
+ 84
+ 60
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/Resources/Images/BackButton.png b/MediaBrowser.UI/Resources/Images/BackButton.png
new file mode 100644
index 0000000000..263eceadbc
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/BackButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/ExitButton.png b/MediaBrowser.UI/Resources/Images/ExitButton.png
new file mode 100644
index 0000000000..c7d5c0f769
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/ExitButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/ForwardButton.png b/MediaBrowser.UI/Resources/Images/ForwardButton.png
new file mode 100644
index 0000000000..a9548b3095
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/ForwardButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/Icon.ico b/MediaBrowser.UI/Resources/Images/Icon.ico
new file mode 100644
index 0000000000..f8accfab24
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/Icon.ico differ
diff --git a/MediaBrowser.UI/Resources/Images/MuteButton.png b/MediaBrowser.UI/Resources/Images/MuteButton.png
new file mode 100644
index 0000000000..fa454b8f3e
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/MuteButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/SettingsButton.png b/MediaBrowser.UI/Resources/Images/SettingsButton.png
new file mode 100644
index 0000000000..04ca4d32b3
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/SettingsButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/VolumeDownButton.png b/MediaBrowser.UI/Resources/Images/VolumeDownButton.png
new file mode 100644
index 0000000000..c7ff252ce1
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/VolumeDownButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/VolumeUpButton.png b/MediaBrowser.UI/Resources/Images/VolumeUpButton.png
new file mode 100644
index 0000000000..c89d256919
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/VolumeUpButton.png differ
diff --git a/MediaBrowser.UI/Resources/Images/mblogoblack.png b/MediaBrowser.UI/Resources/Images/mblogoblack.png
new file mode 100644
index 0000000000..84323fe525
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/mblogoblack.png differ
diff --git a/MediaBrowser.UI/Resources/Images/mblogowhite.png b/MediaBrowser.UI/Resources/Images/mblogowhite.png
new file mode 100644
index 0000000000..a39812e35c
Binary files /dev/null and b/MediaBrowser.UI/Resources/Images/mblogowhite.png differ
diff --git a/MediaBrowser.UI/Resources/MainWindowResources.xaml b/MediaBrowser.UI/Resources/MainWindowResources.xaml
new file mode 100644
index 0000000000..624e7a6335
--- /dev/null
+++ b/MediaBrowser.UI/Resources/MainWindowResources.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/Resources/NavBarResources.xaml b/MediaBrowser.UI/Resources/NavBarResources.xaml
new file mode 100644
index 0000000000..c2181c16f2
--- /dev/null
+++ b/MediaBrowser.UI/Resources/NavBarResources.xaml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MediaBrowser.UI/Themes/Generic.xaml b/MediaBrowser.UI/Themes/Generic.xaml
new file mode 100644
index 0000000000..c34489b4e1
--- /dev/null
+++ b/MediaBrowser.UI/Themes/Generic.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
index b5b24ce61a..7f1015591b 100644
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
@@ -58,7 +58,7 @@
- xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData\Plugins\" /y
+ xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y