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