diff --git a/VideoBrowser.Test/Common/ByteFormatProviderTest.cs b/VideoBrowser.Test/Common/ByteFormatProviderTest.cs new file mode 100644 index 0000000..2fea351 --- /dev/null +++ b/VideoBrowser.Test/Common/ByteFormatProviderTest.cs @@ -0,0 +1,32 @@ +namespace VideoBrowser.Test.Common +{ + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using VideoBrowser.Common; + + /// + /// Defines the + /// + [TestClass] + public class ByteFormatProviderTest + { + #region Methods + + /// + /// The FileSizeFormatProvider_With_String + /// + [TestMethod] + public void ByteSizeFormatProvider_With_String() + { + var provider = new ByteFormatProvider(); + var size = 1000000.0; + var formatedSize = string.Format(new ByteFormatProvider(), "{0:fs}", size); + formatedSize.Should().Be("976.56 KB"); + var speed = 1000000000.0; + var formatedSpeed = string.Format(new ByteFormatProvider(), "{0:s}", speed); + formatedSpeed.Should().Be("953.67 MB/s"); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Common/ManualTestAttribute.cs b/VideoBrowser.Test/Common/ManualTestAttribute.cs new file mode 100644 index 0000000..a9eaf37 --- /dev/null +++ b/VideoBrowser.Test/Common/ManualTestAttribute.cs @@ -0,0 +1,31 @@ +namespace VideoBrowser.Test.Common +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System.Collections.Generic; + + /// + /// Defines the . + /// + public class ManualTestAttribute : TestCategoryBaseAttribute + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public ManualTestAttribute() + { + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the TestCategories. + /// + public override IList TestCategories => new List { "ManualTest" }; + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Common/PercentageTest.cs b/VideoBrowser.Test/Common/PercentageTest.cs new file mode 100644 index 0000000..98b7278 --- /dev/null +++ b/VideoBrowser.Test/Common/PercentageTest.cs @@ -0,0 +1,28 @@ +namespace VideoBrowser.Test.Common +{ + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using VideoBrowser.Common; + + /// + /// Defines the + /// + [TestClass] + public class PercentageTest + { + #region Methods + + /// + /// The PercentageTest_Constructor + /// + [TestMethod] + public void PercentageTest_Constructor() + { + var percentage = new Percentage(50); + percentage.Normalized.Should().Be(0.5); + percentage.ToString().Should().Be("50%"); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Extensions/StringExtensionTest.cs b/VideoBrowser.Test/Extensions/StringExtensionTest.cs new file mode 100644 index 0000000..e5a2f89 --- /dev/null +++ b/VideoBrowser.Test/Extensions/StringExtensionTest.cs @@ -0,0 +1,42 @@ +namespace VideoBrowser.Test.Extensions +{ + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using VideoBrowser.Extensions; + + /// + /// Defines the + /// + [TestClass] + public class StringExtensionTest + { + #region Methods + + /// + /// The ToFormatedByte + /// + [TestMethod] + public void ToFormatedByte() + { + var formatedByte = StringExtension.ToFormatedByte(10000); + formatedByte.Should().Be("9.77 KB"); + formatedByte = StringExtension.ToFormatedByte(20000000); + formatedByte.Should().Be("19.07 MB"); + formatedByte = StringExtension.ToFormatedByte(2000000000); + formatedByte.Should().Be("1.86 GB"); + } + + [TestMethod] + public void ToFormatedSpeed() + { + var formatedSpeed = StringExtension.ToFormatedSpeed(10000); + formatedSpeed.Should().Be("9.77 KB/s"); + formatedSpeed = StringExtension.ToFormatedSpeed(20000000); + formatedSpeed.Should().Be("19.07 MB/s"); + formatedSpeed = StringExtension.ToFormatedSpeed(2000000000); + formatedSpeed.Should().Be("1.86 GB/s"); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Models/OperationModelTest.cs b/VideoBrowser.Test/Models/OperationModelTest.cs new file mode 100644 index 0000000..64ea0fe --- /dev/null +++ b/VideoBrowser.Test/Models/OperationModelTest.cs @@ -0,0 +1,52 @@ +namespace VideoBrowser.Test.Models +{ + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System.Collections.ObjectModel; + using VideoBrowser.Models; + using VideoBrowser.Test.TestHelpers; + + /// + /// Defines the . + /// + [TestClass] + public class OperationModelTest + { + #region Methods + + /// + /// The Test. + /// + [TestMethod] + public void ContainsTest() + { + var operation1 = new DummyOperation + { + Output = "NewVideo1.mp4" + }; + var operation2 = new DummyOperation + { + Output = "NewVideo2.mp4" + }; + var operation3 = new DummyOperation + { + Output = "NewVideo1.mp4" + }; + + var model1 = new OperationModel(operation1); + var model2 = new OperationModel(operation2); + var model3 = new OperationModel(operation3); + var models = new ObservableCollection + { + model1, + model2 + }; + + models.Contains(model1).Should().BeTrue(); + models.Contains(model2).Should().BeTrue(); + models.Contains(model3).Should().BeTrue(); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Properties/AssemblyInfo.cs b/VideoBrowser.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4b4f306 --- /dev/null +++ b/VideoBrowser.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("VideoBrowser.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VideoBrowser.Test")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("ceb9d499-9f24-42c0-b6ad-165d2abf9b7b")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.1.7.0")] +[assembly: AssemblyFileVersion("0.1.7.0")] \ No newline at end of file diff --git a/VideoBrowser.Test/TestHelpers/DummyOperation.cs b/VideoBrowser.Test/TestHelpers/DummyOperation.cs new file mode 100644 index 0000000..5745c9a --- /dev/null +++ b/VideoBrowser.Test/TestHelpers/DummyOperation.cs @@ -0,0 +1,39 @@ +namespace VideoBrowser.Test.TestHelpers +{ + using System.ComponentModel; + using VideoBrowser.Core; + + /// + /// Defines the . + /// + public class DummyOperation : Operation + { + #region Methods + + /// + /// The WorkerCompleted. + /// + /// The e. + protected override void WorkerCompleted(RunWorkerCompletedEventArgs e) + { + } + + /// + /// The WorkerDoWork. + /// + /// The e. + protected override void WorkerDoWork(DoWorkEventArgs e) + { + } + + /// + /// The WorkerProgressChanged. + /// + /// The e. + protected override void WorkerProgressChanged(ProgressChangedEventArgs e) + { + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/TestHelpers/TestHelper.cs b/VideoBrowser.Test/TestHelpers/TestHelper.cs new file mode 100644 index 0000000..1e503d8 --- /dev/null +++ b/VideoBrowser.Test/TestHelpers/TestHelper.cs @@ -0,0 +1,47 @@ +namespace VideoBrowser.Test.TestHelpers +{ + using System; + + /// + /// Defines the . + /// + internal static class TestHelper + { + #region Properties + + /// + /// Gets the Random. + /// + private static Random Random { get; } = new Random(); + + #endregion Properties + + #region Methods + + /// + /// The GetRandomInt. + /// + /// The start. + /// The end. + /// The . + internal static int GetRandomInt(int start, int end) + { + var random = start + Random.NextDouble() * (end - start); + return (int)random; + } + + /// + /// The GetRandomLong. + /// + /// The start. + /// The end. + /// The . + internal static long GetRandomLong(long start, long end) + { + var random = start + Random.NextDouble() * (end - start); + return (long)random; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/TestHelpers/WindowFactory.cs b/VideoBrowser.Test/TestHelpers/WindowFactory.cs new file mode 100644 index 0000000..9b2dd0f --- /dev/null +++ b/VideoBrowser.Test/TestHelpers/WindowFactory.cs @@ -0,0 +1,24 @@ +namespace VideoBrowser.Test.TestHelpers +{ + using System.Windows; + + /// + /// Defines the . + /// + internal static class WindowFactory + { + #region Methods + + /// + /// The CreateAndShow. + /// + /// The view. + internal static void CreateAndShow(FrameworkElement view) + { + var window = new Window { Content = view, MinWidth = 600, MinHeight = 400, SizeToContent = SizeToContent.WidthAndHeight }; + window.ShowDialog(); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/VideoBrowser.Test.csproj b/VideoBrowser.Test/VideoBrowser.Test.csproj new file mode 100644 index 0000000..3390cd7 --- /dev/null +++ b/VideoBrowser.Test/VideoBrowser.Test.csproj @@ -0,0 +1,111 @@ + + + + + Debug + AnyCPU + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B} + Library + Properties + VideoBrowser.Test + VideoBrowser.Test + v4.8 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + true + + + key.snk + + + + ..\..\..\bin\FluentAssertions.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {2d8225bc-f5b6-4f29-a9e3-f4694d260597} + VideoBrowser + + + + + 2.2.7 + + + 2.2.7 + + + + \ No newline at end of file diff --git a/VideoBrowser.Test/Views/AboutViewTest.cs b/VideoBrowser.Test/Views/AboutViewTest.cs new file mode 100644 index 0000000..74af8bf --- /dev/null +++ b/VideoBrowser.Test/Views/AboutViewTest.cs @@ -0,0 +1,30 @@ +namespace VideoBrowser.Test.Views +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using VideoBrowser.Test.Common; + using VideoBrowser.Test.TestHelpers; + using VideoBrowser.ViewModels; + using VideoBrowser.Views; + + /// + /// Defines the . + /// + [TestClass] + public class AboutViewTest + { + #region Methods + + /// + /// The Show_AboutView. + /// + [TestMethod, ManualTest] + public void Show_AboutView() + { + var viewModel = new AboutViewModel(); + var view = new AboutView { DataContext = viewModel }; + WindowFactory.CreateAndShow(view); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Views/DownloadQueueViewTest.cs b/VideoBrowser.Test/Views/DownloadQueueViewTest.cs new file mode 100644 index 0000000..e5bd637 --- /dev/null +++ b/VideoBrowser.Test/Views/DownloadQueueViewTest.cs @@ -0,0 +1,69 @@ +namespace VideoBrowser.Test.Views +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + using System.Collections.ObjectModel; + using System.Windows.Input; + using VideoBrowser.Controls.CefSharpBrowser; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Core; + using VideoBrowser.Models; + using VideoBrowser.Test.Common; + using VideoBrowser.Test.TestHelpers; + using VideoBrowser.ViewModels; + using VideoBrowser.Views; + + /// + /// Defines the . + /// + [TestClass] + public class DownloadQueueViewTest + { + #region Methods + + /// + /// The Show_DownloadQueueView. + /// + [TestMethod, ManualTest] + public void Show_DownloadQueueView() + { + var globalBrowserData = new GlobalBrowserData(); + var viewModel = new DownloadQueueViewModel(globalBrowserData.DownloadItemModels); + this.CreateDummyOperations(viewModel.DownloadItemModels, viewModel.OnPauseDownloadCalled); + var view = new DownloadQueueView { DataContext = viewModel }; + WindowFactory.CreateAndShow(view); + } + + /// + /// The CreateDummyOperations. + /// + /// The operations. + /// The pauseDownloadAction. + private void CreateDummyOperations(ObservableCollection operations, Action pauseDownloadAction) + { + var random = new Random(); + for (var i = 0; i < 10; i++) + { + var operation = new DummyOperation() { Status = (OperationStatus)(i % 6) }; + var operationModel = new OperationModel(operation) { PauseDownloadAction = pauseDownloadAction, IsQueuedControlsVisible = (i & 1) == 1 }; + var progress = TestHelper.GetRandomLong(0, 100); + operation.Duration = TestHelper.GetRandomLong(10, 10000); + operation.FileSize = TestHelper.GetRandomLong(10000, 10000000000); + operation.Input = $"https://youtube.com/view={i}"; + operation.Link = $"https://youtube.com/link={i}"; ; + operation.Output = $@"C:\Users\YoutubeUser\File{TestHelper.GetRandomLong(100, 10000)}.mp4"; + operation.Progress = progress; + operation.ProgressPercentage = (int)progress; + operation.ProgressText = $"{progress}%"; + operation.ReportsProgress = true; + operation.Speed = $"{TestHelper.GetRandomInt(10, 100)}MB/s"; + operation.Status = (OperationStatus)(i % 6); + operation.Thumbnail = null; + operation.Title = $"Youtube Title Movie Number {i}"; + operations.Add(operationModel); + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Views/SettingsViewTest.cs b/VideoBrowser.Test/Views/SettingsViewTest.cs new file mode 100644 index 0000000..d73d1ff --- /dev/null +++ b/VideoBrowser.Test/Views/SettingsViewTest.cs @@ -0,0 +1,30 @@ +namespace VideoBrowser.Test.Views +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Controls.CefSharpBrowser.Views; + using VideoBrowser.Test.Common; + using VideoBrowser.Test.TestHelpers; + + /// + /// Defines the . + /// + [TestClass] + public class SettingsViewTest + { + #region Methods + + /// + /// The Show_SettingsView. + /// + [TestMethod, ManualTest] + public void Show_SettingsView() + { + var viewModel = new SettingsViewModel(); + var view = new SettingsView { DataContext = viewModel }; + WindowFactory.CreateAndShow(view); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/Views/UrlEditorViewTest.cs b/VideoBrowser.Test/Views/UrlEditorViewTest.cs new file mode 100644 index 0000000..dfde681 --- /dev/null +++ b/VideoBrowser.Test/Views/UrlEditorViewTest.cs @@ -0,0 +1,38 @@ +namespace VideoBrowser.Test.Views +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System.Windows.Controls; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Core; + using VideoBrowser.Test.Common; + using VideoBrowser.Test.TestHelpers; + using VideoBrowser.ViewModels; + using VideoBrowser.Views; + + /// + /// Defines the . + /// + [TestClass] + public class UrlEditorViewTest + { + #region Methods + + /// + /// The Show_UrlEditorView. + /// + [TestMethod, ManualTest] + public void Show_UrlEditorView() + { + var urlReader = new UrlReader(); + var settings = new SettingsViewModel(); + var viewModelA = new UrlEditorViewModel(urlReader, settings) { IsVisible = true, FileName = "Youtube Video File Name", FileSize = "5 MB", Duration = "00:02:45" }; + var viewModelB = new UrlEditorViewModel(urlReader, settings) { IsVisible = true, FileName = "Youtube Video File Name", FileSize = "1.4 MB", Duration = "00:02:45", IsBusy = true }; + var stackPanel = new StackPanel(); + stackPanel.Children.Add(new UrlEditorView { DataContext = viewModelA }); + stackPanel.Children.Add(new UrlEditorView { DataContext = viewModelB }); + WindowFactory.CreateAndShow(stackPanel); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser.Test/key.snk b/VideoBrowser.Test/key.snk new file mode 100644 index 0000000..1aeab8d Binary files /dev/null and b/VideoBrowser.Test/key.snk differ diff --git a/VideoBrowser.sln b/VideoBrowser.sln new file mode 100644 index 0000000..666bed7 --- /dev/null +++ b/VideoBrowser.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.960 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoBrowser", "VideoBrowser\VideoBrowser.csproj", "{2D8225BC-F5B6-4F29-A9E3-F4694D260597}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoBrowser.Test", "VideoBrowser.Test\VideoBrowser.Test.csproj", "{CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoBrowserTestApp", "VideoBrowserTestApp\VideoBrowserTestApp.csproj", "{78B417A3-4C48-45FF-91FB-66AF9436CD75}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Debug|x64.ActiveCfg = Debug|x64 + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Debug|x64.Build.0 = Debug|x64 + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Release|Any CPU.Build.0 = Release|Any CPU + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Release|x64.ActiveCfg = Release|x64 + {2D8225BC-F5B6-4F29-A9E3-F4694D260597}.Release|x64.Build.0 = Release|x64 + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Debug|x64.ActiveCfg = Debug|x64 + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Debug|x64.Build.0 = Debug|x64 + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Release|Any CPU.Build.0 = Release|Any CPU + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Release|x64.ActiveCfg = Release|x64 + {CEB9D499-9F24-42C0-B6AD-165D2ABF9B7B}.Release|x64.Build.0 = Release|x64 + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Debug|x64.ActiveCfg = Debug|x64 + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Debug|x64.Build.0 = Debug|x64 + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Release|Any CPU.Build.0 = Release|Any CPU + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Release|x64.ActiveCfg = Release|x64 + {78B417A3-4C48-45FF-91FB-66AF9436CD75}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {66253547-6CF3-47BA-A026-B14989D85653} + EndGlobalSection +EndGlobal diff --git a/VideoBrowser/App.config b/VideoBrowser/App.config new file mode 100644 index 0000000..cdd1fdc --- /dev/null +++ b/VideoBrowser/App.config @@ -0,0 +1,36 @@ + + + + +
+ + + + + + + + + 0,0 + + + 1920 + + + 1080 + + + https://yoyokits.github.io/VideoBrowser/welcome.html + + + False + + + 5 + + + Normal + + + + diff --git a/VideoBrowser/App.xaml b/VideoBrowser/App.xaml new file mode 100644 index 0000000..e3b4ebb --- /dev/null +++ b/VideoBrowser/App.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/App.xaml.cs b/VideoBrowser/App.xaml.cs new file mode 100644 index 0000000..8d51689 --- /dev/null +++ b/VideoBrowser/App.xaml.cs @@ -0,0 +1,50 @@ +namespace VideoBrowser +{ + using CefSharp; + using System.Diagnostics; + using System.Threading.Tasks; + using System.Windows; + using VideoBrowser.Common; + using VideoBrowser.Core; + using VideoBrowser.Helpers; + + /// + /// Interaction logic for App.xaml. + /// + public partial class App : Application + { + #region Methods + + /// + /// The OnApplication_Startup. + /// + /// The sender. + /// The e. + private void OnApplication_Startup(object sender, StartupEventArgs e) + { + AppEnvironment.Arguments = e.Args; + this.UpdateYoutubeDl(); + } + + #endregion Methods + + private void Application_Exit(object sender, ExitEventArgs e) + { + Cef.Shutdown(); + } + + private void UpdateYoutubeDl() + { + Task.Run(() => + { + var path = YoutubeDl.YouTubeDlPath; + ProcessHelper.StartProcess(path, "--update --no-check-certificate", this.UpdateOutput, null, null); + }); + } + + private void UpdateOutput(Process process, string output) + { + Logger.Info($"Youtube-dl message:{output}"); + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Binaries/ffmpeg.exe b/VideoBrowser/Binaries/ffmpeg.exe new file mode 100644 index 0000000..f9208f2 Binary files /dev/null and b/VideoBrowser/Binaries/ffmpeg.exe differ diff --git a/VideoBrowser/Binaries/youtube-dl.exe b/VideoBrowser/Binaries/youtube-dl.exe new file mode 100644 index 0000000..730b7f5 Binary files /dev/null and b/VideoBrowser/Binaries/youtube-dl.exe differ diff --git a/VideoBrowser/Common/AppEnvironment.cs b/VideoBrowser/Common/AppEnvironment.cs new file mode 100644 index 0000000..c037af2 --- /dev/null +++ b/VideoBrowser/Common/AppEnvironment.cs @@ -0,0 +1,154 @@ +namespace VideoBrowser.Common +{ + using System; + using System.IO; + using System.Reflection; + + /// + /// Defines the . + /// + public static class AppEnvironment + { + #region Constants + + public const string Name = "Cekli Browser"; + + public const string LongName = "Cekli Video Browser and Downloader"; + + public const int ProgressUpdateDelay = 250; + + public static readonly string ShortName = Assembly.GetExecutingAssembly().GetName().Name; + + private const string BinariesDirectory = "Binaries"; + + private const string JsonDirectory = "Json"; + + private const string LogsDirectory = "Logs"; + + #endregion Constants + + #region Properties + + /// + /// Gets the AppBinaryDirectory. + /// + public static string AppBinaryDirectory => Path.Combine(AppDirectory, BinariesDirectory); + + /// + /// Gets the AppDirectory. + /// + public static string AppDirectory => AppDomain.CurrentDomain.BaseDirectory; + + /// + /// Gets or sets the Arguments. + /// + public static string[] Arguments { get; internal set; } + + /// + /// Gets the Author. + /// + public static string Author => "Yohanes Wahyu Nurcahyo"; + + /// + /// Gets the HomeUrl. + /// + public static string HomeUrl => "https://yoyokits.github.io/VideoBrowser/welcome.html"; + + /// + /// Gets the ProjectUrl. + /// + public static string ProjectUrl { get; } = "https://github.com/yoyokits/VideoBrowser"; + + /// + /// Gets the UserLocalApplicationData. + /// + public static string UserLocalApplicationData { get; } = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + /// + /// Gets the UserProfile. + /// + public static string UserProfile { get; } = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + /// + /// Gets the UserVideoFolder. + /// + public static string UserVideoFolder { get; } = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos); + + /// + /// Gets the Version. + /// + public static string Version { get; } = $"{Assembly.GetExecutingAssembly().GetName().Version.ToString()}"; + + #endregion Properties + + #region Methods + + /// + /// Returns the local app data directory for this program. Also makes sure the directory exists. + /// + /// The . + public static string GetAppDataDirectory() + { + var path = Path.Combine(UserLocalApplicationData, AppEnvironment.ShortName); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + return path; + } + + /// + /// The application folder. + /// + /// The specialFolder. + /// The . + public static string GetFolder(Environment.SpecialFolder specialFolder) => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + /// + /// Returns the json directory for this program. Also makes sure the directory exists. + /// + /// The . + public static string GetJsonDirectory() + { + var path = Path.Combine(GetAppDataDirectory(), JsonDirectory); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + return path; + } + + /// + /// Returns the logs directory for this program. Also makes sure the directory exists. + /// + /// The . + public static string GetLogsDirectory() + { + var path = Path.Combine(GetAppDataDirectory(), LogsDirectory); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + + return path; + } + + /// + /// The GetUserLocalApplicationData. + /// + /// The . + public static string GetUserLocalApplicationData() + { + var userFolder = UserLocalApplicationData; + var appFolder = Path.Combine(userFolder, ShortName); + if (!Directory.Exists(appFolder)) + { + Directory.CreateDirectory(appFolder); + } + + return appFolder; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/ByteSizeFormatProvider.cs b/VideoBrowser/Common/ByteSizeFormatProvider.cs new file mode 100644 index 0000000..34f7cdc --- /dev/null +++ b/VideoBrowser/Common/ByteSizeFormatProvider.cs @@ -0,0 +1,135 @@ +namespace VideoBrowser.Common +{ + using System; + + /// + /// Defines the + /// + public class ByteFormatProvider : IFormatProvider, ICustomFormatter + { + #region Constants + + private const string FileSizeFormat = "fs"; + + private const decimal OneGigaByte = OneMegaByte * 1024M; + + private const decimal OneKiloByte = 1024M; + + private const decimal OneMegaByte = OneKiloByte * 1024M; + + private const string SpeedFormat = "s"; + + #endregion Constants + + #region Properties + + /// + /// Gets the Lock + /// + private object Lock { get; } = new object(); + + #endregion Properties + + #region Methods + + /// + /// The Format + /// + /// The format + /// The arg + /// The formatProvider + /// The + public string Format(string format, object arg, IFormatProvider formatProvider) + { + lock (this.Lock) + { + if (format == null || (!format.StartsWith(FileSizeFormat) && !format.StartsWith(SpeedFormat))) + { + return DefaultFormat(format, arg, formatProvider); + } + + if (arg is string) + { + return DefaultFormat(format, arg, formatProvider); + } + + decimal size; + + try + { + size = Convert.ToDecimal(arg); + } + catch (InvalidCastException) + { + return DefaultFormat(format, arg, formatProvider); + } + + string suffix; + + if (size > OneGigaByte) + { + size /= OneGigaByte; + suffix = "GB"; + } + else if (size > OneMegaByte) + { + size /= OneMegaByte; + suffix = "MB"; + } + else if (size > OneKiloByte) + { + size /= OneKiloByte; + suffix = "KB"; + } + else + { + suffix = "Bytes"; + } + + if (format.StartsWith(SpeedFormat)) + { + suffix += "/s"; + } + + var postion = format.StartsWith(SpeedFormat) ? SpeedFormat.Length : FileSizeFormat.Length; + var precision = format.Substring(postion); + + if (string.IsNullOrEmpty(precision)) + { + precision = "2"; + } + + return string.Format("{0:N" + precision + "}{1}", size, " " + suffix); + } + } + + /// + /// The GetFormat + /// + /// The formatType + /// The + public object GetFormat(Type formatType) + { + return formatType == typeof(ICustomFormatter) ? (this) : null; + } + + /// + /// The DefaultFormat + /// + /// The format + /// The arg + /// The formatProvider + /// The + private static string DefaultFormat(string format, object arg, IFormatProvider formatProvider) + { + if (arg is IFormattable formattableArg) + { + return formattableArg.ToString(format, formatProvider); + } + + return arg.ToString(); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/Epsilon.cs b/VideoBrowser/Common/Epsilon.cs new file mode 100644 index 0000000..eaf97f4 --- /dev/null +++ b/VideoBrowser/Common/Epsilon.cs @@ -0,0 +1,23 @@ +namespace VideoBrowser.Common +{ + /// + /// Defines the + /// + public static class Epsilon + { + /// + /// The epsilon for angle. + /// + public const double Angle = 0.01; + + /// + /// The epsilon for default. + /// + public const double Default = 0.00000000001; + + /// + /// The epsilon for meter. + /// + public const double Meter = 0.00001; + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/Log.cs b/VideoBrowser/Common/Log.cs new file mode 100644 index 0000000..55c3da9 --- /dev/null +++ b/VideoBrowser/Common/Log.cs @@ -0,0 +1,169 @@ +namespace VideoBrowser.Common +{ + using log4net; + using log4net.Appender; + using log4net.Core; + using log4net.Layout; + using log4net.Repository.Hierarchy; + using System; + using System.IO; + using System.Reflection; + + /// + /// Defines the + /// Log the important process to trace the last processes before error occurs. + /// + public static class Logger + { + #region Constructors + + /// + /// Initializes static members of the class. + /// + static Logger() + { + Setup(nameof(VideoBrowser)); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the Log. + /// + public static ILog Log { get; } = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + /// + /// Gets the LogFilePath. + /// + public static string LogFilePath { get; private set; } + + #endregion Properties + + #region Methods + + /// + /// The Debug Log. + /// + /// The message. + public static void Debug(object message) + { + Log.Debug(message); + } + + /// + /// The Error Log. + /// + /// The message. + public static void Error(object message) + { + Log.Error(message); + } + + /// + /// The Fatal Log. + /// + /// The message. + public static void Fatal(object message) + { + Log.Fatal(message); + } + + /// + /// The Info. + /// + /// The message. + public static void Info(object message = null) + { + if (message == null) + { + message = string.Empty; + } + + Log.Info(message); + System.Diagnostics.Trace.WriteLine(message); + } + + /// + /// The Info Log. + /// + /// The source. + /// The message. + public static void Info(this object source, object message) + { + Log.Info(message); + } + + /// + /// The Setup. + /// + /// The appName. + public static void Setup(string appName) + { + LogFilePath = GetLogFilePath(appName); + var entryAssembly = Assembly.GetExecutingAssembly(); + var hierarchy = (Hierarchy)LogManager.GetRepository(entryAssembly); + + var patternLayout = new PatternLayout + { + ConversionPattern = "%date [%thread] %-5level %logger - %message%newline" + }; + + patternLayout.ActivateOptions(); + + var roller = new RollingFileAppender + { + AppendToFile = false, + File = LogFilePath, + Layout = patternLayout, + MaxSizeRollBackups = 5, + MaximumFileSize = "1GB", + RollingStyle = RollingFileAppender.RollingMode.Size, + StaticLogFileName = true + }; + roller.ActivateOptions(); + hierarchy.Root.AddAppender(roller); + + var memory = new MemoryAppender(); + memory.ActivateOptions(); + hierarchy.Root.AddAppender(memory); + + hierarchy.Root.Level = Level.Info; + hierarchy.Configured = true; + } + + /// + /// The Warn. + /// + /// The source. + /// The message. + public static void Warn(this object source, object message) + { + Log.Warn(message); + } + + /// + /// Saves given Exception's stack trace to a readable file in the local application data folder. + /// + /// The Exception to save. + public static void WriteException(Exception ex) + { + Logger.Info(ex.ToString()); + } + + /// + /// The GetLogFilePath. + /// + /// The appName. + /// The . + internal static string GetLogFilePath(string appName) + { + var folder = AppEnvironment.GetUserLocalApplicationData(); + var path = Path.Combine(folder, $"{appName}.log"); + return path; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/NotifyPropertyChanged.cs b/VideoBrowser/Common/NotifyPropertyChanged.cs new file mode 100644 index 0000000..e412618 --- /dev/null +++ b/VideoBrowser/Common/NotifyPropertyChanged.cs @@ -0,0 +1,31 @@ +namespace VideoBrowser.Common +{ + using System.ComponentModel; + using System.Runtime.CompilerServices; + + /// + /// Defines the + /// Standard INotifyPropertyChanged implementation. + /// + public class NotifyPropertyChanged : INotifyPropertyChanged + { + /// + /// Defines the PropertyChanged + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// The property changed handler + /// + protected PropertyChangedEventHandler PropertyChangedHandler => this.PropertyChanged; + + /// + /// The OnPropertyChanged + /// + /// The + public void OnPropertyChanged([CallerMemberName]string propertyName = null) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/OutputFormat.cs b/VideoBrowser/Common/OutputFormat.cs new file mode 100644 index 0000000..7dbc986 --- /dev/null +++ b/VideoBrowser/Common/OutputFormat.cs @@ -0,0 +1,37 @@ +namespace VideoBrowser.Common +{ + #region Enums + + /// + /// Defines the OutputFormat + /// + public enum OutputFormat + { + /// + /// Defines the Webm + /// + Webm, + + /// + /// Defines the MP4 + /// + MP4, + + /// + /// Defines the MP3 + /// + MP3, + + /// + /// Defines the M4A + /// + M4A, + + /// + /// Defines the Vorbis + /// + Vorbis + } + + #endregion Enums +} \ No newline at end of file diff --git a/VideoBrowser/Common/Percentage.cs b/VideoBrowser/Common/Percentage.cs new file mode 100644 index 0000000..25d1668 --- /dev/null +++ b/VideoBrowser/Common/Percentage.cs @@ -0,0 +1,92 @@ +namespace VideoBrowser.Common +{ + using System; + using VideoBrowser.Extensions; + + /// + /// Defines the + /// + internal struct Percentage : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The percent + public Percentage(double percent) + { + this.Percent = percent; + } + + /// + /// Gets the Empty + /// + public static Percentage Empty { get; } = new Percentage(double.NaN); + + /// + /// Gets a value indicating whether IsEmpty + /// + public bool IsEmpty => double.IsNaN(this.Percent); + + /// + /// Gets the Normalized + /// + public double Normalized => this.Percent * 0.01; + + /// + /// Gets the Percent + /// + public double Percent { get; } + + /// + /// The Equals + /// + /// The obj + /// The + public override bool Equals(object obj) + { + return base.Equals(obj); + } + + /// + /// The Equals + /// + /// The other + /// The + public bool Equals(Percentage other) + { + var equals = this.Percent.IsEqualTo(other.Percent); + return equals; + } + + /// + /// The GetHashCode + /// + /// The + public override int GetHashCode() + { + return this.Percent.GetHashCode(); + } + + /// + /// The ToString + /// + /// The + public override string ToString() + { + var message = $"{this.Percent.Format()}%"; + return message; + } + + public static bool operator ==(Percentage left, Percentage right) + { + var equals = left.Percent.IsEqualTo(right.Percent); + return equals; + } + + public static bool operator !=(Percentage left, Percentage right) + { + var equals = left.Percent.IsEqualTo(right.Percent); + return !equals; + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/Range.cs b/VideoBrowser/Common/Range.cs new file mode 100644 index 0000000..db2d842 --- /dev/null +++ b/VideoBrowser/Common/Range.cs @@ -0,0 +1,107 @@ +namespace VideoBrowser.Common +{ + using System; + using VideoBrowser.If; + + /// + /// Defines the + /// + /// + public struct Range : IRange where T : IComparable + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The start. + /// The end. + public Range(T start, T end) + { + this.Start = start; + this.End = end; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the End + /// + public T End { get; } + + /// + /// Gets the Start + /// + public T Start { get; } + + #endregion Properties + + #region Methods + + /// + /// The Equals + /// + /// The other + /// The + public bool Equals(IRange other) + { + return this.Start.Equals(other.Start) && this.End.Equals(other.End); + } + + /// + /// The Equals + /// + /// The + /// The + public override bool Equals(object obj) + { + return this == (Range)obj; + } + + /// + /// The GetHashCode + /// + /// The + public override int GetHashCode() + { + var startHash = this.Start.GetHashCode(); + var endHash = this.End.GetHashCode(); + return startHash ^ (startHash << 8) ^ endHash ^ (endHash << 4); + } + + /// + /// The ToString + /// + /// The + public override string ToString() + { + return $"Start:{this.Start};End:{this.End}"; + } + + #endregion Methods + + /// + /// Implements the operator ==. + /// + /// The left. + /// The right. + /// The result of the operator. + public static bool operator ==(Range left, Range right) + { + return Equals(left.Start, right.Start) && Equals(left.End, right.End); + } + + /// + /// Implements the operator !=. + /// + /// The left. + /// The right. + /// The result of the operator. + public static bool operator !=(Range left, Range right) + { + return !(left == right); + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/RangeDouble.cs b/VideoBrowser/Common/RangeDouble.cs new file mode 100644 index 0000000..346882d --- /dev/null +++ b/VideoBrowser/Common/RangeDouble.cs @@ -0,0 +1,120 @@ +namespace VideoBrowser.Common +{ + using System; + using VideoBrowser.Extensions; + using VideoBrowser.If; + + /// + /// Defines the + /// + public struct RangeDouble : IRange + { + private const double PRECISION = 1E-6; + + /// + /// Initializes a new instance of the class. + /// + /// The start + /// The end + /// The allowInvert + public RangeDouble(double start, double end, bool allowInvert = false) + { + if (!allowInvert && start >= end) + { + throw new ArgumentException($"{nameof(start)} is equal or greater than {nameof(end)}"); + } + + this.Start = start; + this.End = end; + } + + /// + /// Gets the Empty + /// + public static RangeDouble Empty { get; } = new RangeDouble(); + + /// + /// Gets the End + /// + public double End { get; } + + /// + /// Gets the Start + /// + public double Start { get; } + + /// + /// The Equals + /// + /// The other + /// The + public bool Equals(IRange other) + { + return Equals(this.Start, other.Start) && Equals(this.End, other.End); + } + + /// + /// The Equals + /// + /// The + /// The + public override bool Equals(object obj) + { + if (!(obj is RangeDouble)) + { + return false; + } + + return this == (RangeDouble)obj; + } + + /// Equalses the specified other. + /// The other. + public bool Equals(RangeDouble other) + { + return this.Start.IsEqualTo(other.Start, PRECISION) && this.End.IsEqualTo(other.End, PRECISION); + } + + /// + /// The GetHashCode + /// + /// The + public override int GetHashCode() + { + var startHash = this.Start.GetHashCode(); + var endHash = this.End.GetHashCode(); + return startHash ^ (startHash << 8) ^ endHash ^ (endHash << 4); + } + + /// + /// The ToString + /// + /// The + public override string ToString() + { + return $"Start:{this.Start.Format()};End:{this.End.Format()};Length:{this.Length().Format()}"; + } + + /// + /// Implements the operator ==. + /// + /// The left. + /// The right. + /// The result of the operator. + public static bool operator ==(RangeDouble left, RangeDouble right) + { + return left.Start.IsEqualTo(right.Start, PRECISION) && left.End.IsEqualTo(right.End, PRECISION); + } + + /// + /// Implements the operator !=. + /// + /// The left. + /// The right. + /// The result of the operator. + public static bool operator !=(RangeDouble left, RangeDouble right) + { + return !(left == right); + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/RangeInt.cs b/VideoBrowser/Common/RangeInt.cs new file mode 100644 index 0000000..acd9b13 --- /dev/null +++ b/VideoBrowser/Common/RangeInt.cs @@ -0,0 +1,112 @@ +namespace VideoBrowser.Common +{ + using System; + using VideoBrowser.If; + + /// + /// Defines the + /// + public struct RangeInt : IRange + { + /// + /// Initializes a new instance of the class. + /// + /// The start + /// The end + /// The allowInvert + public RangeInt(int start, int end, bool allowInvert = false) + { + if (!allowInvert && start > end) + { + throw new ArgumentException($"{nameof(start)} is equal or greater than {nameof(end)}"); + } + + this.Start = start; + this.End = end; + } + + /// + /// Gets the End + /// + public int End { get; } + + /// + /// Gets the Start + /// + public int Start { get; } + + /// + /// The Equals + /// + /// The other + /// The + public bool Equals(IRange other) + { + return Equals(this.Start, other.Start) && Equals(this.End, other.End); + } + + /// + /// The Equals + /// + /// The + /// The + public override bool Equals(object obj) + { + if (!(obj is RangeInt)) + { + return false; + } + + return this == (RangeInt)obj; + } + + /// Equalses the specified other. + /// The other. + public bool Equals(RangeInt other) + { + return this.Start == other.Start && this.End == other.End; + } + + /// + /// The GetHashCode + /// + /// The + public override int GetHashCode() + { + var startHash = this.Start.GetHashCode(); + var endHash = this.End.GetHashCode(); + return startHash ^ (startHash << 8) ^ endHash ^ (endHash << 4); + } + + /// + /// The ToString + /// + /// The + public override string ToString() + { + return $"Start:{this.Start};End:{this.End};Length:{this.End - this.Start}"; + } + + /// + /// Implements the operator ==. + /// + /// The left. + /// The right. + /// The result of the operator. + public static bool operator ==(RangeInt left, RangeInt right) + { + return left.Start == right.Start && left.End == right.End; + } + + /// + /// Implements the operator !=. + /// + /// The left. + /// The right. + /// The result of the operator. + public static bool operator !=(RangeInt left, RangeInt right) + { + return !(left == right); + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/RelayCommand.cs b/VideoBrowser/Common/RelayCommand.cs new file mode 100644 index 0000000..413439e --- /dev/null +++ b/VideoBrowser/Common/RelayCommand.cs @@ -0,0 +1,61 @@ +namespace VideoBrowser.Common +{ + using System; + using System.Windows.Input; + + /// + /// The WPF default command. + /// + /// + public class RelayCommand : ICommand + { + private readonly Func _canExecute; + + private readonly Action _execute; + + private readonly string _name; + + /// + /// Initializes a new instance of the class. + /// + /// The execute. + /// The can execute. + public RelayCommand(Action execute, string name = "", Func canExecute = null) + { + this._execute = execute; + this._name = name; + this._canExecute = canExecute; + } + + /// + /// Occurs when changes occur that affect whether or not the command should execute. + /// + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + /// + /// Defines the method that determines whether the command can execute in its current state. + /// + /// Data used by the command. If the command does not require data to be passed, this object can be set to . + /// + /// if this command can be executed; otherwise, . + /// + public bool CanExecute(object parameter) + { + return this._canExecute == null || this._canExecute(parameter); + } + + /// + /// Defines the method to be called when the command is invoked. + /// + /// Data used by the command. If the command does not require data to be passed, this object can be set to . + public void Execute(object parameter) + { + this._execute(parameter); + Logger.Info($"{nameof(RelayCommand)} {this._name} is executed"); + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Common/VideoState.cs b/VideoBrowser/Common/VideoState.cs new file mode 100644 index 0000000..5445136 --- /dev/null +++ b/VideoBrowser/Common/VideoState.cs @@ -0,0 +1,32 @@ +namespace VideoBrowser.Common +{ + #region Enums + + /// + /// Defines the VideoState + /// + public enum VideoState + { + /// + /// Defines the None + /// + None, + + /// + /// Defines the Downloading + /// + Downloading, + + /// + /// Defines the Downloaded + /// + Downloaded, + + /// + /// Defines the Error + /// + Error + } + + #endregion Enums +} \ No newline at end of file diff --git a/VideoBrowser/Controls/AdornedControl/AdornedControl.cs b/VideoBrowser/Controls/AdornedControl/AdornedControl.cs new file mode 100644 index 0000000..ca67bf8 --- /dev/null +++ b/VideoBrowser/Controls/AdornedControl/AdornedControl.cs @@ -0,0 +1,726 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; + +namespace VideoBrowser.Controls.AdornedControl +{ + /// + /// A content control that allows an adorner for the content to + /// be defined in XAML. + /// + public class AdornedControl : ContentControl + { + #region Dependency Properties + + /// + /// Dependency properties. + /// + public static readonly DependencyProperty IsAdornerVisibleProperty = + DependencyProperty.Register("IsAdornerVisible", typeof(bool), typeof(AdornedControl), + new FrameworkPropertyMetadata(IsAdornerVisible_PropertyChanged)); + + public static readonly DependencyProperty AdornerContentProperty = + DependencyProperty.Register("AdornerContent", typeof(FrameworkElement), typeof(AdornedControl), + new FrameworkPropertyMetadata(AdornerContent_PropertyChanged)); + + public static readonly DependencyProperty HorizontalAdornerPlacementProperty = + DependencyProperty.Register("HorizontalAdornerPlacement", typeof(AdornerPlacement), typeof(AdornedControl), + new FrameworkPropertyMetadata(AdornerPlacement.Inside)); + + public static readonly DependencyProperty VerticalAdornerPlacementProperty = + DependencyProperty.Register("VerticalAdornerPlacement", typeof(AdornerPlacement), typeof(AdornedControl), + new FrameworkPropertyMetadata(AdornerPlacement.Inside)); + + public static readonly DependencyProperty AdornerOffsetXProperty = + DependencyProperty.Register("AdornerOffsetX", typeof(double), typeof(AdornedControl)); + + public static readonly DependencyProperty AdornerOffsetYProperty = + DependencyProperty.Register("AdornerOffsetY", typeof(double), typeof(AdornedControl)); + + public static readonly DependencyProperty IsMouseOverShowEnabledProperty = + DependencyProperty.Register("IsMouseOverShowEnabled", typeof(bool), typeof(AdornedControl), + new FrameworkPropertyMetadata(true, IsMouseOverShowEnabled_PropertyChanged)); + + public static readonly DependencyProperty FadeInTimeProperty = + DependencyProperty.Register("FadeInTime", typeof(double), typeof(AdornedControl), + new FrameworkPropertyMetadata(0.25)); + + public static readonly DependencyProperty FadeOutTimeProperty = + DependencyProperty.Register("FadeOutTime", typeof(double), typeof(AdornedControl), + new FrameworkPropertyMetadata(1.0)); + + public static readonly DependencyProperty CloseAdornerTimeOutProperty = + DependencyProperty.Register("CloseAdornerTimeOut", typeof(double), typeof(AdornedControl), + new FrameworkPropertyMetadata(2.0, CloseAdornerTimeOut_PropertyChanged)); + + public static readonly DependencyProperty AdornedTemplatePartNameProperty = + DependencyProperty.Register("AdornedTemplatePartName", typeof(string), typeof(AdornedControl), + new FrameworkPropertyMetadata(null)); + + #endregion Dependency Properties + + #region Commands + + /// + /// Commands. + /// + public static readonly RoutedCommand ShowAdornerCommand = new RoutedCommand("ShowAdorner", typeof(AdornedControl)); + + public static readonly RoutedCommand FadeInAdornerCommand = new RoutedCommand("FadeInAdorner", typeof(AdornedControl)); + public static readonly RoutedCommand HideAdornerCommand = new RoutedCommand("HideAdorner", typeof(AdornedControl)); + public static readonly RoutedCommand FadeOutAdornerCommand = new RoutedCommand("FadeOutAdorner", typeof(AdornedControl)); + + #endregion Commands + + public AdornedControl() + { + this.Focusable = false; // By default don't want 'AdornedControl' to be focusable. + + this.DataContextChanged += new DependencyPropertyChangedEventHandler(AdornedControl_DataContextChanged); + + closeAdornerTimer.Tick += new EventHandler(closeAdornerTimer_Tick); + closeAdornerTimer.Interval = TimeSpan.FromSeconds(CloseAdornerTimeOut); + } + + /// + /// Show the adorner. + /// + public void ShowAdorner() + { + IsAdornerVisible = true; + } + + /// + /// Hide the adorner. + /// + public void HideAdorner() + { + IsAdornerVisible = false; + } + + /// + /// Fade the adorner in and make it visible. + /// + public void FadeInAdorner() + { + if (adornerShowState == AdornerShowState.Visible || + adornerShowState == AdornerShowState.FadingIn) + { + // Already visible or fading in. + return; + } + + this.ShowAdorner(); + + if (adornerShowState != AdornerShowState.FadingOut) + { + adorner.Opacity = 0.0; + } + + DoubleAnimation doubleAnimation = new DoubleAnimation(1.0, new Duration(TimeSpan.FromSeconds(FadeInTime))); + doubleAnimation.Completed += new EventHandler(fadeInAnimation_Completed); + doubleAnimation.Freeze(); + + adorner.BeginAnimation(FrameworkElement.OpacityProperty, doubleAnimation); + + adornerShowState = AdornerShowState.FadingIn; + } + + /// + /// Fade the adorner out and make it visible. + /// + public void FadeOutAdorner() + { + if (adornerShowState == AdornerShowState.FadingOut) + { + // + // Already fading out. + // + return; + } + + if (adornerShowState == AdornerShowState.Hidden) + { + // + // Adorner has already been hidden. + // + return; + } + + DoubleAnimation fadeOutAnimation = new DoubleAnimation(0.0, new Duration(TimeSpan.FromSeconds(FadeOutTime))); + fadeOutAnimation.Completed += new EventHandler(fadeOutAnimation_Completed); + fadeOutAnimation.Freeze(); + + adorner.BeginAnimation(FrameworkElement.OpacityProperty, fadeOutAnimation); + + adornerShowState = AdornerShowState.FadingOut; + } + + /// + /// Shows or hides the adorner. + /// Set to 'true' to show the adorner or 'false' to hide the adorner. + /// + public bool IsAdornerVisible + { + get + { + return (bool)GetValue(IsAdornerVisibleProperty); + } + set + { + SetValue(IsAdornerVisibleProperty, value); + } + } + + /// + /// Used in XAML to define the UI content of the adorner. + /// + public FrameworkElement AdornerContent + { + get + { + return (FrameworkElement)GetValue(AdornerContentProperty); + } + set + { + SetValue(AdornerContentProperty, value); + } + } + + /// + /// Specifies the horizontal placement of the adorner relative to the adorned control. + /// + public AdornerPlacement HorizontalAdornerPlacement + { + get + { + return (AdornerPlacement)GetValue(HorizontalAdornerPlacementProperty); + } + set + { + SetValue(HorizontalAdornerPlacementProperty, value); + } + } + + /// + /// Specifies the vertical placement of the adorner relative to the adorned control. + /// + public AdornerPlacement VerticalAdornerPlacement + { + get + { + return (AdornerPlacement)GetValue(VerticalAdornerPlacementProperty); + } + set + { + SetValue(VerticalAdornerPlacementProperty, value); + } + } + + /// + /// X offset of the adorner. + /// + public double AdornerOffsetX + { + get + { + return (double)GetValue(AdornerOffsetXProperty); + } + set + { + SetValue(AdornerOffsetXProperty, value); + } + } + + /// + /// Y offset of the adorner. + /// + public double AdornerOffsetY + { + get + { + return (double)GetValue(AdornerOffsetYProperty); + } + set + { + SetValue(AdornerOffsetYProperty, value); + } + } + + /// + /// Set to 'true' to make the adorner automatically fade-in and become visible when the mouse is hovered + /// over the adorned control. Also the adorner automatically fades-out when the mouse cursor is moved + /// aware from the adorned control (and the adorner). + /// + public bool IsMouseOverShowEnabled + { + get + { + return (bool)GetValue(IsMouseOverShowEnabledProperty); + } + set + { + SetValue(IsMouseOverShowEnabledProperty, value); + } + } + + /// + /// Specifies the time (in seconds) it takes to fade in the adorner. + /// + public double FadeInTime + { + get + { + return (double)GetValue(FadeInTimeProperty); + } + set + { + SetValue(FadeInTimeProperty, value); + } + } + + /// + /// Specifies the time (in seconds) it takes to fade out the adorner. + /// + public double FadeOutTime + { + get + { + return (double)GetValue(FadeOutTimeProperty); + } + set + { + SetValue(FadeOutTimeProperty, value); + } + } + + /// + /// Specifies the time (in seconds) after the mouse cursor moves away from the + /// adorned control (or the adorner) when the adorner begins to fade out. + /// + public double CloseAdornerTimeOut + { + get + { + return (double)GetValue(CloseAdornerTimeOutProperty); + } + set + { + SetValue(CloseAdornerTimeOutProperty, value); + } + } + + /// + /// By default this property is set to null. + /// When set to non-null it specifies the part name of a UI element + /// in the visual tree of the AdornedControl content that is to be adorned. + /// When this property is null it is the AdornerControl content that is adorned, + /// however when it is set the visual-tree is searched for a UI element that has the + /// specified part name, if the part is found then that UI element is adorned, otherwise + /// an exception "Failed to find part ..." is thrown. /// + /// + public string AdornedTemplatePartName + { + get + { + return (string)GetValue(AdornedTemplatePartNameProperty); + } + set + { + SetValue(AdornedTemplatePartNameProperty, value); + } + } + + #region Private Data Members + + /// + /// Command bindings. + /// + private static readonly CommandBinding ShowAdornerCommandBinding = new CommandBinding(ShowAdornerCommand, ShowAdornerCommand_Executed); + + private static readonly CommandBinding FadeInAdornerCommandBinding = new CommandBinding(FadeInAdornerCommand, FadeInAdornerCommand_Executed); + private static readonly CommandBinding HideAdornerCommandBinding = new CommandBinding(HideAdornerCommand, HideAdornerCommand_Executed); + private static readonly CommandBinding FadeOutAdornerCommandBinding = new CommandBinding(FadeInAdornerCommand, FadeOutAdornerCommand_Executed); + + /// + /// Specifies the current show/hide state of the adorner. + /// + private enum AdornerShowState + { + Visible, + Hidden, + FadingIn, + FadingOut, + } + + /// + /// Specifies the current show/hide state of the adorner. + /// + private AdornerShowState adornerShowState = AdornerShowState.Hidden; + + /// + /// Caches the adorner layer. + /// + private AdornerLayer adornerLayer = null; + + /// + /// The actual adorner create to contain our 'adorner UI content'. + /// + private FrameworkElementAdorner adorner = null; + + /// + /// This timer is used to fade out and close the adorner. + /// + private DispatcherTimer closeAdornerTimer = new DispatcherTimer(); + + #endregion Private Data Members + + #region Private/Internal Functions + + /// + /// Static constructor to register command bindings. + /// + static AdornedControl() + { + CommandManager.RegisterClassCommandBinding(typeof(AdornedControl), ShowAdornerCommandBinding); + CommandManager.RegisterClassCommandBinding(typeof(AdornedControl), FadeOutAdornerCommandBinding); + CommandManager.RegisterClassCommandBinding(typeof(AdornedControl), HideAdornerCommandBinding); + CommandManager.RegisterClassCommandBinding(typeof(AdornedControl), FadeInAdornerCommandBinding); + } + + /// + /// Event raised when the DataContext of the adorned control changes. + /// + private void AdornedControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + UpdateAdornerDataContext(); + } + + /// + /// Update the DataContext of the adorner from the adorned control. + /// + private void UpdateAdornerDataContext() + { + if (this.AdornerContent != null) + { + this.AdornerContent.DataContext = this.DataContext; + } + } + + /// + /// Event raised when the Show command is executed. + /// + private static void ShowAdornerCommand_Executed(object target, ExecutedRoutedEventArgs e) + { + AdornedControl c = (AdornedControl)target; + c.ShowAdorner(); + } + + /// + /// Event raised when the FadeIn command is executed. + /// + private static void FadeInAdornerCommand_Executed(object target, ExecutedRoutedEventArgs e) + { + AdornedControl c = (AdornedControl)target; + c.FadeOutAdorner(); + } + + /// + /// Event raised when the Hide command is executed. + /// + private static void HideAdornerCommand_Executed(object target, ExecutedRoutedEventArgs e) + { + AdornedControl c = (AdornedControl)target; + c.HideAdorner(); + } + + /// + /// Event raised when the FadeOut command is executed. + /// + private static void FadeOutAdornerCommand_Executed(object target, ExecutedRoutedEventArgs e) + { + AdornedControl c = (AdornedControl)target; + c.FadeOutAdorner(); + } + + /// + /// Event raised when the value of IsAdornerVisible has changed. + /// + private static void IsAdornerVisible_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + AdornedControl c = (AdornedControl)o; + c.ShowOrHideAdornerInternal(); + } + + /// + /// Event raised when the IsMouseOverShowEnabled property has changed. + /// + private static void IsMouseOverShowEnabled_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + AdornedControl c = (AdornedControl)o; + c.closeAdornerTimer.Stop(); + c.HideAdorner(); + } + + /// + /// Event raised when the CloseAdornerTimeOut property has change. + /// + private static void CloseAdornerTimeOut_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + AdornedControl c = (AdornedControl)o; + c.closeAdornerTimer.Interval = TimeSpan.FromSeconds(c.CloseAdornerTimeOut); + } + + /// + /// Event raised when the value of AdornerContent has changed. + /// + private static void AdornerContent_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + AdornedControl c = (AdornedControl)o; + c.ShowOrHideAdornerInternal(); + + FrameworkElement oldAdornerContent = (FrameworkElement)e.OldValue; + if (oldAdornerContent != null) + { + oldAdornerContent.MouseEnter -= new MouseEventHandler(c.adornerContent_MouseEnter); + oldAdornerContent.MouseLeave -= new MouseEventHandler(c.adornerContent_MouseLeave); + } + + FrameworkElement newAdornerContent = (FrameworkElement)e.NewValue; + if (newAdornerContent != null) + { + newAdornerContent.MouseEnter += new MouseEventHandler(c.adornerContent_MouseEnter); + newAdornerContent.MouseLeave += new MouseEventHandler(c.adornerContent_MouseLeave); + } + } + + /// + /// Event raised when the mouse cursor enters the area of the adorner. + /// + private void adornerContent_MouseEnter(object sender, MouseEventArgs e) + { + MouseEnterLogic(); + } + + /// + /// Event raised when the mouse cursor leaves the area of the adorner. + /// + private void adornerContent_MouseLeave(object sender, MouseEventArgs e) + { + MouseLeaveLogic(); + } + + /// + /// Internal method to show or hide the adorner based on the value of IsAdornerVisible. + /// + private void ShowOrHideAdornerInternal() + { + if (IsAdornerVisible) + { + ShowAdornerInternal(); + } + else + { + HideAdornerInternal(); + } + } + + /// + /// Finds a child element in the visual tree that has the specified name. + /// Returns null if no child with that name exists. + /// + public static FrameworkElement FindNamedChild(FrameworkElement rootElement, string childName) + { + int numChildren = VisualTreeHelper.GetChildrenCount(rootElement); + for (int i = 0; i < numChildren; ++i) + { + DependencyObject child = VisualTreeHelper.GetChild(rootElement, i); + FrameworkElement childElement = child as FrameworkElement; + if (childElement != null && childElement.Name == childName) + { + return childElement; + } + + FrameworkElement foundElement = FindNamedChild(childElement, childName); + if (foundElement != null) + { + return foundElement; + } + } + + return null; + } + + /// + /// Internal method to show the adorner. + /// + private void ShowAdornerInternal() + { + if (this.adorner != null) + { + // Already adorned. + return; + } + + if (this.AdornerContent != null) + { + if (this.adornerLayer == null) + { + this.adornerLayer = AdornerLayer.GetAdornerLayer(this); + } + + if (this.adornerLayer != null) + { + FrameworkElement adornedControl = this; // The control to be adorned defaults to 'this'. + + if (!string.IsNullOrEmpty(this.AdornedTemplatePartName)) + { + // + // If 'AdornedTemplatePartName' is set to a valid string then search the visual-tree + // for a UI element that has the specified part name. If we find it then use it as the + // adorned control, otherwise throw an exception. + // + adornedControl = FindNamedChild(this, this.AdornedTemplatePartName); + if (adornedControl == null) + { + throw new ApplicationException("Failed to find a FrameworkElement in the visual-tree with the part name '" + this.AdornedTemplatePartName + "'."); + } + } + + this.adorner = new FrameworkElementAdorner(this.AdornerContent, adornedControl, + this.HorizontalAdornerPlacement, this.VerticalAdornerPlacement, + this.AdornerOffsetX, this.AdornerOffsetY); + this.adornerLayer.Add(this.adorner); + + UpdateAdornerDataContext(); + } + } + + this.adornerShowState = AdornerShowState.Visible; + } + + /// + /// Internal method to hide the adorner. + /// + private void HideAdornerInternal() + { + if (this.adornerLayer == null || this.adorner == null) + { + // Not already adorned. + return; + } + + // + // Stop the timer that might be about to fade out the adorner. + // + closeAdornerTimer.Stop(); + this.adornerLayer.Remove(this.adorner); + this.adorner.DisconnectChild(); + + this.adorner = null; + this.adornerLayer = null; + + // + // Ensure that the state of the adorned control reflects that + // the the adorner is no longer. + // + this.adornerShowState = AdornerShowState.Hidden; + } + + /// + /// Called to build the visual tree. + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + ShowOrHideAdornerInternal(); + } + + /// + /// Called when the mouse cursor enters the area of the adorned control. + /// + protected override void OnMouseEnter(MouseEventArgs e) + { + base.OnMouseEnter(e); + + MouseEnterLogic(); + } + + /// + /// Called when the mouse cursor leaves the area of the adorned control. + /// + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + + MouseLeaveLogic(); + } + + /// + /// Shared mouse enter code. + /// + private void MouseEnterLogic() + { + if (!IsMouseOverShowEnabled) + { + return; + } + + closeAdornerTimer.Stop(); + + FadeInAdorner(); + } + + /// + /// Shared mouse leave code. + /// + private void MouseLeaveLogic() + { + if (!IsMouseOverShowEnabled) + { + return; + } + + closeAdornerTimer.Start(); + } + + /// + /// Called when the close adorner time-out has ellapsed, the mouse has moved + /// away from the adorned control and the adorner and it is time to close the adorner. + /// + private void closeAdornerTimer_Tick(object sender, EventArgs e) + { + closeAdornerTimer.Stop(); + + FadeOutAdorner(); + } + + /// + /// Event raised when the fade in animation has completed. + /// + private void fadeInAnimation_Completed(object sender, EventArgs e) + { + adornerShowState = AdornerShowState.Visible; + } + + /// + /// Event raised when the fade-out animation has completed. + /// + private void fadeOutAnimation_Completed(object sender, EventArgs e) + { + if (adornerShowState == AdornerShowState.FadingOut) + { + // Still fading out, eg it wasn't aborted. + this.HideAdorner(); + } + } + + #endregion Private/Internal Functions + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/AdornedControl/AdornerPlacement.cs b/VideoBrowser/Controls/AdornedControl/AdornerPlacement.cs new file mode 100644 index 0000000..166f12f --- /dev/null +++ b/VideoBrowser/Controls/AdornedControl/AdornerPlacement.cs @@ -0,0 +1,22 @@ +namespace VideoBrowser.Controls.AdornedControl +{ + #region Enums + + /// + /// Specifies the placement of the adorner in related to the adorned control. + /// + public enum AdornerPlacement + { + /// + /// Defines the Inside. + /// + Inside, + + /// + /// Defines the Outside. + /// + Outside + } + + #endregion Enums +} \ No newline at end of file diff --git a/VideoBrowser/Controls/AdornedControl/FrameworkElementAdorner.cs b/VideoBrowser/Controls/AdornedControl/FrameworkElementAdorner.cs new file mode 100644 index 0000000..ef3c114 --- /dev/null +++ b/VideoBrowser/Controls/AdornedControl/FrameworkElementAdorner.cs @@ -0,0 +1,334 @@ +// +// This code based on code available here: +// +// http://www.codeproject.com/KB/WPF/WPFJoshSmith.aspx +// +namespace VideoBrowser.Controls.AdornedControl +{ + using System; + using System.Collections; + using System.Windows; + using System.Windows.Documents; + using System.Windows.Media; + + // + // This class is an adorner that allows a FrameworkElement derived class to adorn another FrameworkElement. + // + public class FrameworkElementAdorner : Adorner + { + // + // The framework element that is the adorner. + // + private FrameworkElement child; + + // + // Placement of the child. + // + private readonly AdornerPlacement horizontalAdornerPlacement = AdornerPlacement.Inside; + + private readonly AdornerPlacement verticalAdornerPlacement = AdornerPlacement.Inside; + + // + // Offset of the child. + // + private readonly double offsetX = 0.0; + + private readonly double offsetY = 0.0; + + // + // Position of the child (when not set to NaN). + // + private double positionX = Double.NaN; + + private double positionY = Double.NaN; + + public FrameworkElementAdorner(FrameworkElement adornerChildElement, FrameworkElement adornedElement) + : base(adornedElement) + { + this.child = adornerChildElement; + + base.AddLogicalChild(adornerChildElement); + base.AddVisualChild(adornerChildElement); + } + + public FrameworkElementAdorner(FrameworkElement adornerChildElement, FrameworkElement adornedElement, + AdornerPlacement horizontalAdornerPlacement, AdornerPlacement verticalAdornerPlacement, + double offsetX, double offsetY) + : base(adornedElement) + { + this.child = adornerChildElement; + this.horizontalAdornerPlacement = horizontalAdornerPlacement; + this.verticalAdornerPlacement = verticalAdornerPlacement; + this.offsetX = offsetX; + this.offsetY = offsetY; + + adornedElement.SizeChanged += new SizeChangedEventHandler(adornedElement_SizeChanged); + + base.AddLogicalChild(adornerChildElement); + base.AddVisualChild(adornerChildElement); + } + + /// + /// Event raised when the adorned control's size has changed. + /// + private void adornedElement_SizeChanged(object sender, SizeChangedEventArgs e) + { + InvalidateMeasure(); + } + + // + // Position of the child (when not set to NaN). + // + public double PositionX + { + get + { + return positionX; + } + set + { + positionX = value; + } + } + + public double PositionY + { + get + { + return positionY; + } + set + { + positionY = value; + } + } + + protected override Size MeasureOverride(Size constraint) + { + this.child.Measure(constraint); + return this.child.DesiredSize; + } + + /// + /// Determine the X coordinate of the child. + /// + private double DetermineX() + { + switch (child.HorizontalAlignment) + { + case HorizontalAlignment.Left: + { + if (horizontalAdornerPlacement == AdornerPlacement.Outside) + { + return -child.DesiredSize.Width + offsetX; + } + else + { + return offsetX; + } + } + case HorizontalAlignment.Right: + { + if (horizontalAdornerPlacement == AdornerPlacement.Outside) + { + double adornedWidth = AdornedElement.ActualWidth; + return adornedWidth + offsetX; + } + else + { + double adornerWidth = this.child.DesiredSize.Width; + double adornedWidth = AdornedElement.ActualWidth; + double x = adornedWidth - adornerWidth; + return x + offsetX; + } + } + case HorizontalAlignment.Center: + { + double adornerWidth = this.child.DesiredSize.Width; + double adornedWidth = AdornedElement.ActualWidth; + double x = (adornedWidth / 2) - (adornerWidth / 2); + return x + offsetX; + } + case HorizontalAlignment.Stretch: + { + return 0.0; + } + } + + return 0.0; + } + + /// + /// Determine the Y coordinate of the child. + /// + private double DetermineY() + { + switch (child.VerticalAlignment) + { + case VerticalAlignment.Top: + { + if (verticalAdornerPlacement == AdornerPlacement.Outside) + { + return -child.DesiredSize.Height + offsetY; + } + else + { + return offsetY; + } + } + case VerticalAlignment.Bottom: + { + if (verticalAdornerPlacement == AdornerPlacement.Outside) + { + double adornedHeight = AdornedElement.ActualHeight; + return adornedHeight + offsetY; + } + else + { + double adornerHeight = this.child.DesiredSize.Height; + double adornedHeight = AdornedElement.ActualHeight; + double x = adornedHeight - adornerHeight; + return x + offsetY; + } + } + case VerticalAlignment.Center: + { + double adornerHeight = this.child.DesiredSize.Height; + double adornedHeight = AdornedElement.ActualHeight; + double x = (adornedHeight / 2) - (adornerHeight / 2); + return x + offsetY; + } + case VerticalAlignment.Stretch: + { + return 0.0; + } + } + + return 0.0; + } + + /// + /// Determine the width of the child. + /// + private double DetermineWidth() + { + if (!Double.IsNaN(PositionX)) + { + return this.child.DesiredSize.Width; + } + + switch (child.HorizontalAlignment) + { + case HorizontalAlignment.Left: + { + return this.child.DesiredSize.Width; + } + case HorizontalAlignment.Right: + { + return this.child.DesiredSize.Width; + } + case HorizontalAlignment.Center: + { + return this.child.DesiredSize.Width; + } + case HorizontalAlignment.Stretch: + { + return AdornedElement.ActualWidth; + } + } + + return 0.0; + } + + /// + /// Determine the height of the child. + /// + private double DetermineHeight() + { + if (!Double.IsNaN(PositionY)) + { + return this.child.DesiredSize.Height; + } + + switch (child.VerticalAlignment) + { + case VerticalAlignment.Top: + { + return this.child.DesiredSize.Height; + } + case VerticalAlignment.Bottom: + { + return this.child.DesiredSize.Height; + } + case VerticalAlignment.Center: + { + return this.child.DesiredSize.Height; + } + case VerticalAlignment.Stretch: + { + return AdornedElement.ActualHeight; + } + } + + return 0.0; + } + + protected override Size ArrangeOverride(Size finalSize) + { + double x = PositionX; + if (Double.IsNaN(x)) + { + x = DetermineX(); + } + double y = PositionY; + if (Double.IsNaN(y)) + { + y = DetermineY(); + } + double adornerWidth = DetermineWidth(); + double adornerHeight = DetermineHeight(); + this.child.Arrange(new Rect(x, y, adornerWidth, adornerHeight)); + return finalSize; + } + + protected override Int32 VisualChildrenCount + { + get { return 1; } + } + + protected override Visual GetVisualChild(Int32 index) + { + return this.child; + } + + protected override IEnumerator LogicalChildren + { + get + { + ArrayList list = new ArrayList(); + list.Add(this.child); + return list.GetEnumerator(); + } + } + + /// + /// Disconnect the child element from the visual tree so that it may be reused later. + /// + public void DisconnectChild() + { + base.RemoveLogicalChild(child); + base.RemoveVisualChild(child); + } + + /// + /// Override AdornedElement from base class for less type-checking. + /// + public new FrameworkElement AdornedElement + { + get + { + return (FrameworkElement)base.AdornedElement; + } + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/AirspaceFixer/AirspacePanel.cs b/VideoBrowser/Controls/AirspaceFixer/AirspacePanel.cs new file mode 100644 index 0000000..4ba9247 --- /dev/null +++ b/VideoBrowser/Controls/AirspaceFixer/AirspacePanel.cs @@ -0,0 +1,184 @@ +namespace VideoBrowser.Controls.AirspaceFixer +{ + using System; + using System.Drawing.Imaging; + using System.IO; + using System.Runtime.InteropServices; + using System.Threading.Tasks; + using System.Windows; + using System.Windows.Controls; + using System.Windows.Media; + using System.Windows.Media.Imaging; + + /// + /// Defines the + /// + public class AirspacePanel : ContentControl + { + #region Fields + + public static readonly DependencyProperty FixAirspaceProperty = + DependencyProperty.Register(nameof(FixAirspace), + typeof(bool), + typeof(AirspacePanel), + new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnFixAirspaceChanged))); + + private ContentControl _airspaceContent; + + private Image _airspaceScreenshot; + + private float _scalingFactor; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public AirspacePanel() + { + Loaded += (_, __) => GetScalingFactor(); + } + + /// + /// Initializes static members of the class. + /// + static AirspacePanel() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AirspacePanel), new FrameworkPropertyMetadata(typeof(AirspacePanel))); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets a value indicating whether FixAirspace + /// + public bool FixAirspace + { + get { return (bool)GetValue(FixAirspaceProperty); } + set { SetValue(FixAirspaceProperty, value); } + } + + #endregion Properties + + #region Methods + + /// + /// The GetImageSourceFromBitmap + /// + /// The bitmap + /// The + public ImageSource GetImageSourceFromBitmap(System.Drawing.Bitmap bitmap) + { + using (var memory = new MemoryStream()) + { + bitmap.Save(memory, ImageFormat.Bmp); + memory.Position = 0; + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.StreamSource = memory; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + return bitmapImage; + } + } + + /// + /// The OnApplyTemplate + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _airspaceContent = GetTemplateChild("PART_AirspaceContent") as ContentControl; + _airspaceScreenshot = GetTemplateChild("PART_AirspaceScreenshot") as Image; + } + + // https://stackoverflow.com/questions/5977445/how-to-get-windows-display-settings + /// + /// The GetDeviceCaps + /// + /// The hdc + /// The nIndex + /// The + [DllImport("gdi32.dll")] + internal static extern int GetDeviceCaps(IntPtr hdc, int nIndex); + + /// + /// The OnFixAirspaceChanged + /// + /// The d + /// The e + private static async void OnFixAirspaceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var panel = d as AirspacePanel; + if (panel == null || panel.ActualWidth == 0 || panel.ActualHeight == 0 || PresentationSource.FromVisual(panel) == null) + { + return; + } + + if ((bool)e.NewValue) + { + panel.CreateScreenshotFromContent(); + panel._airspaceScreenshot.Visibility = Visibility.Visible; + await Task.Delay(300); + panel._airspaceContent.Visibility = Visibility.Hidden; + } + else + { + panel._airspaceContent.Visibility = Visibility.Visible; + panel._airspaceScreenshot.Visibility = Visibility.Hidden; + panel._airspaceScreenshot.Source = null; + } + } + + /// + /// The CreateScreenshotFromContent + /// + private void CreateScreenshotFromContent() + { + var source = PresentationSource.FromVisual(this); + + var scalingX = source.CompositionTarget.TransformToDevice.M11; + var scalingY = source.CompositionTarget.TransformToDevice.M22; + + var upperLeftPoint = _airspaceContent.PointToScreen(new Point(0, 0)); + var bounds = new System.Drawing.Rectangle((int)(upperLeftPoint.X * _scalingFactor), + (int)(upperLeftPoint.Y * _scalingFactor), + (int)(_airspaceContent.RenderSize.Width * scalingX), + (int)(_airspaceContent.RenderSize.Height * scalingY)); + + using (var bitmap = new System.Drawing.Bitmap(bounds.Width, bounds.Height)) + { + using (var g = System.Drawing.Graphics.FromImage(bitmap)) + { + g.CopyFromScreen(new System.Drawing.Point(bounds.Left, bounds.Top), + System.Drawing.Point.Empty, + new System.Drawing.Size(bounds.Width, bounds.Height)); + } + + _airspaceScreenshot.Source = GetImageSourceFromBitmap(bitmap); + } + } + + /// + /// The GetScalingFactor + /// + private void GetScalingFactor() + { + var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero); + var desktop = g.GetHdc(); + var LogicalScreenHeight = GetDeviceCaps(desktop, 10); + var PhysicalScreenHeight = GetDeviceCaps(desktop, 117); + + var ScreenScalingFactor = PhysicalScreenHeight / (float)LogicalScreenHeight; + + _scalingFactor = ScreenScalingFactor; // 1.25 = 125% + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/AirspaceFixer/Themes/AirspacePanel.xaml b/VideoBrowser/Controls/AirspaceFixer/Themes/AirspacePanel.xaml new file mode 100644 index 0000000..311b68e --- /dev/null +++ b/VideoBrowser/Controls/AirspaceFixer/Themes/AirspacePanel.xaml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/AirspaceFixer/Themes/Generic.xaml b/VideoBrowser/Controls/AirspaceFixer/Themes/Generic.xaml new file mode 100644 index 0000000..b543a6e --- /dev/null +++ b/VideoBrowser/Controls/AirspaceFixer/Themes/Generic.xaml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/AddInButton.cs b/VideoBrowser/Controls/CefSharpBrowser/AddInButton.cs new file mode 100644 index 0000000..367c5f4 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/AddInButton.cs @@ -0,0 +1,100 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using System.ComponentModel; + using System.Windows.Input; + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public abstract class AddInButton : NotifyPropertyChanged + { + #region Fields + + private Geometry _icon; + + private bool _isEnabled = true; + + private string _toolTip; + + private bool isVisible; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The name. + protected AddInButton(string name = null) + { + if (name == null) + { + name = this.GetType().Name; + } + + this.Name = name; + this.Command = new RelayCommand(this.OnExecute, $"{this.Name} add in is executed"); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the Command. + /// + public ICommand Command { get; } + + /// + /// Gets or sets the Icon. + /// + public Geometry Icon { get => _icon; set => this.Set(this.PropertyChangedHandler, ref _icon, value); } + + /// + /// Gets or sets a value indicating whether IsEnabled. + /// + public bool IsEnabled { get => _isEnabled; set => this.Set(this.PropertyChangedHandler, ref _isEnabled, value); } + + /// + /// Gets or sets a value indicating whether IsVisible. + /// + public bool IsVisible { get => isVisible; set => this.Set(this.PropertyChangedHandler, ref isVisible, value); } + + /// + /// Gets or sets the Name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the ToolTip. + /// + public string ToolTip { get => _toolTip; set => this.Set(this.PropertyChangedHandler, ref _toolTip, value); } + + #endregion Properties + + #region Methods + + /// + /// The Execute. + /// + /// The viewModel. + protected abstract void Execute(WebBrowserTabControlViewModel viewModel); + + /// + /// The OnExecute. + /// + /// The obj. + private void OnExecute(object obj) + { + var viewModel = obj as WebBrowserTabControlViewModel; + this.Execute(viewModel); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/AddIns/BookmarkAddIn.cs b/VideoBrowser/Controls/CefSharpBrowser/AddIns/BookmarkAddIn.cs new file mode 100644 index 0000000..1d34d5e --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/AddIns/BookmarkAddIn.cs @@ -0,0 +1,85 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.AddIns +{ + using System.Collections.Generic; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Controls.CefSharpBrowser.Resources; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class BookmarkAddIn : AddInButton + { + #region Fields + + private string url; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal BookmarkAddIn() + { + this.Icon = BrowserIcons.StarWF; + this.ToolTip = "Bookmark this page"; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the BookmarkModels. + /// + public IList BookmarkModels { get; set; } + + /// + /// Gets or sets the Url. + /// + public string Url + { + get => url; + set + { + if (!this.Set(this.PropertyChangedHandler, ref url, value)) + { + return; + } + + this.IsVisible = !string.IsNullOrEmpty(this.Url); + var bookmarkExist = false; + if (this.BookmarkModels != null) + { + foreach (var bookmark in this.BookmarkModels) + { + if (bookmark.Url == this.Url) + { + bookmarkExist = true; + break; + } + } + } + + this.Icon = bookmarkExist ? BrowserIcons.Star : BrowserIcons.StarWF; + } + } + + #endregion Properties + + #region Methods + + /// + /// The Execute. + /// + /// The viewModel. + protected override void Execute(WebBrowserTabControlViewModel viewModel) + { + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/AddIns/IsSecureAddIn.cs b/VideoBrowser/Controls/CefSharpBrowser/AddIns/IsSecureAddIn.cs new file mode 100644 index 0000000..99894b6 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/AddIns/IsSecureAddIn.cs @@ -0,0 +1,49 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.AddIns +{ + using VideoBrowser.Controls.CefSharpBrowser.Resources; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class IsSecureAddIn : AddInButton + { + private string _url; + + public IsSecureAddIn() + { + this.Icon = BrowserIcons.Lock; + this.ToolTip = "The connection is secure"; + } + + /// + /// Gets or sets the Url. + /// + public string Url + { + get => _url; + set + { + if (!this.Set(this.PropertyChangedHandler, ref _url, value) || string.IsNullOrEmpty(this.Url)) + { + return; + } + + this.IsVisible = this.Url.Contains("https://"); + } + } + + #region Methods + + /// + /// The Execute. + /// + /// The viewModel. + protected override void Execute(WebBrowserTabControlViewModel viewModel) + { + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/BrowserSettings.cs b/VideoBrowser/Controls/CefSharpBrowser/BrowserSettings.cs new file mode 100644 index 0000000..ac3316c --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/BrowserSettings.cs @@ -0,0 +1,48 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using System.Collections.Generic; + using VideoBrowser.Controls.CefSharpBrowser.Models; + + /// + /// Defines the . + /// + public class BrowserSettings + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public BrowserSettings() + { + this.BookmarkModels = new List(); + this.TabSettingModels = new List(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the BookmarkModels. + /// + public IList BookmarkModels { get; set; } + + /// + /// Gets or sets the SelectedTabSettingIndex. + /// + public int SelectedTabSettingIndex { get; set; } + + /// + /// Gets or sets the TabSettingModels. + /// + public IList TabSettingModels { get; set; } + + /// + /// Gets or sets the Version. + /// + public string Version { get; set; } = "1.0"; + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/CefSharpBrowser.xaml b/VideoBrowser/Controls/CefSharpBrowser/CefSharpBrowser.xaml new file mode 100644 index 0000000..8413a8e --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/CefSharpBrowser.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/CefSharpBrowser.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/CefSharpBrowser.xaml.cs new file mode 100644 index 0000000..407792b --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/CefSharpBrowser.xaml.cs @@ -0,0 +1,300 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using CefSharp; + using CefSharp.Wpf; + using System.Windows; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Handlers; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + using VideoBrowser.Helpers; + + /// + /// Interaction logic for CefSharpBrowser.xaml. + /// + public partial class CefSharpBrowser + { + #region Fields + + public static readonly DependencyProperty BackwardCommandProperty = + DependencyProperty.Register(nameof(BackwardCommand), typeof(ICommand), typeof(CefSharpBrowser), new PropertyMetadata(null)); + + public static readonly DependencyProperty ForwardCommandProperty = + DependencyProperty.Register(nameof(ForwardCommand), typeof(ICommand), typeof(CefSharpBrowser), new PropertyMetadata(null)); + + public static readonly DependencyProperty IsAirspaceVisibleProperty = + DependencyProperty.Register(nameof(IsAirspaceVisible), typeof(bool), typeof(CefSharpBrowser), new PropertyMetadata(false)); + + public static readonly DependencyProperty IsFullScreenCommandProperty = + DependencyProperty.Register(nameof(IsFullScreenCommand), typeof(ICommand), typeof(CefSharpBrowser), new PropertyMetadata(null, OnIsFullScreenCommandChanged)); + + public static readonly DependencyProperty ReloadCommandProperty = + DependencyProperty.Register(nameof(ReloadCommand), typeof(ICommand), typeof(CefSharpBrowser), new PropertyMetadata(null)); + + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register(nameof(Title), typeof(string), typeof(CefSharpBrowser), new FrameworkPropertyMetadata("Home", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + public static readonly DependencyProperty UrlProperty = + DependencyProperty.Register(nameof(Url), typeof(string), typeof(CefSharpBrowser), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnUrlChanged)); + + public static readonly DependencyProperty WebBrowserProperty = + DependencyProperty.Register(nameof(WebBrowser), typeof(IWebBrowser), typeof(CefSharpBrowser), new PropertyMetadata(null)); + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public CefSharpBrowser() + { + Initialize(); + this.CefDisplayHandler = new CefDisplayHandler(); + this.InitializeComponent(); + this.ChromiumWebBrowser.TitleChanged += this.OnChromiumWebBrowser_TitleChanged; + this.Loaded += this.OnLoaded; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the BackwardCommand. + /// + public ICommand BackwardCommand + { + get { return (ICommand)GetValue(BackwardCommandProperty); } + set { SetValue(BackwardCommandProperty, value); } + } + + /// + /// Gets the CefSettings. + /// + public CefSettings CefSettings { get; } + + /// + /// Gets or sets the ForwardCommand. + /// + public ICommand ForwardCommand + { + get { return (ICommand)GetValue(ForwardCommandProperty); } + set { SetValue(ForwardCommandProperty, value); } + } + + /// + /// Gets or sets a value indicating whether IsAirspaceVisible. + /// + public bool IsAirspaceVisible + { + get { return (bool)GetValue(IsAirspaceVisibleProperty); } + set { SetValue(IsAirspaceVisibleProperty, value); } + } + + /// + /// Gets or sets the IsFullScreenCommand. + /// + public ICommand IsFullScreenCommand + { + get { return (ICommand)GetValue(IsFullScreenCommandProperty); } + set { SetValue(IsFullScreenCommandProperty, value); } + } + + /// + /// Gets or sets the ReloadCommand. + /// + public ICommand ReloadCommand + { + get { return (ICommand)GetValue(ReloadCommandProperty); } + set { SetValue(ReloadCommandProperty, value); } + } + + /// + /// Gets or sets the Title. + /// + public string Title + { + get { return (string)GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + + /// + /// Gets or sets the Url. + /// + public string Url + { + get { return (string)GetValue(UrlProperty); } + set { SetValue(UrlProperty, value); } + } + + /// + /// Gets or sets the WebBrowser. + /// + public IWebBrowser WebBrowser + { + get { return (IWebBrowser)this.GetValue(WebBrowserProperty); } + set { this.SetValue(WebBrowserProperty, value); } + } + + /// + /// Gets or sets a value indicating whether CanBackward. + /// + private bool CanBackward { get; set; } + + /// + /// Gets or sets a value indicating whether CanForward. + /// + private bool CanForward { get; set; } + + /// + /// Gets or sets a value indicating whether CanReload. + /// + private bool CanReload { get; set; } + + /// + /// Gets the CefDisplayHandler. + /// + private CefDisplayHandler CefDisplayHandler { get; } + + #endregion Properties + + #region Methods + + /// + /// The Initialize. + /// + public static void Initialize() + { + if (!Cef.IsInitialized) + { + System.AppContext.SetSwitch("Switch.System.Windows.Input.Stylus.EnablePointerSupport", true); + const bool multiThreadedMessageLoop = true; + var browserProcessHandler = new BrowserProcessHandler(); + var settings = new CefSettings + { + //UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0", + MultiThreadedMessageLoop = multiThreadedMessageLoop, + ExternalMessagePump = !multiThreadedMessageLoop + }; + + settings.PersistSessionCookies = true; + settings.SetOffScreenRenderingBestPerformanceArgs(); + settings.CefCommandLineArgs.Remove("disable-gpu-compositing"); + CefConfig.Init(settings, browserProcessHandler: browserProcessHandler); + } + } + + /// + /// The OnIsFullScreenCommandChanged. + /// + /// The d. + /// The e. + private static void OnIsFullScreenCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var browser = (CefSharpBrowser)d; + var isFullScreenCommand = (ICommand)e.NewValue; + browser.CefDisplayHandler.IsFullScreenCommand = isFullScreenCommand; + } + + /// + /// The OnUrlChanged. + /// + /// The d. + /// The e. + private static void OnUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UIThreadHelper.InvokeAsync(() => + { + CommandManager.InvalidateRequerySuggested(); + }); + } + + /// + /// The OnBackward. + /// + /// The obj. + private void OnBackward(object obj) + { + if (this.WebBrowser.CanGoBack) + { + this.WebBrowser.Back(); + } + } + + /// + /// The OnChromiumWebBrowser_TitleChanged. + /// + /// The sender. + /// The e. + private void OnChromiumWebBrowser_TitleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + UIThreadHelper.InvokeAsync(() => this.Title = (string)e.NewValue); + } + + /// + /// The OnForward. + /// + /// The obj. + private void OnForward(object obj) + { + if (this.WebBrowser.CanGoForward) + { + this.WebBrowser.Forward(); + } + } + + /// + /// The OnLoaded. + /// + /// The sender. + /// The e. + private void OnLoaded(object sender, RoutedEventArgs e) + { + this.Loaded -= this.OnLoaded; + this.WebBrowser = this.ChromiumWebBrowser; + this.WebBrowser.LoadingStateChanged += OnWebBrowser_LoadingStateChanged; + this.WebBrowser.LoadError += OnWebBrowser_LoadError; + this.WebBrowser.DisplayHandler = this.CefDisplayHandler; + this.WebBrowser.KeyboardHandler = new CefKeyboardHandler(this.ChromiumWebBrowser); + this.BackwardCommand = new RelayCommand(this.OnBackward, "Backward", (o) => this.CanBackward); + this.ForwardCommand = new RelayCommand(this.OnForward, "Forward", (o) => this.CanForward); + this.ReloadCommand = new RelayCommand(this.OnReload, "Reload", (o) => this.CanReload); + } + + /// + /// The OnReload. + /// + /// The obj. + private void OnReload(object obj) + { + this.WebBrowser.Reload(true); + } + + /// + /// The OnWebBrowser_LoadError. + /// + /// The sender. + /// The e. + private void OnWebBrowser_LoadError(object sender, LoadErrorEventArgs e) + { + } + + /// + /// The OnWebBrowser_LoadingStateChanged. + /// + /// The sender. + /// The e. + private void OnWebBrowser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) + { + UIThreadHelper.Invoke(() => + { + this.CanBackward = e.CanGoBack; + this.CanForward = e.CanGoForward; + this.CanReload = e.CanReload; + }); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/CefWindowData.cs b/VideoBrowser/Controls/CefSharpBrowser/CefWindowData.cs new file mode 100644 index 0000000..ec4cd9e --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/CefWindowData.cs @@ -0,0 +1,144 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using MahApps.Metro.Controls; + using MahApps.Metro.Controls.Dialogs; + using System; + using System.ComponentModel; + using System.Threading.Tasks; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Handlers; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class CefWindowData : INotifyPropertyChanged + { + #region Fields + + private bool _isAirspaceVisible; + + private bool _isFullScreen; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal CefWindowData() + { + this.IsFullScreenCommand = new RelayCommand(this.OnIsFullScreen); + this.CefContextMenuHandler = new CefContextMenuHandler(); + this.CefRequestHandler = new CefRequestHandler(); + } + + #endregion Constructors + + #region Events + + /// + /// Defines the PropertyChanged. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion Events + + #region Properties + + /// + /// Gets the CefContextMenuHandler. + /// + public CefContextMenuHandler CefContextMenuHandler { get; } + + /// + /// Gets the CefRequestHandler. + /// + public CefRequestHandler CefRequestHandler { get; } + + /// + /// Gets or sets a value indicating whether IsAirspaceVisible. + /// + public bool IsAirspaceVisible + { + get => this._isAirspaceVisible; + set + { + if (this.IsMessageBoxVisible) + { + return; + } + + this.Set(this.PropertyChanged, ref this._isAirspaceVisible, value); + } + } + + /// + /// Gets or sets a value indicating whether IsFullScreen. + /// + public bool IsFullScreen { get => _isFullScreen; set => this.Set(this.PropertyChanged, ref _isFullScreen, value); } + + /// + /// Gets the IsFullScreenCommand. + /// + public ICommand IsFullScreenCommand { get; } + + /// + /// Gets a value indicating whether IsMessageBoxVisible. + /// + public bool IsMessageBoxVisible { get; private set; } + + /// + /// Gets or sets the MainWindow. + /// + public MetroWindow MainWindow { get; internal set; } + + #endregion Properties + + #region Methods + + /// + /// The ShowMessage. + /// + /// The title. + /// The message. + /// The style. + /// The . + internal async Task ShowMessageAsync(string title, string message, MessageDialogStyle style = MessageDialogStyle.Affirmative) + { + var currentAirspaceVisible = this.IsAirspaceVisible; + this.IsAirspaceVisible = true; + this.IsMessageBoxVisible = true; + var dispatcher = this.MainWindow.Dispatcher; + Task result = null; + if (dispatcher.CheckAccess()) + { + result = this.MainWindow.ShowMessageAsync(title, message, style); + } + else + { + await dispatcher.BeginInvoke((Action)(() => result = this.MainWindow.ShowMessageAsync(title, message, style))); + } + + await result; + + this.IsMessageBoxVisible = false; + this.IsAirspaceVisible = false; + + return result.Result; + } + + /// + /// The OnIsFullScreen. + /// + /// The obj. + private void OnIsFullScreen(object obj) + { + this.IsFullScreen = (bool)obj; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Converters/StringToImageConverter.cs b/VideoBrowser/Controls/CefSharpBrowser/Converters/StringToImageConverter.cs new file mode 100644 index 0000000..e3ab044 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Converters/StringToImageConverter.cs @@ -0,0 +1,63 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Converters +{ + using System; + using System.Globalization; + using System.Windows.Data; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + + /// + /// Defines the . + /// + public class StringToImageConverter : IValueConverter + { + #region Properties + + /// + /// Gets the Instance. + /// + public static StringToImageConverter Instance { get; } = new StringToImageConverter(); + + #endregion Properties + + #region Methods + + /// + /// The Convert. + /// + /// The value. + /// The targetType. + /// The parameter. + /// The culture. + /// The . + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(value is string imageString) || string.IsNullOrEmpty(imageString)) + { + return null; + } + + if (imageString.IsImageUrl()) + { + return imageString; + } + + var imageSource = imageString.FindIconForFilename(true); + return imageSource; + } + + /// + /// The ConvertBack. + /// + /// The value. + /// The targetType. + /// The parameter. + /// The culture. + /// The . + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/CreateTabAddInButton.cs b/VideoBrowser/Controls/CefSharpBrowser/CreateTabAddInButton.cs new file mode 100644 index 0000000..e89055a --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/CreateTabAddInButton.cs @@ -0,0 +1,81 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using System; + using System.Windows; + using System.Windows.Media; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public abstract class CreateTabAddInButton : AddInButton + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The title. + /// The icon. + /// The name. + public CreateTabAddInButton(string title, Geometry icon, string name = null) : base(name) + { + this.Title = title; + this.Icon = icon; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the Guid. + /// + public Guid Guid { get; } = Guid.NewGuid(); + + /// + /// Gets or sets the Title. + /// + public string Title { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The CreateView. + /// This method is already in UI Thread. + /// + /// The . + protected abstract UIElement CreateView(); + + /// + /// The Execute. + /// + /// The viewModel. + protected override void Execute(WebBrowserTabControlViewModel viewModel) + { + if (viewModel.IsTabItemExist(this.Guid)) + { + viewModel.SetActiveTab(this.Guid); + return; + } + + UIThreadHelper.InvokeAsync(() => + { + var view = this.CreateView(); + var tab = new TabItem(this.Guid) + { + Content = view, + Icon = this.Icon, + Title = this.Title + }; + + viewModel.AddTab(tab); + }); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/GlobalBrowserData.cs b/VideoBrowser/Controls/CefSharpBrowser/GlobalBrowserData.cs new file mode 100644 index 0000000..9c4fd75 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/GlobalBrowserData.cs @@ -0,0 +1,117 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.ComponentModel; + using System.IO; + using System.Linq; + using VideoBrowser.Controls.CefSharpBrowser.Handlers; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Extensions; + using VideoBrowser.ViewModels; + + /// + /// Defines the . + /// All singleton instances are saved here. + /// + public class GlobalBrowserData : IDisposable + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal GlobalBrowserData() + { + this.InterTabClient = new InterTabClient(this); + this.Settings = new SettingsViewModel(); + this.Settings.PropertyChanged += this.OnSettings_PropertyChanged; + this.DownloadHandler = new DownloadHandler(this.DownloadItemModels) { DownloadPath = this.Settings.OutputFolder }; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the AddInButtons. + /// + public ICollection AddInButtons { get; } = new ObservableCollection(); + + /// + /// Gets the BrowserSettings. + /// + public BrowserSettings BrowserSettings => this.Settings.BrowserSettings; + + /// + /// Gets the DownloadHandler. + /// + public DownloadHandler DownloadHandler { get; } + + /// + /// Gets the DownloadItemModels. + /// + public ObservableCollection DownloadItemModels { get; } = new ObservableCollection(); + + /// + /// Gets the InterTabClient. + /// + public InterTabClient InterTabClient { get; } + + /// + /// Gets the Settings. + /// + public SettingsViewModel Settings { get; } + + /// + /// Gets or sets a value indicating whether IsSettingsLoaded. + /// + internal bool IsSettingsLoaded { get; set; } + + /// + /// Gets the WindowViewModels. + /// + internal IList WindowViewModels { get; } = new List(); + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + public void Dispose() + { + this.Settings.PropertyChanged -= this.OnSettings_PropertyChanged; + this.DownloadHandler.Dispose(); + } + + /// + /// The GetAddInButton. + /// + /// The addInType. + /// The . + public AddInButton GetAddInButton(Type addInType) + { + var addIn = this.AddInButtons.FirstOrDefault(o => o.GetType() == addInType); + return addIn; + } + + /// + /// The OnSettings_PropertyChanged. + /// + /// The sender. + /// The e. + private void OnSettings_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.IsMatch(nameof(this.Settings.OutputFolder)) && Directory.Exists(this.Settings.OutputFolder)) + { + this.DownloadHandler.DownloadPath = this.Settings.OutputFolder; + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefContextMenuHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefContextMenuHandler.cs new file mode 100644 index 0000000..c48b527 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefContextMenuHandler.cs @@ -0,0 +1,373 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Handlers +{ + using CefSharp; + using CefSharp.Wpf; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Windows; + using System.Windows.Controls; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + + /// + /// Defines the . + /// + public class CefContextMenuHandler : IContextMenuHandler + { + #region Constants + + private const CefMenuCommand CopyLinkAdressId = (CefMenuCommand)10000; + + private const CefMenuCommand OpenImageInNewTabId = (CefMenuCommand)10001; + + private const CefMenuCommand OpenInNewTabId = (CefMenuCommand)10002; + + private const CefMenuCommand OpenInNewWindowId = (CefMenuCommand)10003; + + private const CefMenuCommand SearchInWebsiteId = (CefMenuCommand)10004; + + #endregion Constants + + #region Properties + + /// + /// Gets or sets the OpenInNewTabAction. + /// + public Action OpenInNewTabAction { get; set; } + + /// + /// Gets or sets the OpenInNewWindowAction. + /// + public Action OpenInNewWindowAction { get; set; } + + /// + /// Gets or sets the SearchEngineQuery. + /// + public string SearchEngineQuery { get; set; } = "https://www.youtube.com/results?search_query="; + + /// + /// Gets the CopyLinkAddress. + /// + private static (CefMenuCommand Id, string Text) CopyLinkAddress { get; } = (CopyLinkAdressId, "Copy link address"); + + /// + /// Gets the OpenImageInNewTab. + /// + private static (CefMenuCommand Id, string Text) OpenImageInNewTab { get; } = (OpenImageInNewTabId, "Open image in new tab"); + + /// + /// Gets the OpenInNewTab. + /// + private static (CefMenuCommand Id, string Text) OpenInNewTab { get; } = (OpenInNewTabId, "Open in new tab"); + + /// + /// Gets the OpenInNewWindow. + /// + private static (CefMenuCommand Id, string Text) OpenInNewWindow { get; } = (OpenInNewWindowId, "Open in new window"); + + /// + /// Gets the SearchInWebsite. + /// + private static (CefMenuCommand Id, string Text) SearchInWebsite { get; } = (SearchInWebsiteId, "Search in Youtube"); + + #endregion Properties + + #region Methods + + /// + /// The OnBeforeContextMenu. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The frame. + /// The parameters. + /// The model. + public virtual void OnBeforeContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) + { + var linkExist = !string.IsNullOrEmpty(parameters.LinkUrl); + var selectionTextExist = !string.IsNullOrEmpty(parameters.SelectionText); + var sourceUrlExist = !string.IsNullOrEmpty(parameters.SourceUrl); + + if (linkExist || selectionTextExist) + { + model.AddSeparator(); + } + + ////model.Clear(); + if (linkExist) + { + this.AddItem(model, CopyLinkAddress.Id, CopyLinkAddress.Text); + this.AddItem(model, OpenInNewTab.Id, OpenInNewTab.Text); + this.AddItem(model, OpenInNewWindow.Id, OpenInNewWindow.Text); + } + + if (sourceUrlExist && (parameters.SourceUrl != parameters.PageUrl)) + { + if (parameters.SourceUrl.IsImageUrl()) + { + this.AddItem(model, OpenImageInNewTab.Id, OpenImageInNewTab.Text); + } + } + + if (selectionTextExist) + { + this.AddItem(model, SearchInWebsite.Id, SearchInWebsite.Text); + } + } + + /// + /// The OnContextMenuCommand. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The frame. + /// The parameters. + /// The commandId. + /// The eventFlags. + /// The . + public virtual bool OnContextMenuCommand(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) + { + return true; + } + + /// + /// The OnContextMenuDismissed. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The frame. + public virtual void OnContextMenuDismissed(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame) + { + var webBrowser = (ChromiumWebBrowser)chromiumWebBrowser; + webBrowser.Dispatcher.Invoke(() => + { + webBrowser.ContextMenu = null; + }); + } + + /// + /// The GetMenuItems. + /// + /// The model. + /// The . + private static IEnumerable> GetMenuItems(IMenuModel model) + { + for (var i = 0; i < model.Count; i++) + { + var header = model.GetLabelAt(i); + var commandId = model.GetCommandIdAt(i); + var isEnabled = model.IsEnabledAt(i); + yield return new Tuple(header, commandId, isEnabled); + } + } + + /// + /// The AddItem. + /// + /// The model. + /// The id. + /// The label. + private void AddItem(IMenuModel model, CefMenuCommand id, string label) + { + model.AddItem(id, label); + model.SetEnabled(id, true); + } + + /// + /// The RunContextMenu. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The frame. + /// The parameters. + /// The model. + /// The callback. + /// The . + bool IContextMenuHandler.RunContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) + { + //NOTE: Return false to use the built in Context menu - in WPF this requires you integrate into your existing message loop, read the General Usage Guide for more details + //https://github.com/cefsharp/CefSharp/wiki/General-Usage#multithreadedmessageloop + //return false; + + var webBrowser = (ChromiumWebBrowser)chromiumWebBrowser; + + //IMenuModel is only valid in the context of this method, so need to read the values before invoking on the UI thread + var menuItems = GetMenuItems(model).ToList(); + var linkUrl = parameters.LinkUrl; + var sourceUrl = parameters.SourceUrl; + var selectionText = parameters.SelectionText; + + webBrowser.Dispatcher.Invoke(() => + { + var menu = new ContextMenu + { + IsOpen = true + }; + + RoutedEventHandler handler = null; + + handler = (s, e) => + { + menu.Closed -= handler; + + //If the callback has been disposed then it's already been executed + //so don't call Cancel + if (!callback.IsDisposed) + { + callback.Cancel(); + } + }; + + menu.Closed += handler; + + foreach (var item in menuItems) + { + if (item.Item2 == CefMenuCommand.NotFound && string.IsNullOrWhiteSpace(item.Item1)) + { + menu.Items.Add(new Separator()); + continue; + } + + menu.Items.Add(new MenuItem + { + Header = item.Item1.Replace("&", "_"), + IsEnabled = item.Item3, + Command = new RelayCommand((o) => + { + //BUG: CEF currently not executing callbacks correctly so we manually map the commands below + //see https://github.com/cefsharp/CefSharp/issues/1767 + //The following line worked in previous versions, it doesn't now, so custom EXAMPLE below + //callback.Continue(item.Item2, CefEventFlags.None); + + //NOTE: Note all menu item options below have been tested, you can work out the rest + switch (item.Item2) + { + ////case CefMenuCommand.Copy: + //// Clipboard.SetText(parameters.SelectionText); + //// break; + + ////case CefMenuCommand.Paste: + //// chromiumWebBrowser.Paste(); + //// break; + + case CopyLinkAdressId: + Clipboard.SetText(linkUrl); + break; + + case OpenImageInNewTabId: + this.OpenInNewTabAction?.Invoke(sourceUrl); + break; + + case OpenInNewWindowId: + this.OpenInNewWindowAction?.Invoke(linkUrl); + break; + + case OpenInNewTabId: + this.OpenInNewTabAction?.Invoke(linkUrl); + break; + + case SearchInWebsiteId: + var searchUrl = $"{this.SearchEngineQuery}{selectionText}"; + this.OpenInNewTabAction?.Invoke(searchUrl); + break; + + case CefMenuCommand.Back: + { + browser.GoBack(); + break; + } + case CefMenuCommand.Forward: + { + browser.GoForward(); + break; + } + case CefMenuCommand.Cut: + { + browser.FocusedFrame.Cut(); + break; + } + + case CefMenuCommand.Copy: + { + browser.FocusedFrame.Copy(); + break; + } + case CefMenuCommand.Paste: + { + browser.FocusedFrame.Paste(); + break; + } + case CefMenuCommand.Print: + { + browser.GetHost().Print(); + break; + } + case CefMenuCommand.ViewSource: + { + browser.FocusedFrame.ViewSource(); + break; + } + case CefMenuCommand.Undo: + { + browser.FocusedFrame.Undo(); + break; + } + case CefMenuCommand.StopLoad: + { + browser.StopLoad(); + break; + } + case CefMenuCommand.SelectAll: + { + browser.FocusedFrame.SelectAll(); + break; + } + case CefMenuCommand.Redo: + { + browser.FocusedFrame.Redo(); + break; + } + case CefMenuCommand.Find: + { + browser.GetHost().Find(parameters.SelectionText, true, false, false); + break; + } + case CefMenuCommand.AddToDictionary: + { + browser.GetHost().AddWordToDictionary(parameters.MisspelledWord); + break; + } + case CefMenuCommand.Reload: + { + browser.Reload(); + break; + } + case CefMenuCommand.ReloadNoCache: + { + browser.Reload(ignoreCache: true); + break; + } + case (CefMenuCommand)26501: + { + browser.GetHost().ShowDevTools(); + break; + } + case (CefMenuCommand)26502: + { + browser.GetHost().CloseDevTools(); + break; + } + } + }, "") + }); + } + webBrowser.ContextMenu = menu; + }); + + return true; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefDisplayHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefDisplayHandler.cs new file mode 100644 index 0000000..e6b1ea8 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefDisplayHandler.cs @@ -0,0 +1,171 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Handlers +{ + using CefSharp; + using CefSharp.Structs; + using CefSharp.Wpf; + using System; + using System.Collections.Generic; + using System.Windows.Input; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class CefDisplayHandler : IDisplayHandler + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal CefDisplayHandler() + { + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the IsFullScreenCommand. + /// + internal ICommand IsFullScreenCommand { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The OnAutoResize. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The newSize. + /// The . + public bool OnAutoResize(IWebBrowser chromiumWebBrowser, IBrowser browser, Size newSize) + { + return true; + } + + /// + /// The OnCursorChange. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The cursor. + /// The type. + /// The customCursorInfo. + /// The . + public bool OnCursorChange(IWebBrowser chromiumWebBrowser, IBrowser browser, IntPtr cursor, CefSharp.Enums.CursorType type, CursorInfo customCursorInfo) + { + return false; + } + + /// + /// The OnLoadingProgressChange. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The progress. + public void OnLoadingProgressChange(IWebBrowser chromiumWebBrowser, IBrowser browser, double progress) + { + } + + /// + /// The OnTooltipChanged. + /// + /// The chromiumWebBrowser. + /// The text. + /// The . + public bool OnTooltipChanged(IWebBrowser chromiumWebBrowser, ref string text) + { + return false; + } + + /// + /// The OnAddressChanged. + /// + /// The browserControl. + /// The addressChangedArgs. + void IDisplayHandler.OnAddressChanged(IWebBrowser browserControl, AddressChangedEventArgs addressChangedArgs) + { + } + + /// + /// The OnConsoleMessage. + /// + /// The browserControl. + /// The consoleMessageArgs. + /// The . + bool IDisplayHandler.OnConsoleMessage(IWebBrowser browserControl, ConsoleMessageEventArgs consoleMessageArgs) + { + return false; + } + + /// + /// The OnFaviconUrlChange. + /// + /// The browserControl. + /// The browser. + /// The urls. + void IDisplayHandler.OnFaviconUrlChange(IWebBrowser browserControl, IBrowser browser, IList urls) + { + } + + /// + /// The OnFullscreenModeChange. + /// + /// The browserControl. + /// The browser. + /// The fullscreen. + void IDisplayHandler.OnFullscreenModeChange(IWebBrowser browserControl, IBrowser browser, bool fullscreen) + { + var chromiumWebBrowser = (ChromiumWebBrowser)browserControl; + chromiumWebBrowser.Invoke(() => + { + this.IsFullScreenCommand.Execute(fullscreen); + ////if (fullscreen) + ////{ + //// parent = chromiumWebBrowser.Parent; + //// parent.Controls.Remove(chromiumWebBrowser); + + //// fullScreenForm = new Form + //// { + //// FormBorderStyle = FormBorderStyle.None, + //// WindowState = FormWindowState.Maximized + //// }; + //// fullScreenForm.Controls.Add(chromiumWebBrowser); + //// fullScreenForm.ShowDialog(parent.FindForm()); + ////} + ////else + ////{ + //// fullScreenForm.Controls.Remove(chromiumWebBrowser); + //// parent.Controls.Add(chromiumWebBrowser); + //// fullScreenForm.Close(); + //// fullScreenForm.Dispose(); + //// fullScreenForm = null; + ////} + }); + } + + /// + /// The OnStatusMessage. + /// + /// The browserControl. + /// The statusMessageArgs. + void IDisplayHandler.OnStatusMessage(IWebBrowser browserControl, StatusMessageEventArgs statusMessageArgs) + { + } + + /// + /// The OnTitleChanged. + /// + /// The browserControl. + /// The titleChangedArgs. + void IDisplayHandler.OnTitleChanged(IWebBrowser browserControl, TitleChangedEventArgs titleChangedArgs) + { + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefKeyboardHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefKeyboardHandler.cs new file mode 100644 index 0000000..093d832 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefKeyboardHandler.cs @@ -0,0 +1,113 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Handlers +{ + using CefSharp; + using CefSharp.Wpf; + using System.Windows; + using System.Windows.Forms; + using System.Windows.Input; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class CefKeyboardHandler : IKeyboardHandler + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The host. + internal CefKeyboardHandler(FrameworkElement host) + { + this.WebBrowser = host; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the WebBrowser. + /// + public FrameworkElement WebBrowser { get; } + + /// + /// Gets or sets a value indicating whether IsFullScreen. + /// + private bool IsFullScreen { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The OnKeyEvent. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The type. + /// The windowsKeyCode. + /// The nativeKeyCode. + /// The modifiers. + /// The isSystemKey. + /// The . + public bool OnKeyEvent(IWebBrowser chromiumWebBrowser, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey) + { + return false; + + return this.WebBrowser.Dispatcher.Invoke(() => + { + var window = Window.GetWindow(this.WebBrowser); + var routedEvent = UIElement.KeyDownEvent; + var kb = Keyboard.PrimaryDevice; + var ps = PresentationSource.FromDependencyObject(this.WebBrowser); + var ts = 0; + var key = KeyInterop.KeyFromVirtualKey(windowsKeyCode); + + var e = new System.Windows.Input.KeyEventArgs(kb, ps, ts, key) + { + RoutedEvent = routedEvent + }; + + // WPF gets modifiers from PrimaryKeyboard only + System.Diagnostics.Debug.WriteLine("Raising {0} {1}+{{{2}}}", routedEvent, key, Keyboard.Modifiers); + this.WebBrowser.RaiseEvent(e); + return e.Handled; + }); + } + + /// + /// The OnPreKeyEvent. + /// + /// The browserControl. + /// The browser. + /// The type. + /// The windowsKeyCode. + /// The nativeKeyCode. + /// The modifiers. + /// The isSystemKey. + /// The isKeyboardShortcut. + /// The . + public bool OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut) + { + var chromiumWebBrowser = (ChromiumWebBrowser)browserControl; + if ((Keys)windowsKeyCode == Keys.Escape) + { + chromiumWebBrowser.Invoke(delegate + { + ////var screenSize = Screen.FromControl(chromiumWebBrowser).Bounds.Size; + ////bool fullScreen = screenSize == chromiumWebBrowser.Size; + ////if (fullScreen) + ////{ + //// chromiumWebBrowser.DisplayHandler.OnFullscreenModeChange(browserControl, browser, false); + ////} + }); + } + + return false; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefRequestHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefRequestHandler.cs new file mode 100644 index 0000000..4c0042d --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Handlers/CefRequestHandler.cs @@ -0,0 +1,41 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Handlers +{ + using CefSharp; + using CefSharp.Handler; + using System; + + /// + /// Defines the . + /// + public class CefRequestHandler : RequestHandler + { + #region Properties + + /// + /// Gets or sets the OpenUrlFromTabAction. + /// + internal Action OpenUrlFromTabAction { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The OnOpenUrlFromTab. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The frame. + /// The targetUrl. + /// The targetDisposition. + /// The userGesture. + /// The . + protected override bool OnOpenUrlFromTab(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) + { + this.OpenUrlFromTabAction?.Invoke(targetUrl); + return true; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Handlers/DownloadHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Handlers/DownloadHandler.cs new file mode 100644 index 0000000..47e5333 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Handlers/DownloadHandler.cs @@ -0,0 +1,172 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Handlers +{ + using CefSharp; + using Ookii.Dialogs.Wpf; + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.IO; + using System.Windows; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class DownloadHandler : IDownloadHandler, IDisposable + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The downloadItemModels. + public DownloadHandler(IList downloadItemModels) + { + this.DownloadItemModels = downloadItemModels; + this.DownloadItemDict = new ConcurrentDictionary(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets a value indicating whether Disposed. + /// + public bool Disposed { get; private set; } + + /// + /// Gets the DownloadItemDict. + /// + public IDictionary DownloadItemDict { get; } + + /// + /// Gets the DownloadItemModels. + /// + public IList DownloadItemModels { get; } + + /// + /// Gets or sets the DownloadPath. + /// + public string DownloadPath { get; set; } = AppEnvironment.UserVideoFolder; + + /// + /// Gets or sets a value indicating whether IsShowDialog. + /// + public bool IsShowDialog { get; set; } = true; + + /// + /// Gets or sets a value indicating whether IsCancelAllDownloads. + /// + private bool IsCancelAllDownloads { get; set; } + + /// + /// Gets the Lock. + /// + private object Lock { get; } = new object(); + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + public void Dispose() + { + if (this.Disposed) + { + return; + } + + this.Disposed = true; + } + + /// + /// The OnBeforeDownload. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The downloadItem. + /// The callback. + public void OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback) + { + lock (this.Lock) + { + if (this.DownloadItemDict.ContainsKey(downloadItem.Id) || callback.IsDisposed) + { + return; + } + + UIThreadHelper.Invoke(() => + { + using (callback) + { + var fileName = downloadItem.SuggestedFileName; + var filePath = Path.Combine(this.DownloadPath, downloadItem.SuggestedFileName); + if (this.IsShowDialog) + { + var dialog = new VistaSaveFileDialog + { + FileName = fileName, + CheckPathExists = true, + InitialDirectory = this.DownloadPath, + OverwritePrompt = true, + Title = "Save Link to...", + }; + + var element = chromiumWebBrowser as FrameworkElement; + + var window = Window.GetWindow(element); + if (!(bool)dialog.ShowDialog(window)) + { + return; + } + + filePath = dialog.FileName; + } + + this.DownloadPath = Path.GetDirectoryName(filePath); + var model = new DownloadProcessModel(downloadItem); + this.DownloadItemDict.Add(downloadItem.Id, model); + this.DownloadItemModels.Insert(0, model); + callback.Continue(filePath, false); + } + }); + } + } + + /// + /// The OnDownloadUpdated. + /// + /// The chromiumWebBrowser. + /// The browser. + /// The downloadItem. + /// The callback. + public void OnDownloadUpdated(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IDownloadItemCallback callback) + { + lock (this.Lock) + { + var id = downloadItem.Id; + if (!this.DownloadItemDict.TryGetValue(id, out DownloadProcessModel processModel)) + { + return; + } + + if (processModel.IsCanceled || this.IsCancelAllDownloads) + { + this.DownloadItemDict.Remove(id); + this.DownloadItemModels.Remove(processModel); + callback.Cancel(); + return; + } + + processModel.UpdateInfo(downloadItem); + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/ApplicationIconHelper.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/ApplicationIconHelper.cs new file mode 100644 index 0000000..ae3a1a1 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/ApplicationIconHelper.cs @@ -0,0 +1,235 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.IO; + using System.Runtime.InteropServices; + using System.Windows; + using System.Windows.Interop; + using System.Windows.Media; + using System.Windows.Media.Imaging; + + /// + /// Internals are mostly from here: http://www.codeproject.com/Articles/2532/Obtaining-and-managing-file-and-folder-icons-using + /// Caches all results. + /// + public static class ApplicationIconHelper + { + #region Fields + + private static readonly Dictionary _largeIconCache = new Dictionary(); + + private static readonly Dictionary _smallIconCache = new Dictionary(); + + #endregion Fields + + #region Methods + + /// + /// Get an icon for a given filename. + /// + /// any filename. + /// 16x16 or 32x32 icon. + /// null if path is null, otherwise - an icon. + public static ImageSource FindIconForFilename(this string fileName, bool large) + { + var extension = Path.GetExtension(fileName); + if (extension == null) + { + return null; + } + + var cache = large ? _largeIconCache : _smallIconCache; + ImageSource icon; + if (cache.TryGetValue(extension, out icon)) + { + return icon; + } + + icon = IconReader.GetFileIcon(fileName, large ? IconReader.IconSize.Large : IconReader.IconSize.Small, false).ToImageSource(); + cache.Add(extension, icon); + return icon; + } + + /// + /// http://stackoverflow.com/a/6580799/1943849. + /// + /// The icon. + /// The . + private static ImageSource ToImageSource(this Icon icon) + { + var imageSource = Imaging.CreateBitmapSourceFromHIcon( + icon.Handle, + Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + return imageSource; + } + + #endregion Methods + + /// + /// Provides static methods to read system icons for both folders and files. + /// + private static class IconReader + { + #region Enums + + /// + /// Options to specify the size of icons to return. + /// + public enum IconSize + { + /// + /// Specify large icon - 32 pixels by 32 pixels. + /// + Large = 0, + + /// + /// Specify small icon - 16 pixels by 16 pixels. + /// + Small = 1 + } + + #endregion Enums + + #region Methods + + /// + /// Returns an icon for a given file - indicated by the name parameter. + /// + /// Pathname for file. + /// Large or small. + /// Whether to include the link icon. + /// System.Drawing.Icon. + public static Icon GetFileIcon(string name, IconSize size, bool linkOverlay) + { + var shfi = new Shell32.Shfileinfo(); + var flags = Shell32.ShgfiIcon | Shell32.ShgfiUsefileattributes; + if (linkOverlay) + { + flags += Shell32.ShgfiLinkoverlay; + } + + /* Check the size specified for return. */ + if (IconSize.Small == size) + { + flags += Shell32.ShgfiSmallicon; + } + else + { + flags += Shell32.ShgfiLargeicon; + } + + Shell32.SHGetFileInfo(name, + Shell32.FileAttributeNormal, + ref shfi, + (uint)Marshal.SizeOf(shfi), + flags); + // Copy (clone) the returned icon to a new object, thus allowing us to clean-up properly + var icon = (Icon)Icon.FromHandle(shfi.hIcon).Clone(); + User32.DestroyIcon(shfi.hIcon); // Cleanup + return icon; + } + + #endregion Methods + } + + /// + /// Wraps necessary Shell32.dll structures and functions required to retrieve Icon Handles using SHGetFileInfo. Code + /// courtesy of MSDN Cold Rooster Consulting case study. + /// + private static class Shell32 + { + #region Constants + + public const uint FileAttributeNormal = 0x00000080; + + public const uint ShgfiIcon = 0x000000100;// get icon + + public const uint ShgfiLargeicon = 0x000000000;// get large icon + + public const uint ShgfiLinkoverlay = 0x000008000;// put a link overlay on icon + + public const uint ShgfiSmallicon = 0x000000001;// get small icon + + public const uint ShgfiUsefileattributes = 0x000000010;// use passed dwFileAttribute + + private const int MaxPath = 256; + + #endregion Constants + + #region Methods + + /// + /// The SHGetFileInfo. + /// + /// The pszPath. + /// The dwFileAttributes. + /// The psfi. + /// The cbFileInfo. + /// The uFlags. + /// The . + [DllImport("Shell32.dll")] + public static extern IntPtr SHGetFileInfo( + string pszPath, + uint dwFileAttributes, + ref Shfileinfo psfi, + uint cbFileInfo, + uint uFlags + ); + + #endregion Methods + + /// + /// Defines the . + /// + [StructLayout(LayoutKind.Sequential)] + public struct Shfileinfo + { + #region Constants + + private const int Namesize = 80; + + #endregion Constants + + #region Fields + + public readonly IntPtr hIcon; + + private readonly uint dwAttributes; + + private readonly int iIcon; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MaxPath)] + private readonly string szDisplayName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = Namesize)] + private readonly string szTypeName; + + #endregion Fields + } + +; + } + + /// + /// Wraps necessary functions imported from User32.dll. Code courtesy of MSDN Cold Rooster Consulting example. + /// + private static class User32 + { + #region Methods + + /// + /// Provides access to function required to delete handle. This method is used internally + /// and is not required to be called separately. + /// + /// Pointer to icon handle. + /// N/A. + [DllImport("User32.dll")] + public static extern int DestroyIcon(IntPtr hIcon); + + #endregion Methods + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/BrowserProcessHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/BrowserProcessHandler.cs new file mode 100644 index 0000000..88a2635 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/BrowserProcessHandler.cs @@ -0,0 +1,149 @@ +// Copyright © 2016 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using CefSharp; + using System; + using System.Diagnostics; + using System.Threading.Tasks; + + /// + /// Defines the . + /// + public class BrowserProcessHandler : IBrowserProcessHandler + { + #region Constants + + /// + /// The interval between calls to Cef.DoMessageLoopWork + /// + protected const int SixtyTimesPerSecond = 1000 / 60;// 60fps + + /// + /// The maximum number of milliseconds we're willing to wait between calls to OnScheduleMessagePumpWork(). + /// + protected const int ThirtyTimesPerSecond = 1000 / 30;//30fps + + #endregion Constants + + #region Methods + + /// + /// The Dispose. + /// + public virtual void Dispose() + { + } + + /// + /// The OnScheduleMessagePumpWork. + /// + /// The delay. + protected virtual void OnScheduleMessagePumpWork(int delay) + { + } + + /// + /// The OnContextInitialized. + /// + void IBrowserProcessHandler.OnContextInitialized() + { + //The Global CookieManager has been initialized, you can now set cookies + var cookieManager = Cef.GetGlobalCookieManager(); + ////cookieManager.SetSupportedSchemes(new string[] { "custom" }, true); + if (cookieManager.SetCookie("custom://cefsharp/home.html", new Cookie + { + Name = "CefSharpTestCookie", + Value = "ILikeCookies", + Expires = DateTime.Now.AddDays(1) + })) + { + cookieManager.VisitUrlCookiesAsync("custom://cefsharp/home.html", false).ContinueWith(previous => + { + if (previous.Status == TaskStatus.RanToCompletion) + { + var cookies = previous.Result; + + foreach (var cookie in cookies) + { + Debug.WriteLine("CookieName: " + cookie.Name); + } + } + else + { + Debug.WriteLine("No Cookies found"); + } + }); + + cookieManager.VisitAllCookiesAsync().ContinueWith(t => + { + if (t.Status == TaskStatus.RanToCompletion) + { + var cookies = t.Result; + + foreach (var cookie in cookies) + { + Debug.WriteLine("CookieName: " + cookie.Name); + } + } + else + { + Debug.WriteLine("No Cookies found"); + } + }); + } + + //The Request Context has been initialized, you can now set preferences, like proxy server settings + //Dispose of context when finished - preferable not to keep a reference if possible. + using (var context = Cef.GetGlobalRequestContext()) + { + string errorMessage; + //You can set most preferences using a `.` notation rather than having to create a complex set of dictionaries. + //The default is true, you can change to false to disable + context.SetPreference("webkit.webprefs.plugins_enabled", true, out errorMessage); + + //string error; + //var dicts = new List { "en-GB", "en-US" }; + //var success = context.SetPreference("spellcheck.dictionaries", dicts, out error); + + //The no-proxy-server flag is set in CefExample.cs class, you'll have to remove that before you can test + //this code out + //var v = new Dictionary + //{ + // ["mode"] = "fixed_servers", + // ["server"] = "scheme://host:port" + //}; + //success = context.SetPreference("proxy", v, out errorMessage); + + //It's possible to register a scheme handler for the default http and https schemes + //In this example we register the FolderSchemeHandlerFactory for https://cefsharp.example + //Best to include the domain name, so only requests for that domain are forwarded to your scheme handler + //It is possible to intercept all requests for a scheme, including the built in http/https ones, be very careful doing this! + ////var folderSchemeHandlerExample = new FolderSchemeHandlerFactory(rootFolder: @"..\..\..\..\CefSharp.Example\Resources", + //// hostName: "cefsharp.example", //Optional param no hostname checking if null + //// defaultPage: "home.html"); //Optional param will default to index.html + + ////context.RegisterSchemeHandlerFactory("https", "cefsharp.example", folderSchemeHandlerExample); + } + } + + /// + /// The OnScheduleMessagePumpWork. + /// + /// The delay. + void IBrowserProcessHandler.OnScheduleMessagePumpWork(long delay) + { + //If the delay is greater than the Maximum then use ThirtyTimesPerSecond + //instead - we do this to achieve a minimum number of FPS + if (delay > ThirtyTimesPerSecond) + { + delay = ThirtyTimesPerSecond; + } + OnScheduleMessagePumpWork((int)delay); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/BrowserSettingsHelper.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/BrowserSettingsHelper.cs new file mode 100644 index 0000000..bb28a93 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/BrowserSettingsHelper.cs @@ -0,0 +1,126 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + using System; + using System.IO; + using System.Reflection; + using System.Windows; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + + /// + /// Defines the . + /// + internal static class BrowserSettingsHelper + { + #region Constructors + + /// + /// Initializes static members of the class. + /// + static BrowserSettingsHelper() + { + var appName = Assembly.GetExecutingAssembly().GetName().Name; + var userAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + UserJsonSettingsPath = Path.Combine(userAppDataPath, $@"{appName}\BrowserSettings.json"); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the UserJsonSettingsPath. + /// + public static string UserJsonSettingsPath { get; } + + #endregion Properties + + #region Methods + + /// + /// The Load. + /// + /// The . + internal static BrowserSettings Load() + { + Logger.Info($"Loading Browser setting {UserJsonSettingsPath}"); + if (!File.Exists(UserJsonSettingsPath)) + { + return null; + } + + try + { + using (StreamReader file = File.OpenText(UserJsonSettingsPath)) + { + JsonSerializer serializer = new JsonSerializer(); + var browserSettings = (BrowserSettings)serializer.Deserialize(file, typeof(BrowserSettings)); + return browserSettings; + } + } + catch (Exception e) + { + Logger.Error($"Error Loading Browser Setting: {e.Message}"); + } + + return null; + } + + /// + /// The Save. + /// + /// The settings. + /// The browserModel. + internal static void Save(BrowserSettings settings, WebBrowserTabControlViewModel browserModel) + { + settings.BookmarkModels.Clear(); + settings.DownloadItems.Clear(); + settings.TabSettingModels.Clear(); + foreach (var tabItem in browserModel.TabItems) + { + if (tabItem.Content is FrameworkElement element && element.DataContext is VideoBrowserViewModel videoModel) + { + var tabModel = new TabSettingsModel + { + Title = videoModel.Header, + Url = videoModel.Url + }; + + settings.TabSettingModels.Add(tabModel); + } + } + + foreach (var downloadItem in browserModel.GlobalBrowserData.DownloadItemModels) + { + if (downloadItem.Status == "Completed") + { + settings.DownloadItems.Add(new DownloadItemModel(downloadItem)); + } + } + + settings.SelectedTabSettingIndex = browserModel.SelectedTabIndex; + var settingsFolder = Path.GetDirectoryName(UserJsonSettingsPath); + if (!Directory.Exists(settingsFolder)) + { + Directory.CreateDirectory(settingsFolder); + } + + var serializer = new JsonSerializer(); + serializer.Converters.Add(new JavaScriptDateTimeConverter()); + serializer.NullValueHandling = NullValueHandling.Ignore; + + using (var sw = new StreamWriter(UserJsonSettingsPath)) + using (var writer = new JsonTextWriter(sw)) + { + serializer.Serialize(writer, settings); + } + + Logger.Info($"Browser setting is saved in {UserJsonSettingsPath}"); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefConfig.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefConfig.cs new file mode 100644 index 0000000..339ccca --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefConfig.cs @@ -0,0 +1,300 @@ +// Copyright © 2014 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using CefSharp; + using System; + using System.Diagnostics; + using System.IO; + using System.Text; + + public static class CefConfig + { + //TODO: Revert after https://bitbucket.org/chromiumembedded/cef/issues/2685/networkservice-custom-scheme-unable-to + //has been fixed. + public const string ExampleDomain = "cefsharp.example"; + + public const string BaseUrl = "https://" + ExampleDomain; + public const string DefaultUrl = BaseUrl + "/home.html"; + public const string BindingTestUrl = BaseUrl + "/BindingTest.html"; + public const string BindingTestSingleUrl = BaseUrl + "/BindingTestSingle.html"; + public const string BindingTestsAsyncTaskUrl = BaseUrl + "/BindingTestsAsyncTask.html"; + public const string LegacyBindingTestUrl = BaseUrl + "/LegacyBindingTest.html"; + public const string PostMessageTestUrl = BaseUrl + "/PostMessageTest.html"; + public const string PluginsTestUrl = BaseUrl + "/plugins.html"; + public const string PopupTestUrl = BaseUrl + "/PopupTest.html"; + public const string TooltipTestUrl = BaseUrl + "/TooltipTest.html"; + public const string BasicSchemeTestUrl = BaseUrl + "/SchemeTest.html"; + public const string ResponseFilterTestUrl = BaseUrl + "/ResponseFilterTest.html"; + public const string DraggableRegionTestUrl = BaseUrl + "/DraggableRegionTest.html"; + public const string DragDropCursorsTestUrl = BaseUrl + "/DragDropCursorsTest.html"; + public const string CssAnimationTestUrl = BaseUrl + "/CssAnimationTest.html"; + public const string CdmSupportTestUrl = BaseUrl + "/CdmSupportTest.html"; + public const string BindingApiCustomObjectNameTestUrl = BaseUrl + "/BindingApiCustomObjectNameTest.html"; + public const string TestResourceUrl = "http://test/resource/load"; + public const string RenderProcessCrashedUrl = "http://processcrashed"; + public const string TestUnicodeResourceUrl = "http://test/resource/loadUnicode"; + public const string PopupParentUrl = "http://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_win_close"; + + // Use when debugging the actual SubProcess, to make breakpoints etc. inside that project work. + private static readonly bool DebuggingSubProcess = Debugger.IsAttached; + + private static string PluginInformation = ""; + + public static void Init(CefSettingsBase settings, IBrowserProcessHandler browserProcessHandler) + { + // Set Google API keys, used for Geolocation requests sans GPS. See http://www.chromium.org/developers/how-tos/api-keys + // Environment.SetEnvironmentVariable("GOOGLE_API_KEY", ""); + // Environment.SetEnvironmentVariable("GOOGLE_DEFAULT_CLIENT_ID", ""); + // Environment.SetEnvironmentVariable("GOOGLE_DEFAULT_CLIENT_SECRET", ""); + + // Widevine CDM registration - pass in directory where Widevine CDM binaries and manifest.json are located. + // For more information on support for DRM content with Widevine see: https://github.com/cefsharp/CefSharp/issues/1934 + //Cef.RegisterWidevineCdm(@".\WidevineCdm"); + + //Chromium Command Line args + //http://peter.sh/experiments/chromium-command-line-switches/ + //NOTE: Not all relevant in relation to `CefSharp`, use for reference purposes only. + //CEF specific command line args + //https://bitbucket.org/chromiumembedded/cef/src/master/libcef/common/cef_switches.cc?fileviewer=file-view-default + //IMPORTANT: For enabled/disabled command line arguments like disable-gpu specifying a value of "0" like + //settings.CefCommandLineArgs.Add("disable-gpu", "0"); will have no effect as the second argument is ignored. + + settings.RemoteDebuggingPort = 8088; + //The location where cache data will be stored on disk. If empty an in-memory cache will be used for some features and a temporary disk cache for others. + //HTML5 databases such as localStorage will only persist across sessions if a cache path is specified. + settings.RootCachePath = Path.GetFullPath("cache"); + //If non-null then CachePath must be equal to or a child of RootCachePath + //We're using a sub folder. + // + settings.CachePath = Path.GetFullPath("cache\\global"); + //settings.UserAgent = "CefSharp Browser" + Cef.CefSharpVersion; // Example User Agent + //settings.CefCommandLineArgs.Add("renderer-process-limit", "1"); + //settings.CefCommandLineArgs.Add("renderer-startup-dialog"); + //settings.CefCommandLineArgs.Add("enable-media-stream"); //Enable WebRTC + //settings.CefCommandLineArgs.Add("no-proxy-server"); //Don't use a proxy server, always make direct connections. Overrides any other proxy server flags that are passed. + //settings.CefCommandLineArgs.Add("debug-plugin-loading"); //Dumps extra logging about plugin loading to the log file. + //settings.CefCommandLineArgs.Add("disable-plugins-discovery"); //Disable discovering third-party plugins. Effectively loading only ones shipped with the browser plus third-party ones as specified by --extra-plugin-dir and --load-plugin switches + //settings.CefCommandLineArgs.Add("enable-system-flash"); //Automatically discovered and load a system-wide installation of Pepper Flash. + //settings.CefCommandLineArgs.Add("allow-running-insecure-content"); //By default, an https page cannot run JavaScript, CSS or plugins from http URLs. This provides an override to get the old insecure behavior. Only available in 47 and above. + //https://peter.sh/experiments/chromium-command-line-switches/#disable-site-isolation-trials + //settings.CefCommandLineArgs.Add("disable-site-isolation-trials"); + + //settings.CefCommandLineArgs.Add("enable-logging"); //Enable Logging for the Renderer process (will open with a cmd prompt and output debug messages - use in conjunction with setting LogSeverity = LogSeverity.Verbose;) + //settings.LogSeverity = LogSeverity.Verbose; // Needed for enable-logging to output messages + + //settings.CefCommandLineArgs.Add("disable-extensions"); //Extension support can be disabled + //settings.CefCommandLineArgs.Add("disable-pdf-extension"); //The PDF extension specifically can be disabled + + //Load the pepper flash player that comes with Google Chrome - may be possible to load these values from the registry and query the dll for it's version info (Step 2 not strictly required it seems) + //settings.CefCommandLineArgs.Add("ppapi-flash-path", @"C:\Program Files (x86)\Google\Chrome\Application\47.0.2526.106\PepperFlash\pepflashplayer.dll"); //Load a specific pepper flash version (Step 1 of 2) + //settings.CefCommandLineArgs.Add("ppapi-flash-version", "20.0.0.228"); //Load a specific pepper flash version (Step 2 of 2) + + //Audo play example + //settings.CefCommandLineArgs["autoplay-policy"] = "no-user-gesture-required"; + + //NOTE: For OSR best performance you should run with GPU disabled: + // `--disable-gpu --disable-gpu-compositing --enable-begin-frame-scheduling` + // (you'll loose WebGL support but gain increased FPS and reduced CPU usage). + // http://magpcss.org/ceforum/viewtopic.php?f=6&t=13271#p27075 + //https://bitbucket.org/chromiumembedded/cef/commits/e3c1d8632eb43c1c2793d71639f3f5695696a5e8 + + //NOTE: The following function will set all three params + //settings.SetOffScreenRenderingBestPerformanceArgs(); + //settings.CefCommandLineArgs.Add("disable-gpu"); + //settings.CefCommandLineArgs.Add("disable-gpu-compositing"); + //settings.CefCommandLineArgs.Add("enable-begin-frame-scheduling"); + + //settings.CefCommandLineArgs.Add("disable-gpu-vsync"); //Disable Vsync + + // The following options control accessibility state for all frames. + // These options only take effect if accessibility state is not set by IBrowserHost.SetAccessibilityState call. + // --force-renderer-accessibility enables browser accessibility. + // --disable-renderer-accessibility completely disables browser accessibility. + //settings.CefCommandLineArgs.Add("force-renderer-accessibility"); + //settings.CefCommandLineArgs.Add("disable-renderer-accessibility"); + + //Enables Uncaught exception handler + settings.UncaughtExceptionStackSize = 10; + + //Disable WebAssembly + //settings.JavascriptFlags = "--noexpose_wasm"; + + // Off Screen rendering (WPF/Offscreen) + if (settings.WindowlessRenderingEnabled) + { + //Disable Direct Composition to test https://github.com/cefsharp/CefSharp/issues/1634 + //settings.CefCommandLineArgs.Add("disable-direct-composition"); + + // DevTools doesn't seem to be working when this is enabled + // http://magpcss.org/ceforum/viewtopic.php?f=6&t=14095 + //settings.CefCommandLineArgs.Add("enable-begin-frame-scheduling"); + } + + var proxy = ProxyConfig.GetProxyInformation(); + switch (proxy.AccessType) + { + case InternetOpenType.Direct: + { + //Don't use a proxy server, always make direct connections. + settings.CefCommandLineArgs.Add("no-proxy-server"); + break; + } + case InternetOpenType.Proxy: + { + settings.CefCommandLineArgs.Add("proxy-server", proxy.ProxyAddress); + break; + } + case InternetOpenType.PreConfig: + { + settings.CefCommandLineArgs.Add("proxy-auto-detect"); + break; + } + } + + //settings.LogSeverity = LogSeverity.Verbose; + + if (DebuggingSubProcess) + { + var architecture = Environment.Is64BitProcess ? "x64" : "x86"; + var path = Path.GetFullPath("CefSharp.BrowserSubprocess.exe"); + settings.BrowserSubprocessPath = path; + } + + settings.RegisterScheme(new CefCustomScheme + { + SchemeName = CefSharpSchemeHandlerFactory.SchemeName, + SchemeHandlerFactory = new CefSharpSchemeHandlerFactory(), + IsSecure = true, //treated with the same security rules as those applied to "https" URLs + }); + + settings.RegisterScheme(new CefCustomScheme + { + SchemeName = "https", + SchemeHandlerFactory = new CefSharpSchemeHandlerFactory(), + DomainName = ExampleDomain + }); + + settings.RegisterScheme(new CefCustomScheme + { + SchemeName = CefSharpSchemeHandlerFactory.SchemeNameTest, + SchemeHandlerFactory = new CefSharpSchemeHandlerFactory(), + IsSecure = true //treated with the same security rules as those applied to "https" URLs + }); + + //You can use the http/https schemes - best to register for a specific domain + settings.RegisterScheme(new CefCustomScheme + { + SchemeName = "https", + SchemeHandlerFactory = new CefSharpSchemeHandlerFactory(), + DomainName = "cefsharp.com", + IsSecure = true //treated with the same security rules as those applied to "https" URLs + }); + + ////settings.RegisterScheme(new CefCustomScheme + ////{ + //// SchemeName = "localfolder", + //// SchemeHandlerFactory = new FolderSchemeHandlerFactory(rootFolder: @"..\..\..\Resources", + //// schemeName: "localfolder", //Optional param no schemename checking if null + //// hostName: "cefsharp", //Optional param no hostname checking if null + //// defaultPage: "home.html") //Optional param will default to index.html + ////}); + + ////settings.RegisterExtension(new V8Extension("cefsharp/example", Resources.extension)); + + //This must be set before Cef.Initialized is called + CefSharpSettings.FocusedNodeChangedEnabled = true; + + //Async Javascript Binding - methods are queued on TaskScheduler.Default. + //Set this to true to when you have methods that return Task + //CefSharpSettings.ConcurrentTaskExecution = true; + + //Legacy Binding Behaviour - Same as Javascript Binding in version 57 and below + //See issue https://github.com/cefsharp/CefSharp/issues/1203 for details + //CefSharpSettings.LegacyJavascriptBindingEnabled = true; + + //Exit the subprocess if the parent process happens to close + //This is optional at the moment + //https://github.com/cefsharp/CefSharp/pull/2375/ + CefSharpSettings.SubprocessExitIfParentProcessClosed = true; + + //NOTE: Set this before any calls to Cef.Initialize to specify a proxy with username and password + //One set this cannot be changed at runtime. If you need to change the proxy at runtime (dynamically) then + //see https://github.com/cefsharp/CefSharp/wiki/General-Usage#proxy-resolution + //CefSharpSettings.Proxy = new ProxyOptions(ip: "127.0.0.1", port: "8080", username: "cefsharp", password: "123"); + + if (!Cef.Initialize(settings, performDependencyCheck: !DebuggingSubProcess, browserProcessHandler: browserProcessHandler)) + { + throw new Exception("Unable to Initialize Cef"); + } + + Cef.AddCrossOriginWhitelistEntry(BaseUrl, "https", "cefsharp.com", false); + } + + public static async void RegisterTestResources(IWebBrowser browser) + { + if (browser.ResourceRequestHandlerFactory == null) + { + browser.ResourceRequestHandlerFactory = new ResourceRequestHandlerFactory(); + } + + var handler = browser.ResourceRequestHandlerFactory as ResourceRequestHandlerFactory; + + if (handler != null) + { + const string renderProcessCrashedBody = "

Render Process Crashed

Your seeing this message as the render process has crashed

"; + handler.RegisterHandler(RenderProcessCrashedUrl, ResourceHandler.GetByteArray(renderProcessCrashedBody, Encoding.UTF8)); + + const string responseBody = "

Success

This document is loaded from a System.IO.Stream

"; + handler.RegisterHandler(TestResourceUrl, ResourceHandler.GetByteArray(responseBody, Encoding.UTF8)); + + const string unicodeResponseBody = "整体满意度"; + handler.RegisterHandler(TestUnicodeResourceUrl, ResourceHandler.GetByteArray(unicodeResponseBody, Encoding.UTF8)); + + if (string.IsNullOrEmpty(PluginInformation)) + { + var pluginBody = new StringBuilder(); + pluginBody.Append("

Plugins

"); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + + var plugins = await Cef.GetPlugins(); + + if (plugins.Count == 0) + { + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + } + else + { + foreach (var plugin in plugins) + { + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + pluginBody.Append(""); + } + } + + pluginBody.Append("
NameDescriptionVersionPath
Cef.GetPlugins returned an empty list - likely no plugins were loaded on your system
You may find that NPAPI/PPAPI need to be enabled
" + plugin.Name + "" + plugin.Description + "" + plugin.Version + "" + plugin.Path + "
"); + + PluginInformation = pluginBody.ToString(); + } + + handler.RegisterHandler(PluginsTestUrl, ResourceHandler.GetByteArray(PluginInformation, Encoding.UTF8)); + } + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefSharpSchemeHandler.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefSharpSchemeHandler.cs new file mode 100644 index 0000000..7aea5f9 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefSharpSchemeHandler.cs @@ -0,0 +1,70 @@ +// Copyright © 2012 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using CefSharp; + using System; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Threading.Tasks; + + internal class CefSharpSchemeHandler : ResourceHandler + { + public override CefReturnValue ProcessRequestAsync(IRequest request, ICallback callback) + { + var uri = new Uri(request.Url); + var fileName = uri.AbsolutePath; + + Task.Run(() => + { + using (callback) + { + Stream stream = null; + + if (string.Equals(fileName, "/PostDataTest.html", StringComparison.OrdinalIgnoreCase)) + { + var postDataElement = request.PostData.Elements.FirstOrDefault(); + stream = ResourceHandler.GetMemoryStream("Post Data: " + (postDataElement == null ? "null" : postDataElement.GetBody()), Encoding.UTF8); + } + + if (string.Equals(fileName, "/PostDataAjaxTest.html", StringComparison.OrdinalIgnoreCase)) + { + var postData = request.PostData; + if (postData == null) + { + stream = ResourceHandler.GetMemoryStream("Post Data: null", Encoding.UTF8); + } + else + { + var postDataElement = postData.Elements.FirstOrDefault(); + stream = ResourceHandler.GetMemoryStream("Post Data: " + (postDataElement == null ? "null" : postDataElement.GetBody()), Encoding.UTF8); + } + } + + if (stream == null) + { + callback.Cancel(); + } + else + { + //Reset the stream position to 0 so the stream can be copied into the underlying unmanaged buffer + stream.Position = 0; + //Populate the response values - No longer need to implement GetResponseHeaders (unless you need to perform a redirect) + ResponseLength = stream.Length; + MimeType = "text/html"; + StatusCode = (int)HttpStatusCode.OK; + Stream = stream; + + callback.Continue(); + } + } + }); + + return CefReturnValue.ContinueAsync; + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefSharpSchemeHandlerFactory.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefSharpSchemeHandlerFactory.cs new file mode 100644 index 0000000..9a66a3d --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/CefSharpSchemeHandlerFactory.cs @@ -0,0 +1,114 @@ +// Copyright © 2013 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using CefSharp; + using System; + using System.Collections.Generic; + using System.IO; + + public class CefSharpSchemeHandlerFactory : ISchemeHandlerFactory + { + public const string SchemeName = "custom"; + public const string SchemeNameTest = "test"; + + private static readonly IDictionary ResourceDictionary; + + static CefSharpSchemeHandlerFactory() + { + ResourceDictionary = new Dictionary + { + ////{ "/home.html", Resources.home_html }, + + ////{ "/assets/css/shCore.css", Resources.assets_css_shCore_css }, + ////{ "/assets/css/shCoreDefault.css", Resources.assets_css_shCoreDefault_css }, + ////{ "/assets/css/docs.css", Resources.assets_css_docs_css }, + ////{ "/assets/js/application.js", Resources.assets_js_application_js }, + ////{ "/assets/js/jquery.js", Resources.assets_js_jquery_js }, + ////{ "/assets/js/shBrushCSharp.js", Resources.assets_js_shBrushCSharp_js }, + ////{ "/assets/js/shBrushJScript.js", Resources.assets_js_shBrushJScript_js }, + ////{ "/assets/js/shCore.js", Resources.assets_js_shCore_js }, + + ////{ "/bootstrap/bootstrap-theme.min.css", Resources.bootstrap_theme_min_css }, + ////{ "/bootstrap/bootstrap.min.css", Resources.bootstrap_min_css }, + ////{ "/bootstrap/bootstrap.min.js", Resources.bootstrap_min_js }, + + ////{ "/BindingTest.html", Resources.BindingTest }, + ////{ "/BindingTestSingle.html", Resources.BindingTestSingle }, + ////{ "/LegacyBindingTest.html", Resources.LegacyBindingTest }, + ////{ "/PostMessageTest.html", Resources.PostMessageTest }, + ////{ "/ExceptionTest.html", Resources.ExceptionTest }, + ////{ "/PopupTest.html", Resources.PopupTest }, + ////{ "/SchemeTest.html", Resources.SchemeTest }, + ////{ "/TooltipTest.html", Resources.TooltipTest }, + ////{ "/FramedWebGLTest.html", Resources.FramedWebGLTest }, + ////{ "/MultiBindingTest.html", Resources.MultiBindingTest }, + ////{ "/ScriptedMethodsTest.html", Resources.ScriptedMethodsTest }, + ////{ "/ResponseFilterTest.html", Resources.ResponseFilterTest }, + ////{ "/DraggableRegionTest.html", Resources.DraggableRegionTest }, + ////{ "/DragDropCursorsTest.html", Resources.DragDropCursorsTest }, + ////{ "/CssAnimationTest.html", Resources.CssAnimation }, + ////{ "/CdmSupportTest.html", Resources.CdmSupportTest }, + ////{ "/Recaptcha.html", Resources.Recaptcha }, + ////{ "/UnicodeExampleGreaterThan32kb.html", Resources.UnicodeExampleGreaterThan32kb }, + ////{ "/UnocodeExampleEqualTo32kb.html", Resources.UnocodeExampleEqualTo32kb }, + ////{ "/JavascriptCallbackTest.html", Resources.JavascriptCallbackTest }, + ////{ "/BindingTestsAsyncTask.html", Resources.BindingTestsAsyncTask }, + ////{ "/BindingApiCustomObjectNameTest.html", Resources.BindingApiCustomObjectNameTest } + }; + } + + public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) + { + //Notes: + // - The 'host' portion is entirely ignored by this scheme handler. + // - If you register a ISchemeHandlerFactory for http/https schemes you should also specify a domain name + // - Avoid doing lots of processing in this method as it will affect performance. + // - Use the Default ResourceHandler implementation + + var uri = new Uri(request.Url); + var fileName = uri.AbsolutePath; + + //Load a file directly from Disk + if (fileName.EndsWith("CefSharp.Core.xml", StringComparison.OrdinalIgnoreCase)) + { + //Convenient helper method to lookup the mimeType + var mimeType = Cef.GetMimeType("xml"); + //Load a resource handler for CefSharp.Core.xml + //mimeType is optional and will default to text/html + return ResourceHandler.FromFilePath("CefSharp.Core.xml", mimeType, autoDisposeStream: true); + } + + if (fileName.EndsWith("Logo.png", StringComparison.OrdinalIgnoreCase)) + { + //Convenient helper method to lookup the mimeType + var mimeType = Cef.GetMimeType("png"); + //Load a resource handler for Logo.png + //mimeType is optional and will default to text/html + return ResourceHandler.FromFilePath("..\\..\\..\\..\\CefSharp.WinForms.Example\\Resources\\chromium-256.png", mimeType, autoDisposeStream: true); + } + + if (uri.Host == "cefsharp.com" && schemeName == "https" && (string.Equals(fileName, "/PostDataTest.html", StringComparison.OrdinalIgnoreCase) || + string.Equals(fileName, "/PostDataAjaxTest.html", StringComparison.OrdinalIgnoreCase))) + { + return new CefSharpSchemeHandler(); + } + + if (string.Equals(fileName, "/EmptyResponseFilterTest.html", StringComparison.OrdinalIgnoreCase)) + { + return ResourceHandler.FromString("", mimeType: ResourceHandler.DefaultMimeType); + } + + string resource; + if (ResourceDictionary.TryGetValue(fileName, out resource) && !string.IsNullOrEmpty(resource)) + { + var fileExtension = Path.GetExtension(fileName); + return ResourceHandler.FromString(resource, includePreamble: true, mimeType: Cef.GetMimeType(fileExtension)); + } + + return null; + } + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/InternetOpenType.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/InternetOpenType.cs new file mode 100644 index 0000000..c70f409 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/InternetOpenType.cs @@ -0,0 +1,36 @@ +// Copyright © 2014 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + #region Enums + + /// + /// Defines the InternetOpenType. + /// + public enum InternetOpenType + { + /// + /// Defines the PreConfig. + /// + PreConfig = 0, + + /// + /// Defines the Direct. + /// + Direct = 1, + + /// + /// Defines the Proxy. + /// + Proxy = 3, + + /// + /// Defines the PreConfigWithNoAutoProxy. + /// + PreConfigWithNoAutoProxy = 4 + } + + #endregion Enums +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/InternetProxyInfo.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/InternetProxyInfo.cs new file mode 100644 index 0000000..5be4b12 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/InternetProxyInfo.cs @@ -0,0 +1,22 @@ +// Copyright © 2014 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + /// + /// Defines the . + /// + public struct InternetProxyInfo + { + #region Fields + + public InternetOpenType AccessType; + + public string ProxyAddress; + + public string ProxyBypass; + + #endregion Fields + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/ProcessHelper.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/ProcessHelper.cs new file mode 100644 index 0000000..c2cf925 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/ProcessHelper.cs @@ -0,0 +1,57 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using System.Diagnostics; + using System.IO; + using System.Threading.Tasks; + + /// + /// Defines the . + /// + public static class ProcessHelper + { + #region Methods + + /// + /// The OpenFolder. + /// + /// The filePath. + public static void OpenContainedFolder(string filePath) + { + var path = Path.GetDirectoryName(filePath); + Start(path); + } + + /// + /// The OpenUrl. + /// + /// The url. + public static void OpenUrl(string url) + { + Start(url); + } + + /// + /// The Start. + /// + /// The filePath. + public static void Start(string filePath) + { + Task.Run(() => + { + var process = new Process + { + StartInfo = new ProcessStartInfo() + { + CreateNoWindow = true, + ErrorDialog = true, + FileName = filePath, + Verb = "Open" + } + }; + process.Start(); + }); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/ProxyConfig.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/ProxyConfig.cs new file mode 100644 index 0000000..d231ace --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/ProxyConfig.cs @@ -0,0 +1,68 @@ +// Copyright © 2015 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using System; + using System.ComponentModel; + using System.Runtime.InteropServices; + + /// + /// Defines the . + /// + public class ProxyConfig + { + #region Constants + + private const uint InternetOptionProxy = 38; + + #endregion Constants + + #region Methods + + /// + /// The GetProxyInformation. + /// + /// The . + public static InternetProxyInfo GetProxyInformation() + { + var bufferLength = 0; + InternetQueryOption(IntPtr.Zero, InternetOptionProxy, IntPtr.Zero, ref bufferLength); + var buffer = IntPtr.Zero; + + try + { + buffer = Marshal.AllocHGlobal(bufferLength); + + if (InternetQueryOption(IntPtr.Zero, InternetOptionProxy, buffer, ref bufferLength)) + { + var ipi = (InternetProxyInfo)Marshal.PtrToStructure(buffer, typeof(InternetProxyInfo)); + return ipi; + } + + throw new Win32Exception(); + } + finally + { + if (buffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(buffer); + } + } + } + + /// + /// The InternetQueryOption. + /// + /// The hInternet. + /// The dwOption. + /// The lpBuffer. + /// The lpdwBufferLength. + /// The . + [DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool InternetQueryOption(IntPtr hInternet, uint dwOption, IntPtr lpBuffer, ref int lpdwBufferLength); + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Helpers/UrlHelper.cs b/VideoBrowser/Controls/CefSharpBrowser/Helpers/UrlHelper.cs new file mode 100644 index 0000000..623cef2 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Helpers/UrlHelper.cs @@ -0,0 +1,42 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Helpers +{ + using System; + using System.Globalization; + using System.Net; + + /// + /// Defines the . + /// + public static class UrlHelper + { + #region Methods + + /// + /// The IsImageUrl. + /// + /// The URL. + /// The . + public static bool IsImageUrl(this string URL) + { + if (!(HttpWebRequest.Create(URL) is HttpWebRequest req)) + { + return false; + } + + req.Method = "HEAD"; + try + { + using (var resp = req.GetResponse()) + { + return resp.ContentType.ToLower(CultureInfo.InvariantCulture).StartsWith("image/"); + } + } + catch (Exception) + { + return false; + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/InterTabClient.cs b/VideoBrowser/Controls/CefSharpBrowser/InterTabClient.cs new file mode 100644 index 0000000..099bfd1 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/InterTabClient.cs @@ -0,0 +1,88 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using Dragablz; + using System; + using System.Windows; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Controls.CefSharpBrowser.Views; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class InterTabClient : IInterTabClient + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The data. + internal InterTabClient(GlobalBrowserData data) + { + this.GlobalBrowserData = data; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the CreateWindow. + /// + public Func<(Window, TabablzControl)> CreateWindow { get; set; } + + /// + /// Gets the GlobalBrowserData. + /// + public GlobalBrowserData GlobalBrowserData { get; } + + #endregion Properties + + #region Methods + + /// + /// The GetNewHost. + /// + /// The interTabClient. + /// The partition. + /// The source. + /// The . + public INewTabHost GetNewHost(IInterTabClient interTabClient, object partition, TabablzControl source) + { + NewTabHost host = null; + UIThreadHelper.Invoke(() => + { + var (window, tabControl) = this.CreateWindow != null ? this.CreateWindow() : this.CreateDefaultWindow(); + host = new NewTabHost(window, tabControl); + }); + + return host; + } + + /// + /// The TabEmptiedHandler. + /// + /// The tabControl. + /// The window. + /// The . + public TabEmptiedResponse TabEmptiedHandler(TabablzControl tabControl, Window window) + { + return TabEmptiedResponse.CloseWindowOrLayoutBranch; + } + + /// + /// The CreateDefaultWindow. + /// + /// The . + internal (Window, TabablzControl) CreateDefaultWindow() + { + var viewModel = new DefaultTabHostViewModel(this.GlobalBrowserData); + var window = new DefaultTabHostWindow { DataContext = viewModel }; + var tabControl = window.WebBrowserTabControlView.InitialTabablzControl; + return (window, tabControl); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Models/BookmarkModel.cs b/VideoBrowser/Controls/CefSharpBrowser/Models/BookmarkModel.cs new file mode 100644 index 0000000..cb845e6 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Models/BookmarkModel.cs @@ -0,0 +1,42 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Models +{ + using System.ComponentModel; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class BookmarkModel : INotifyPropertyChanged + { + #region Fields + + private string _name; + + private string _url; + + #endregion Fields + + #region Events + + /// + /// Defines the PropertyChanged. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion Events + + #region Properties + + /// + /// Gets or sets the Name. + /// + public string Name { get => _name; set => this.Set(this.PropertyChanged, ref _name, value); } + + /// + /// Gets or sets the Url. + /// + public string Url { get => _url; set => this.Set(this.PropertyChanged, ref _url, value); } + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Models/BrowserSettings.cs b/VideoBrowser/Controls/CefSharpBrowser/Models/BrowserSettings.cs new file mode 100644 index 0000000..2251524 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Models/BrowserSettings.cs @@ -0,0 +1,58 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Models +{ + using System.Collections.Generic; + + /// + /// Defines the . + /// + public class BrowserSettings + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public BrowserSettings() + { + this.BookmarkModels = new List(); + this.DownloadItems = new List(); + this.TabSettingModels = new List(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the BookmarkModels. + /// + public IList BookmarkModels { get; set; } + + /// + /// Gets or sets the DownloadItems. + /// + public IList DownloadItems { get; set; } + + /// + /// Gets or sets the OutputFolder. + /// + public string OutputFolder { get; set; } + + /// + /// Gets or sets the SelectedTabSettingIndex. + /// + public int SelectedTabSettingIndex { get; set; } + + /// + /// Gets or sets the TabSettingModels. + /// + public IList TabSettingModels { get; set; } + + /// + /// Gets or sets the Version. + /// + public string Version { get; set; } = "1.0"; + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Models/DownloadItemModel.cs b/VideoBrowser/Controls/CefSharpBrowser/Models/DownloadItemModel.cs new file mode 100644 index 0000000..cf50a08 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Models/DownloadItemModel.cs @@ -0,0 +1,248 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Models +{ + using Newtonsoft.Json; + using System; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class DownloadItemModel : NotifyPropertyChanged, IEquatable, IDisposable + { + #region Fields + + private string _fileSize; + + private bool _isQueuedControlsVisible = true; + + private string _outputPath; + + private string _pauseText = "Pause"; + + private int _progress; + + private string _status; + + private string _thumbnail; + + private string _title; + + private string _url; + + private bool Disposed = false;// To detect redundant calls + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public DownloadItemModel() + { + this.ExecuteDownloadedCommand = new RelayCommand(this.OnExecuteDownloaded, nameof(this.ExecuteDownloadedCommand)); + this.ShowDownloadedFolderCommand = new RelayCommand(this.OnShowDownloadedFolderCommand, nameof(this.ShowDownloadedFolderCommand)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other. + internal DownloadItemModel(DownloadItemModel other) + { + this.FileSize = other.FileSize; + this.IsApplicationThumbnail = other.IsApplicationThumbnail; + this.IsQueuedControlsVisible = other.IsQueuedControlsVisible; + this.OutputPath = other.OutputPath; + this.PauseText = other.PauseText; + this.Progress = other.Progress; + this.Status = other.Status; + this.Thumbnail = other.Thumbnail; + this.Title = other.Title; + this.Url = other.Url; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the CancelDownloadCommand. + /// + [JsonIgnore] + public ICommand CancelDownloadCommand { get; protected set; } + + /// + /// Gets or sets the ExecuteDownloadedCommand. + /// + [JsonIgnore] + public ICommand ExecuteDownloadedCommand { get; protected set; } + + /// + /// Gets or sets the FileSize. + /// + public string FileSize { get => this._fileSize; set => this.Set(this.PropertyChangedHandler, ref this._fileSize, value); } + + /// + /// Gets or sets a value indicating whether IsApplicationThumbnail. + /// + public bool IsApplicationThumbnail { get; set; } + + /// + /// Gets a value indicating whether IsCompletedControlsVisible. + /// + [JsonIgnore] + public bool IsCompletedControlsVisible { get => !this.IsQueuedControlsVisible; } + + /// + /// Gets or sets a value indicating whether IsQueuedControlsVisible. + /// + public bool IsQueuedControlsVisible + { + get => _isQueuedControlsVisible; + set + { + if (!this.Set(this.PropertyChangedHandler, ref _isQueuedControlsVisible, value)) + { + return; + } + + this.OnPropertyChanged(nameof(this.IsCompletedControlsVisible)); + } + } + + /// + /// Gets or sets the OutputPath. + /// + public string OutputPath { get => _outputPath; set => this.Set(this.PropertyChangedHandler, ref _outputPath, value); } + + /// + /// Gets or sets the PauseDownloadCommand. + /// + [JsonIgnore] + public ICommand PauseDownloadCommand { get; protected set; } + + /// + /// Gets or sets the PauseText. + /// + public string PauseText { get => _pauseText; set => this.Set(this.PropertyChangedHandler, ref _pauseText, value); } + + /// + /// Gets or sets the Progress. + /// + public int Progress { get => this._progress; set => this.Set(this.PropertyChangedHandler, ref this._progress, value); } + + /// + /// Gets or sets the ShowDownloadedFolderCommand. + /// + [JsonIgnore] + public ICommand ShowDownloadedFolderCommand { get; protected set; } + + /// + /// Gets or sets the Status. + /// + public string Status { get => this._status; set => this.Set(this.PropertyChangedHandler, ref this._status, value); } + + /// + /// Gets or sets the Thumbnail. + /// + public string Thumbnail { get => _thumbnail; set => this.Set(this.PropertyChangedHandler, ref _thumbnail, value); } + + /// + /// Gets or sets the Title. + /// + public string Title { get => this._title; set => this.Set(this.PropertyChangedHandler, ref this._title, value); } + + /// + /// Gets or sets the Url. + /// + public string Url { get => this._url; set => this.Set(this.PropertyChangedHandler, ref this._url, value); } + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + + /// + /// The Equals. + /// + /// The other. + /// The . + public bool Equals(DownloadItemModel other) + { + var isEqual = other != null && this.OutputPath == other.OutputPath; + return isEqual; + } + + /// + /// The Equals. + /// + /// The obj. + /// The . + public override bool Equals(object obj) + { + return Equals(obj as DownloadItemModel); + } + + /// + /// The GetHashCode. + /// + /// The . + public override int GetHashCode() + { + var hash = this.OutputPath == null ? string.Empty.GetHashCode() : this.OutputPath.GetHashCode(); + return hash; + } + + /// + /// The Dispose. + /// + /// The disposing. + protected virtual void Dispose(bool disposing) + { + if (!this.Disposed) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + this.Disposed = true; + } + } + + /// + /// The OnPlayMedia. + /// + /// The obj. + private void OnExecuteDownloaded(object obj) + { + ProcessHelper.Start(this.OutputPath); + } + + /// + /// The OnShowMediaInFolder. + /// + /// The obj. + private void OnShowDownloadedFolderCommand(object obj) + { + ProcessHelper.OpenContainedFolder(this.OutputPath); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Models/DownloadProcessModel.cs b/VideoBrowser/Controls/CefSharpBrowser/Models/DownloadProcessModel.cs new file mode 100644 index 0000000..6c86038 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Models/DownloadProcessModel.cs @@ -0,0 +1,79 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Models +{ + using CefSharp; + using VideoBrowser.Common; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class DownloadProcessModel : DownloadItemModel + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The item. + internal DownloadProcessModel(DownloadItem item) + { + this.Initialize(item); + this.IsApplicationThumbnail = true; + this.CancelDownloadCommand = new RelayCommand(this.OnCancelDownload, nameof(this.CancelDownloadCommand)); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets a value indicating whether IsCanceled. + /// + internal bool IsCanceled { get; private set; } + + #endregion Properties + + #region Methods + + /// + /// The UpdateInfo. + /// + internal void UpdateInfo(DownloadItem downloadItem) + { + this.OutputPath = downloadItem.FullPath; + this.Progress = downloadItem.PercentComplete; + var speed = $"{downloadItem.CurrentSpeed.FormatFileSize()}/s"; + var percentComplete = $"{downloadItem.PercentComplete}%"; + var completeStatus = "Completed"; + this.Status = downloadItem.IsComplete ? completeStatus : $"{percentComplete} - {speed}"; + if (this.Status == completeStatus) + { + this.IsQueuedControlsVisible = false; + } + + this.Thumbnail = this.OutputPath; + } + + /// + /// The Initialize. + /// + private void Initialize(DownloadItem downloadItem) + { + this.FileSize = FormatString.FormatFileSize(downloadItem.TotalBytes); + this.Title = downloadItem.SuggestedFileName; + this.Url = downloadItem.OriginalUrl; + this.UpdateInfo(downloadItem); + } + + /// + /// The OnCancelDownload. + /// + /// The obj. + private void OnCancelDownload(object obj) + { + this.IsCanceled = true; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Models/TabSettingsModel.cs b/VideoBrowser/Controls/CefSharpBrowser/Models/TabSettingsModel.cs new file mode 100644 index 0000000..aead172 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Models/TabSettingsModel.cs @@ -0,0 +1,42 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Models +{ + using System.ComponentModel; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class TabSettingsModel : INotifyPropertyChanged + { + #region Fields + + private string _title; + + private string _url; + + #endregion Fields + + #region Events + + /// + /// Defines the PropertyChanged. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion Events + + #region Properties + + /// + /// Gets or sets the Title. + /// + public string Title { get => _title; set => this.Set(this.PropertyChanged, ref _title, value); } + + /// + /// Gets or sets the Url. + /// + public string Url { get => _url; set => this.Set(this.PropertyChanged, ref _url, value); } + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/BrowserIcons.cs b/VideoBrowser/Controls/CefSharpBrowser/Resources/BrowserIcons.cs new file mode 100644 index 0000000..eecda55 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Resources/BrowserIcons.cs @@ -0,0 +1,29 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Resources +{ + using System.Windows.Media; + + /// + /// Defines the . + /// + public static class BrowserIcons + { + #region Properties + + /// + /// Gets the Lock. + /// + public static Geometry Lock { get; } = Geometry.Parse("M2,16L2,30 22,30 22,16z M12,2C8.14,2,5,5.141,5,9L5,14 19,14 19,9C19,5.14,15.86,2,12,2z M12,0C16.962,0,21,4.037,21,9L21,14 24,14 24,32 0,32 0,14 3,14 3,9C3,4,7,0,12,0z"); + + /// + /// Gets the Star. + /// + public static Geometry Star { get; } = Geometry.Parse("M16,0L21,10.533997 32,12.223022 24,20.421997 25.889008,32 16,26.533997 6.1109924,32 8,20.421997 0,12.223022 11.057007,10.533997z"); + + /// + /// Gets the StarWF. + /// + public static Geometry StarWF { get; } = Geometry.Parse("M16,5.6780396L12.681,12.743042 5.2630005,13.877014 10.631989,19.378052 9.3639984,27.147034 16,23.48 22.634995,27.147034 21.366,19.378052 26.735,13.877014 19.317,12.743042z M16,0L20.942993,10.533997 32,12.223022 24,20.421997 25.886993,32 16,26.533997 6.111,32 8,20.421997 0,12.223 11,10.533997z"); + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/docs.css b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/docs.css new file mode 100644 index 0000000..18e7f60 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/docs.css @@ -0,0 +1,1110 @@ +/* + * Bootstrap Documentation + * Special styles for presenting Bootstrap's documentation and code examples. + * + * Table of contents: + * + * Scaffolding + * Main navigation + * Footer + * Social buttons + * Homepage + * Page headers + * Old docs callout + * Ads + * Side navigation + * Docs sections + * Callouts + * Grid styles + * Examples + * Code snippets (highlight) + * Responsive tests + * Glyphicons + * Customizer + * Miscellaneous + */ + + +/* + * Scaffolding + * + * Update the basics of our documents to prep for docs content. + */ + +body { + position: relative; /* For scrollyspy */ + padding-top: 50px; /* Account for fixed navbar */ +} + +/* Keep code small in tables on account of limited space */ +.table code { + font-size: 13px; + font-weight: normal; +} + +/* Outline button for use within the docs */ +.btn-outline { + color: #563d7c; + background-color: #fff; + border-color: #e5e5e5; +} +.btn-outline:hover, +.btn-outline:focus, +.btn-outline:active { + color: #fff; + background-color: #563d7c; + border-color: #563d7c; +} + +/* Inverted outline button (white on dark) */ +.btn-outline-inverse { + color: #fff; + background-color: transparent; + border-color: #cdbfe3; +} +.btn-outline-inverse:hover, +.btn-outline-inverse:focus, +.btn-outline-inverse:active { + color: #563d7c; + text-shadow: none; + background-color: #fff; + border-color: #fff; +} + + +/* + * Main navigation + * + * Turn the `.navbar` at the top of the docs purple. + */ + +.bs-docs-nav { + text-shadow: 0 -1px 0 rgba(0,0,0,.15); + background-color: #563d7c; + border-color: #463265; + box-shadow: 0 1px 0 rgba(255,255,255,.1); +} +.bs-docs-nav .navbar-collapse { + border-color: #463265; +} +.bs-docs-nav .navbar-brand { + color: #fff; +} +.bs-docs-nav .navbar-nav > li > a { + color: #cdbfe3; +} +.bs-docs-nav .navbar-nav > li > a:hover { + color: #fff; +} +.bs-docs-nav .navbar-nav > .active > a, +.bs-docs-nav .navbar-nav > .active > a:hover { + color: #fff; + background-color: #463265; +} +.bs-docs-nav .navbar-toggle { + border-color: #563d7c; +} +.bs-docs-nav .navbar-toggle:hover { + background-color: #463265; + border-color: #463265; +} + + +/* + * Footer + * + * Separated section of content at the bottom of all pages, save the homepage. + */ + +.bs-footer { + padding-top: 40px; + padding-bottom: 30px; + margin-top: 100px; + color: #777; + text-align: center; + border-top: 1px solid #e5e5e5; +} +.footer-links { + margin: 10px 0; + padding-left: 0; +} +.footer-links li { + display: inline; + padding: 0 2px; +} +.footer-links li:first-child { + padding-left: 0; +} + +@media (min-width: 768px) { + .bs-footer { + text-align: left; + } + .bs-footer p { + margin-bottom: 0; + } +} + + +/* + * Social buttons + * + * Twitter and GitHub social action buttons (for homepage and footer). + */ + +.bs-social { + text-align: center; +} +.bs-social-buttons { + display: inline-block; + margin-bottom: 20px; + padding-left: 0; + list-style: none; +} +.bs-social-buttons li { + display: inline-block; + line-height: 1; + padding: 5px 8px; +} +.bs-social-buttons .twitter-follow-button { + width: 225px !important; +} +.bs-social-buttons .twitter-share-button { + width: 98px !important; +} +/* Style the GitHub buttons via CSS instead of inline attributes */ +.github-btn { + border: 0; + overflow: hidden; +} + +@media screen and (min-width: 768px) { + .bs-social { + text-align: left; + } +} + + +/* + * Topography, yo! + * + * Apply the map background via base64 and relevant colors where we need 'em. + */ + +.bs-docs-home, +.bs-header { + color: #cdbfe3; + background-color: #563d7c; + background-image: url(data:image/png;base64,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); +} + + +/* + * Homepage + * + * Tweaks to the custom homepage and the masthead (main jumbotron). + */ + + /* Masthead (headings and download button) */ + .bs-masthead { + position: relative; + padding: 30px 15px; + text-align: center; + text-shadow: 0 1px 0 rgba(0,0,0,.15); +} +.bs-masthead h1 { + font-size: 50px; + line-height: 1; + color: #fff; +} +.bs-masthead .btn-outline-inverse { + margin-top: 20px; + margin-bottom: 20px; + padding: 18px 24px; + font-size: 24px; +} + +/* Links to project-level content like the repo, Expo, etc */ +.bs-masthead-links { + margin-bottom: 20px; + padding-left: 0; + list-style: none; + text-align: center; +} +.bs-masthead-links li { + display: inline-block; + padding: 4px 8px; +} +.bs-masthead-links a { + color: #fff; +} + +@media screen and (min-width: 768px) { + .bs-masthead { + text-align: left; + padding-top: 140px; + padding-bottom: 140px; + } + .bs-masthead h1 { + font-size: 100px; + } + .bs-masthead .lead { + margin-right: 25%; + font-size: 30px; + } + .bs-masthead-links { + padding: 0; + text-align: left; + } +} + + +/* + * Page headers + * + * Jumbotron-esque headers at the top of every page that's not the homepage. + */ + + +/* Page headers */ +.bs-header { + padding: 30px 15px 40px; /* side padding builds on .container 15px, so 30px */ + font-size: 16px; + text-align: center; + text-shadow: 0 1px 0 rgba(0,0,0,.15); +} +.bs-header h1 { + color: #fff; +} +.bs-header p { + font-weight: 300; + line-height: 1.5; +} +.bs-header .container { + position: relative; +} + +@media screen and (min-width: 768px) { + .bs-header { + font-size: 21px; + text-align: left; + } + .bs-header h1 { + font-size: 60px; + line-height: 1; + } +} + +@media screen and (min-width: 992px) { + .bs-header h1, + .bs-header p { + margin-right: 380px; + } +} + + +/* + * Carbon ads + * + * Single display ad that shows on all pages (except homepage) in page headers. + * The hella `!important` is required for any pre-set property. + */ + +.carbonad { + width: auto !important; + margin: 50px -30px -40px !important; + padding: 20px !important; + overflow: hidden; /* clearfix */ + height: auto !important; + font-size: 13px !important; + line-height: 16px !important; + text-align: left; + background: #463265 !important; + border: 0 !important; + box-shadow: inset 0 3px 5px rgba(0,0,0,.075); +} +.carbonad-img { + margin: 0 !important; +} +.carbonad-text, +.carbonad-tag { + float: none !important; + display: block !important; + width: auto !important; + height: auto !important; + margin-left: 145px !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} +.carbonad-text { + padding-top: 0 !important; +} +.carbonad-tag { + color: #cdbfe3 !important; + text-align: left !important; +} +.carbonad-text a, +.carbonad-tag a { + color: #fff !important; +} +.carbonad #azcarbon > img { + display: none; /* hide what I assume are tracking images */ +} + +@media screen and (min-width: 768px) { + .carbonad { + margin: 0 !important; + border-radius: 4px; + box-shadow: inset 0 3px 5px rgba(0,0,0,.075), 0 1px 0 rgba(255,255,255,.1); + } +} + +@media screen and (min-width: 992px) { + .carbonad { + position: absolute; + top: 20px; + right: 0; + padding: 15px !important; + width: 330px !important; + min-height: 132px; + } +} + + +/* Homepage variations */ +.bs-docs-home .carbonad { + margin: 0 -15px 40px !important; +} +@media screen and (min-width: 480px) { + .bs-docs-home .carbonad { + width: 330px !important; + margin: 0 auto 40px !important; + border-radius: 4px; + } +} +@media screen and (min-width: 768px) { + .bs-docs-home .carbonad { + float: left; + width: 330px !important; + margin: 0 0 30px !important; + } + .bs-docs-home .bs-social, + .bs-docs-home .bs-masthead-links { + margin-left: 350px; + } +} +@media screen and (min-width: 992px) { + .bs-docs-home .carbonad { + position: static; + } +} +@media screen and (min-width: 1170px) { + .bs-docs-home .carbonad { + margin-top: -25px !important; + } +} + + +/* + * Callout for 2.3.2 docs + * + * Only appears below page headers (not on the homepage). The homepage gets its + * own link with the masthead links. + */ + +.bs-old-docs { + padding: 15px 20px; + color: #777; + background-color: #fafafa; + border-top: 1px solid #fff; + border-bottom: 1px solid #e5e5e5; +} +.bs-old-docs strong { + color: #555; +} + + +/* + * Side navigation + * + * Scrollspy and affixed enhanced navigation to highlight sections and secondary + * sections of docs content. + */ + +/* By default it's not affixed in mobile views, so undo that */ +.bs-sidebar.affix { + position: static; +} + +/* First level of nav */ +.bs-sidenav { + margin-top: 30px; + margin-bottom: 30px; + padding-top: 10px; + padding-bottom: 10px; + text-shadow: 0 1px 0 #fff; + background-color: #f7f5fa; + border-radius: 5px; +} + +/* All levels of nav */ +.bs-sidebar .nav > li > a { + display: block; + color: #716b7a; + padding: 5px 20px; +} +.bs-sidebar .nav > li > a:hover, +.bs-sidebar .nav > li > a:focus { + text-decoration: none; + background-color: #e5e3e9; + border-right: 1px solid #dbd8e0; +} +.bs-sidebar .nav > .active > a, +.bs-sidebar .nav > .active:hover > a, +.bs-sidebar .nav > .active:focus > a { + font-weight: bold; + color: #563d7c; + background-color: transparent; + border-right: 1px solid #563d7c; +} + +/* Nav: second level (shown on .active) */ +.bs-sidebar .nav .nav { + display: none; /* Hide by default, but at >768px, show it */ + margin-bottom: 8px; +} +.bs-sidebar .nav .nav > li > a { + padding-top: 3px; + padding-bottom: 3px; + padding-left: 30px; + font-size: 90%; +} + +/* Show and affix the side nav when space allows it */ +@media screen and (min-width: 992px) { + .bs-sidebar .nav > .active > ul { + display: block; + } + /* Widen the fixed sidebar */ + .bs-sidebar.affix, + .bs-sidebar.affix-bottom { + width: 213px; + } + .bs-sidebar.affix { + position: fixed; /* Undo the static from mobile first approach */ + top: 80px; + } + .bs-sidebar.affix-bottom { + position: absolute; /* Undo the static from mobile first approach */ + } + .bs-sidebar.affix-bottom .bs-sidenav, + .bs-sidebar.affix .bs-sidenav { + margin-top: 0; + margin-bottom: 0; + } +} +@media screen and (min-width: 1200px) { + /* Widen the fixed sidebar again */ + .bs-sidebar.affix-bottom, + .bs-sidebar.affix { + width: 263px; + } +} + + +/* + * Docs sections + * + * Content blocks for each component or feature. + */ + +/* Space things out */ +.bs-docs-section + .bs-docs-section { + padding-top: 40px; +} + +/* Janky fix for preventing navbar from overlapping */ +h1[id] { + padding-top: 80px; + margin-top: -45px; +} + + +/* + * Callouts + * + * Not quite alerts, but custom and helpful notes for folks reading the docs. + * Requires a base and modifier class. + */ + +/* Common styles for all types */ +.bs-callout { + margin: 20px 0; + padding: 15px 30px 15px 15px; + border-left: 5px solid #eee; +} +.bs-callout h4 { + margin-top: 0; +} +.bs-callout p:last-child { + margin-bottom: 0; +} +.bs-callout code, +.bs-callout .highlight { + background-color: #fff; +} + +/* Variations */ +.bs-callout-danger { + background-color: #fcf2f2; + border-color: #dFb5b4; +} +.bs-callout-warning { + background-color: #fefbed; + border-color: #f1e7bc; +} +.bs-callout-info { + background-color: #f0f7fd; + border-color: #d0e3f0; +} + + +/* + * Grid examples + * + * Highlight the grid columns within the docs so folks can see their padding, + * alignment, sizing, etc. + */ + +.show-grid { + margin-bottom: 15px; +} +.show-grid [class^="col-"] { + padding-top: 10px; + padding-bottom: 10px; + background-color: #eee; + border: 1px solid #ddd; + background-color: rgba(86,61,124,.15); + border: 1px solid rgba(86,61,124,.2); +} + + +/* + * Examples + * + * Isolated sections of example content for each component or feature. Usually + * followed by a code snippet. + */ + +.bs-example { + position: relative; + padding: 45px 15px 15px; + margin: 0 -15px 15px; + background-color: #fafafa; + box-shadow: inset 0 3px 6px rgba(0,0,0,.05); + border-color: #e5e5e5 #eee #eee; + border-style: solid; + border-width: 1px 0; +} +/* Echo out a label for the example */ +.bs-example:after { + content: "Example"; + position: absolute; + top: 15px; + left: 15px; + font-size: 12px; + font-weight: bold; + color: #bbb; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Tweak display of the code snippets when following an example */ +.bs-example + .highlight { + margin: -15px -15px 15px; + border-radius: 0; + border-width: 0 0 1px; +} + +/* Make the examples and snippets not full-width */ +@media screen and (min-width: 768px) { + .bs-example { + margin-left: 0; + margin-right: 0; + background-color: #fff; + border-width: 1px; + border-color: #ddd; + border-radius: 4px 4px 0 0; + box-shadow: none; + } + .bs-example + .highlight { + margin-top: -16px; + margin-left: 0; + margin-right: 0; + border-width: 1px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } +} + +/* Tweak content of examples for optimum awesome */ +.bs-example > p:last-child, +.bs-example > ul:last-child, +.bs-example > ol:last-child, +.bs-example > blockquote:last-child, +.bs-example > .form-control:last-child, +.bs-example > .table:last-child, +.bs-example > .navbar:last-child, +.bs-example > .jumbotron:last-child, +.bs-example > .alert:last-child, +.bs-example > .panel:last-child, +.bs-example > .list-group:last-child, +.bs-example > .well:last-child, +.bs-example > .progress:last-child, +.bs-example > .table-responsive:last-child > .table { + margin-bottom: 0; +} +.bs-example > p > .close { + float: none; +} + +/* Typography */ +.bs-example-type .table td:last-child { + color: #999; + vertical-align: middle; +} +.bs-example-type .table td { + padding: 15px 0; + border-color: #eee; +} +.bs-example-type .table tr:first-child td { + border-top: 0; +} +.bs-example-type h1, +.bs-example-type h2, +.bs-example-type h3, +.bs-example-type h4, +.bs-example-type h5, +.bs-example-type h6 { + margin: 0; +} + +/* Images */ +.bs-example > .img-circle, +.bs-example > .img-rounded, +.bs-example > .img-thumbnail { + margin: 5px; +} + +/* Buttons */ +.bs-example > .btn, +.bs-example > .btn-group { + margin-top: 5px; + margin-bottom: 5px; +} +.bs-example > .btn-toolbar + .btn-toolbar { + margin-top: 10px; +} + +/* Forms */ +.bs-example-control-sizing select, +.bs-example-control-sizing input[type="text"] + input[type="text"] { + margin-top: 10px; +} +.bs-example-form .input-group { + margin-bottom: 10px; +} +.bs-example > textarea.form-control { + resize: vertical; +} + +/* List groups */ +.bs-example > .list-group { + max-width: 400px; +} + +/* Navbars */ +.bs-example .navbar:last-child { + margin-bottom: 0; +} +.bs-navbar-top-example, +.bs-navbar-bottom-example { + z-index: 1; + padding: 0; + overflow: hidden; /* cut the drop shadows off */ +} +.bs-navbar-top-example .navbar-header, +.bs-navbar-bottom-example .navbar-header { + margin-left: 0; +} +.bs-navbar-top-example .navbar-fixed-top, +.bs-navbar-bottom-example .navbar-fixed-bottom { + position: relative; + margin-left: 0; + margin-right: 0; +} +.bs-navbar-top-example { + padding-bottom: 45px; +} +.bs-navbar-top-example:after { + top: auto; + bottom: 15px; +} +.bs-navbar-top-example .navbar-fixed-top { + top: -1px; +} +.bs-navbar-bottom-example { + padding-top: 45px; +} +.bs-navbar-bottom-example .navbar-fixed-bottom { + bottom: -1px; +} +.bs-navbar-bottom-example .navbar { + margin-bottom: 0; +} +@media (min-width: 768px) { + .bs-navbar-top-example .navbar-fixed-top, + .bs-navbar-bottom-example .navbar-fixed-bottom { + position: absolute; + } + .bs-navbar-top-example { + border-radius: 0 0 4px 4px; + } + .bs-navbar-bottom-example { + border-radius: 4px 4px 0 0; + } +} + +/* Pagination */ +.bs-example .pagination { + margin-top: 10px; + margin-bottom: 10px; +} + +/* Pager */ +.bs-example > .pager { + margin-top: 0; +} + +/* Example modals */ +.bs-example-modal { + background-color: #f5f5f5; +} +.bs-example-modal .modal { + position: relative; + top: auto; + right: auto; + left: auto; + bottom: auto; + z-index: 1; + display: block; +} +.bs-example-modal .modal-dialog { + left: auto; + margin-left: auto; + margin-right: auto; +} + +/* Example dropdowns */ +.bs-example > .dropdown > .dropdown-menu { + position: static; + display: block; + margin-bottom: 5px; +} + +/* Example tabbable tabs */ +.bs-example-tabs .nav-tabs { + margin-bottom: 15px; +} + +/* Tooltips */ +.bs-example-tooltips { + text-align: center; +} +.bs-example-tooltips > .btn { + margin-top: 5px; + margin-bottom: 5px; +} + +/* Popovers */ +.bs-example-popover { + padding-bottom: 24px; + background-color: #f9f9f9; +} +.bs-example-popover .popover { + position: relative; + display: block; + float: left; + width: 260px; + margin: 20px; +} + +/* Scrollspy demo on fixed height div */ +.scrollspy-example { + position: relative; + height: 200px; + margin-top: 10px; + overflow: auto; +} + + +/* + * Code snippets + * + * Generated via Pygments and Jekyll, these are snippets of HTML, CSS, and JS. + */ + +.highlight { + display: none; /* hidden by default, until >480px */ + padding: 9px 14px; + margin-bottom: 14px; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; + border-radius: 4px; +} +.highlight pre { + padding: 0; + margin-top: 0; + margin-bottom: 0; + background-color: transparent; + border: 0; + white-space: nowrap; +} +.highlight pre code { + font-size: inherit; + color: #333; /* Effectively the base text color */ +} +.highlight pre .lineno { + display: inline-block; + width: 22px; + padding-right: 5px; + margin-right: 10px; + text-align: right; + color: #bebec5; +} + +/* Show code snippets when we have the space */ +@media screen and (min-width: 481px) { + .highlight { + display: block; + } +} + + +/* + * Responsive tests + * + * Generate a set of tests to show the responsive utilities in action. + */ + +/* Responsive (scrollable) doc tables */ +.table-responsive .highlight pre { + white-space: normal; +} + +/* Utility classes table */ +.bs-table th small, +.responsive-utilities th small { + display: block; + font-weight: normal; + color: #999; +} +.responsive-utilities tbody th { + font-weight: normal; +} +.responsive-utilities td { + text-align: center; +} +.responsive-utilities td.is-visible { + color: #468847; + background-color: #dff0d8 !important; +} +.responsive-utilities td.is-hidden { + color: #ccc; + background-color: #f9f9f9 !important; +} + +/* Responsive tests */ +.responsive-utilities-test { + margin-top: 5px; +} +.responsive-utilities-test .col-xs-6 { + margin-bottom: 10px; +} +.responsive-utilities-test span { + padding: 15px 10px; + font-size: 14px; + font-weight: bold; + line-height: 1.1; + text-align: center; + border-radius: 4px; +} +.visible-on .col-xs-6 .hidden-xs, +.visible-on .col-xs-6 .hidden-sm, +.visible-on .col-xs-6 .hidden-md, +.visible-on .col-xs-6 .hidden-lg, +.hidden-on .col-xs-6 .visible-xs, +.hidden-on .col-xs-6 .visible-sm, +.hidden-on .col-xs-6 .visible-md, +.hidden-on .col-xs-6 .visible-lg { + color: #999; + border: 1px solid #ddd; +} +.visible-on .col-xs-6 .visible-xs, +.visible-on .col-xs-6 .visible-sm, +.visible-on .col-xs-6 .visible-md, +.visible-on .col-xs-6 .visible-lg, +.hidden-on .col-xs-6 .hidden-xs, +.hidden-on .col-xs-6 .hidden-sm, +.hidden-on .col-xs-6 .hidden-md, +.hidden-on .col-xs-6 .hidden-lg { + color: #468847; + background-color: #dff0d8; + border: 1px solid #d6e9c6; +} + + +/* + * Glyphicons + * + * Special styles for displaying the icons and their classes in the docs. + */ + +.bs-glyphicons { + padding-left: 0; + padding-bottom: 1px; + margin-bottom: 20px; + list-style: none; + overflow: hidden; +} +.bs-glyphicons li { + float: left; + width: 25%; + height: 115px; + padding: 10px; + margin: 0 -1px -1px 0; + font-size: 12px; + line-height: 1.4; + text-align: center; + border: 1px solid #ddd; +} +.bs-glyphicons .glyphicon { + display: block; + margin: 5px auto 10px; + font-size: 24px; +} +.bs-glyphicons li:hover { + background-color: rgba(86,61,124,.1); +} + +@media (min-width: 768px) { + .bs-glyphicons li { + width: 12.5%; + } +} + + +/* + * Customizer + * + * Since this is so form control heavy, we have quite a few styles to customize + * the display of inputs, headings, and more. Also included are all the download + * buttons and actions. + */ + +.bs-customizer .toggle { + float: right; + margin-top: 85px; /* On account of ghetto navbar fix */ +} + +/* Headings and form contrls */ +.bs-customizer label { + margin-top: 10px; + font-weight: 500; + color: #444; +} +.bs-customizer h2 { + margin-top: 0; + margin-bottom: 5px; + padding-top: 30px; +} +.bs-customizer h4 { + margin-top: 15px; +} +.bs-customizer input[type="text"] { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + background-color: #fafafa; +} +.bs-customizer .help-block { + font-size: 12px; +} + +/* For the variables, use regular weight */ +#less-section label { + font-weight: normal; +} + +/* Downloads */ +.bs-customize-download .btn-outline { + padding: 20px; +} + +/* Error handling */ +.bs-customizer-alert { + position: fixed; + top: 51px; + left: 0; + right: 0; + z-index: 1030; + padding: 15px 0; + color: #fff; + background-color: #d9534f; + box-shadow: inset 0 1px 0 rgba(255,255,255,.25); + border-bottom: 1px solid #b94441; +} +.bs-customizer-alert .close { + margin-top: -4px; + font-size: 24px; +} +.bs-customizer-alert p { + margin-bottom: 0; +} +.bs-customizer-alert .glyphicon { + margin-right: 5px; +} +.bs-customizer-alert pre { + margin: 10px 0 0; + color: #fff; + background-color: #a83c3a; + border-color: #973634; + box-shadow: inset 0 2px 4px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); +} + + +/* + * Miscellaneous + * + * Odds and ends for optimum docs display. + */ + +/* Examples gallery: space out content better */ +.bs-examples h4 { + margin-bottom: 5px; +} +.bs-examples p { + margin-bottom: 20px; +} + +/* Pseudo :focus state for showing how it looks in the docs */ +#focusedInput { + border-color: rgba(82,168,236,.8); + outline: 0; + outline: thin dotted \9; /* IE6-9 */ + -moz-box-shadow: 0 0 8px rgba(82,168,236,.6); + box-shadow: 0 0 8px rgba(82,168,236,.6); +} + +/* Better spacing on download options in getting started */ +.bs-docs-dl-options h4 { + margin-top: 15px; + margin-bottom: 5px; +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/shCore.css b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/shCore.css new file mode 100644 index 0000000..34f6864 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/shCore.css @@ -0,0 +1,226 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +.syntaxhighlighter a, +.syntaxhighlighter div, +.syntaxhighlighter code, +.syntaxhighlighter table, +.syntaxhighlighter table td, +.syntaxhighlighter table tr, +.syntaxhighlighter table tbody, +.syntaxhighlighter table thead, +.syntaxhighlighter table caption, +.syntaxhighlighter textarea { + -moz-border-radius: 0 0 0 0 !important; + -webkit-border-radius: 0 0 0 0 !important; + background: none !important; + border: 0 !important; + bottom: auto !important; + float: none !important; + height: auto !important; + left: auto !important; + line-height: 1.1em !important; + margin: 0 !important; + outline: 0 !important; + overflow: visible !important; + padding: 0 !important; + position: static !important; + right: auto !important; + text-align: left !important; + top: auto !important; + vertical-align: baseline !important; + width: auto !important; + box-sizing: content-box !important; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; + font-weight: normal !important; + font-style: normal !important; + font-size: 1em !important; + min-height: inherit !important; + min-height: auto !important; +} + +.syntaxhighlighter { + width: 100% !important; + margin: 1em 0 1em 0 !important; + position: relative !important; + overflow: auto !important; + font-size: 1em !important; +} +.syntaxhighlighter.source { + overflow: hidden !important; +} +.syntaxhighlighter .bold { + font-weight: bold !important; +} +.syntaxhighlighter .italic { + font-style: italic !important; +} +.syntaxhighlighter .line { + white-space: pre !important; +} +.syntaxhighlighter table { + width: 100% !important; +} +.syntaxhighlighter table caption { + text-align: left !important; + padding: .5em 0 0.5em 1em !important; +} +.syntaxhighlighter table td.code { + width: 100% !important; +} +.syntaxhighlighter table td.code .container { + position: relative !important; +} +.syntaxhighlighter table td.code .container textarea { + box-sizing: border-box !important; + position: absolute !important; + left: 0 !important; + top: 0 !important; + width: 100% !important; + height: 100% !important; + border: none !important; + background: white !important; + padding-left: 1em !important; + overflow: hidden !important; + white-space: pre !important; +} +.syntaxhighlighter table td.gutter .line { + text-align: right !important; + padding: 0 0.5em 0 1em !important; +} +.syntaxhighlighter table td.code .line { + padding: 0 1em !important; +} +.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { + padding-left: 0em !important; +} +.syntaxhighlighter.show { + display: block !important; +} +.syntaxhighlighter.collapsed table { + display: none !important; +} +.syntaxhighlighter.collapsed .toolbar { + padding: 0.1em 0.8em 0em 0.8em !important; + font-size: 1em !important; + position: static !important; + width: auto !important; + height: auto !important; +} +.syntaxhighlighter.collapsed .toolbar span { + display: inline !important; + margin-right: 1em !important; +} +.syntaxhighlighter.collapsed .toolbar span a { + padding: 0 !important; + display: none !important; +} +.syntaxhighlighter.collapsed .toolbar span a.expandSource { + display: inline !important; +} +.syntaxhighlighter .toolbar { + position: absolute !important; + right: 1px !important; + top: 1px !important; + width: 11px !important; + height: 11px !important; + font-size: 10px !important; + z-index: 10 !important; +} +.syntaxhighlighter .toolbar span.title { + display: inline !important; +} +.syntaxhighlighter .toolbar a { + display: block !important; + text-align: center !important; + text-decoration: none !important; + padding-top: 1px !important; +} +.syntaxhighlighter .toolbar a.expandSource { + display: none !important; +} +.syntaxhighlighter.ie { + font-size: .9em !important; + padding: 1px 0 1px 0 !important; +} +.syntaxhighlighter.ie .toolbar { + line-height: 8px !important; +} +.syntaxhighlighter.ie .toolbar a { + padding-top: 0px !important; +} +.syntaxhighlighter.printing .line.alt1 .content, +.syntaxhighlighter.printing .line.alt2 .content, +.syntaxhighlighter.printing .line.highlighted .number, +.syntaxhighlighter.printing .line.highlighted.alt1 .content, +.syntaxhighlighter.printing .line.highlighted.alt2 .content { + background: none !important; +} +.syntaxhighlighter.printing .line .number { + color: #bbbbbb !important; +} +.syntaxhighlighter.printing .line .content { + color: black !important; +} +.syntaxhighlighter.printing .toolbar { + display: none !important; +} +.syntaxhighlighter.printing a { + text-decoration: none !important; +} +.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { + color: black !important; +} +.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { + color: #008200 !important; +} +.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { + color: blue !important; +} +.syntaxhighlighter.printing .keyword { + color: #006699 !important; + font-weight: bold !important; +} +.syntaxhighlighter.printing .preprocessor { + color: gray !important; +} +.syntaxhighlighter.printing .variable { + color: #aa7700 !important; +} +.syntaxhighlighter.printing .value { + color: #009900 !important; +} +.syntaxhighlighter.printing .functions { + color: #ff1493 !important; +} +.syntaxhighlighter.printing .constants { + color: #0066cc !important; +} +.syntaxhighlighter.printing .script { + font-weight: bold !important; +} +.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { + color: gray !important; +} +.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { + color: #ff1493 !important; +} +.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { + color: red !important; +} +.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { + color: black !important; +} diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/shCoreDefault.css b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/shCoreDefault.css new file mode 100644 index 0000000..08f9e10 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/css/shCoreDefault.css @@ -0,0 +1,328 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/SyntaxHighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 3.0.83 (July 02 2010) + * + * @copyright + * Copyright (C) 2004-2010 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ +.syntaxhighlighter a, +.syntaxhighlighter div, +.syntaxhighlighter code, +.syntaxhighlighter table, +.syntaxhighlighter table td, +.syntaxhighlighter table tr, +.syntaxhighlighter table tbody, +.syntaxhighlighter table thead, +.syntaxhighlighter table caption, +.syntaxhighlighter textarea { + -moz-border-radius: 0 0 0 0 !important; + -webkit-border-radius: 0 0 0 0 !important; + background: none !important; + border: 0 !important; + bottom: auto !important; + float: none !important; + height: auto !important; + left: auto !important; + line-height: 1.1em !important; + margin: 0 !important; + outline: 0 !important; + overflow: visible !important; + padding: 0 !important; + position: static !important; + right: auto !important; + text-align: left !important; + top: auto !important; + vertical-align: baseline !important; + width: auto !important; + box-sizing: content-box !important; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; + font-weight: normal !important; + font-style: normal !important; + font-size: 1em !important; + min-height: inherit !important; + min-height: auto !important; +} + +.syntaxhighlighter { + width: 100% !important; + margin: 1em 0 1em 0 !important; + position: relative !important; + overflow: auto !important; + font-size: 1em !important; +} +.syntaxhighlighter.source { + overflow: hidden !important; +} +.syntaxhighlighter .bold { + font-weight: bold !important; +} +.syntaxhighlighter .italic { + font-style: italic !important; +} +.syntaxhighlighter .line { + white-space: pre !important; +} +.syntaxhighlighter table { + width: 100% !important; +} +.syntaxhighlighter table caption { + text-align: left !important; + padding: .5em 0 0.5em 1em !important; +} +.syntaxhighlighter table td.code { + width: 100% !important; +} +.syntaxhighlighter table td.code .container { + position: relative !important; +} +.syntaxhighlighter table td.code .container textarea { + box-sizing: border-box !important; + position: absolute !important; + left: 0 !important; + top: 0 !important; + width: 100% !important; + height: 100% !important; + border: none !important; + background: white !important; + padding-left: 1em !important; + overflow: hidden !important; + white-space: pre !important; +} +.syntaxhighlighter table td.gutter .line { + text-align: right !important; + padding: 0 0.5em 0 1em !important; +} +.syntaxhighlighter table td.code .line { + padding: 0 1em !important; +} +.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { + padding-left: 0em !important; +} +.syntaxhighlighter.show { + display: block !important; +} +.syntaxhighlighter.collapsed table { + display: none !important; +} +.syntaxhighlighter.collapsed .toolbar { + padding: 0.1em 0.8em 0em 0.8em !important; + font-size: 1em !important; + position: static !important; + width: auto !important; + height: auto !important; +} +.syntaxhighlighter.collapsed .toolbar span { + display: inline !important; + margin-right: 1em !important; +} +.syntaxhighlighter.collapsed .toolbar span a { + padding: 0 !important; + display: none !important; +} +.syntaxhighlighter.collapsed .toolbar span a.expandSource { + display: inline !important; +} +.syntaxhighlighter .toolbar { + position: absolute !important; + right: 1px !important; + top: 1px !important; + width: 11px !important; + height: 11px !important; + font-size: 10px !important; + z-index: 10 !important; +} +.syntaxhighlighter .toolbar span.title { + display: inline !important; +} +.syntaxhighlighter .toolbar a { + display: block !important; + text-align: center !important; + text-decoration: none !important; + padding-top: 1px !important; +} +.syntaxhighlighter .toolbar a.expandSource { + display: none !important; +} +.syntaxhighlighter.ie { + font-size: .9em !important; + padding: 1px 0 1px 0 !important; +} +.syntaxhighlighter.ie .toolbar { + line-height: 8px !important; +} +.syntaxhighlighter.ie .toolbar a { + padding-top: 0px !important; +} +.syntaxhighlighter.printing .line.alt1 .content, +.syntaxhighlighter.printing .line.alt2 .content, +.syntaxhighlighter.printing .line.highlighted .number, +.syntaxhighlighter.printing .line.highlighted.alt1 .content, +.syntaxhighlighter.printing .line.highlighted.alt2 .content { + background: none !important; +} +.syntaxhighlighter.printing .line .number { + color: #bbbbbb !important; +} +.syntaxhighlighter.printing .line .content { + color: black !important; +} +.syntaxhighlighter.printing .toolbar { + display: none !important; +} +.syntaxhighlighter.printing a { + text-decoration: none !important; +} +.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { + color: black !important; +} +.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { + color: #008200 !important; +} +.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { + color: blue !important; +} +.syntaxhighlighter.printing .keyword { + color: #006699 !important; + font-weight: bold !important; +} +.syntaxhighlighter.printing .preprocessor { + color: gray !important; +} +.syntaxhighlighter.printing .variable { + color: #aa7700 !important; +} +.syntaxhighlighter.printing .value { + color: #009900 !important; +} +.syntaxhighlighter.printing .functions { + color: #ff1493 !important; +} +.syntaxhighlighter.printing .constants { + color: #0066cc !important; +} +.syntaxhighlighter.printing .script { + font-weight: bold !important; +} +.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { + color: gray !important; +} +.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { + color: #ff1493 !important; +} +.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { + color: red !important; +} +.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { + color: black !important; +} + +.syntaxhighlighter { + background-color: white !important; +} +.syntaxhighlighter .line.alt1 { + background-color: white !important; +} +.syntaxhighlighter .line.alt2 { + background-color: white !important; +} +.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { + background-color: #e0e0e0 !important; +} +.syntaxhighlighter .line.highlighted.number { + color: black !important; +} +.syntaxhighlighter table caption { + color: black !important; +} +.syntaxhighlighter .gutter { + color: #afafaf !important; +} +.syntaxhighlighter .gutter .line { + border-right: 3px solid #6ce26c !important; +} +.syntaxhighlighter .gutter .line.highlighted { + background-color: #6ce26c !important; + color: white !important; +} +.syntaxhighlighter.printing .line .content { + border: none !important; +} +.syntaxhighlighter.collapsed { + overflow: visible !important; +} +.syntaxhighlighter.collapsed .toolbar { + color: blue !important; + background: white !important; + border: 1px solid #6ce26c !important; +} +.syntaxhighlighter.collapsed .toolbar a { + color: blue !important; +} +.syntaxhighlighter.collapsed .toolbar a:hover { + color: red !important; +} +.syntaxhighlighter .toolbar { + color: white !important; + background: #6ce26c !important; + border: none !important; +} +.syntaxhighlighter .toolbar a { + color: white !important; +} +.syntaxhighlighter .toolbar a:hover { + color: black !important; +} +.syntaxhighlighter .plain, .syntaxhighlighter .plain a { + color: black !important; +} +.syntaxhighlighter .comments, .syntaxhighlighter .comments a { + color: #008200 !important; +} +.syntaxhighlighter .string, .syntaxhighlighter .string a { + color: blue !important; +} +.syntaxhighlighter .keyword { + color: #006699 !important; +} +.syntaxhighlighter .preprocessor { + color: gray !important; +} +.syntaxhighlighter .variable { + color: #aa7700 !important; +} +.syntaxhighlighter .value { + color: #009900 !important; +} +.syntaxhighlighter .functions { + color: #ff1493 !important; +} +.syntaxhighlighter .constants { + color: #0066cc !important; +} +.syntaxhighlighter .script { + font-weight: bold !important; + color: #006699 !important; + background-color: none !important; +} +.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { + color: gray !important; +} +.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { + color: #ff1493 !important; +} +.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { + color: red !important; +} + +.syntaxhighlighter .keyword { + font-weight: bold !important; +} diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/images/beach-2089936_1920.jpg b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/images/beach-2089936_1920.jpg new file mode 100644 index 0000000..58255b4 Binary files /dev/null and b/VideoBrowser/Controls/CefSharpBrowser/Resources/assets/images/beach-2089936_1920.jpg differ diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/bootstrap/bootstrap-theme.min.css b/VideoBrowser/Controls/CefSharpBrowser/Resources/bootstrap/bootstrap-theme.min.css new file mode 100644 index 0000000..cad36b4 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Resources/bootstrap/bootstrap-theme.min.css @@ -0,0 +1 @@ +.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,0%,#e6e6e6,100%);background-image:-moz-linear-gradient(top,#fff 0,#e6e6e6 100%);background-image:linear-gradient(to bottom,#fff 0,#e6e6e6 100%);background-repeat:repeat-x;border-color:#e0e0e0;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0)}.btn-default:active,.btn-default.active{background-color:#e6e6e6;border-color:#e0e0e0}.btn-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca,0%,#3071a9,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;border-color:#2d6ca2;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.btn-primary:active,.btn-primary.active{background-color:#3071a9;border-color:#2d6ca2}.btn-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c,0%,#449d44,100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;border-color:#419641;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.btn-success:active,.btn-success.active{background-color:#449d44;border-color:#419641}.btn-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e,0%,#ec971f,100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;border-color:#eb9316;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.btn-warning:active,.btn-warning.active{background-color:#ec971f;border-color:#eb9316}.btn-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f,0%,#c9302c,100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;border-color:#c12e2a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.btn-danger:active,.btn-danger.active{background-color:#c9302c;border-color:#c12e2a}.btn-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de,0%,#31b0d5,100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;border-color:#2aabd2;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.btn-info:active,.btn-info.active{background-color:#31b0d5;border-color:#2aabd2}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca,0%,#357ebd,100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff,0%,#f8f8f8,100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar .navbar-nav>.active>a{background-color:#f8f8f8}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c,0%,#222,100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0)}.navbar-inverse .navbar-nav>.active>a{background-color:#222}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8,0%,#c8e5bc,100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7,0%,#b9def0,100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3,0%,#f8efc0,100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede,0%,#e7c3c3,100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb,0%,#f5f5f5,100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca,0%,#3071a9,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c,0%,#449d44,100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de,0%,#31b0d5,100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e,0%,#ec971f,100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f,0%,#c9302c,100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca,0%,#3278b3,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5,0%,#e8e8e8,100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca,0%,#357ebd,100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8,0%,#d0e9c6,100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7,0%,#c4e3f3,100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3,0%,#faf2cc,100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede,0%,#ebcccc,100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8,0%,#f5f5f5,100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Resources/bootstrap/bootstrap.min.css b/VideoBrowser/Controls/CefSharpBrowser/Resources/bootstrap/bootstrap.min.css new file mode 100644 index 0000000..a553c4f --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Resources/bootstrap/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v3.0.0 + * + * Copyright 2013 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + *//*! normalize.css v2.1.0 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}button,input,select[multiple],textarea{background-image:none}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16.099999999999998px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-warning{color:#c09853}.text-danger{color:#b94a48}.text-success{color:#468847}.text-info{color:#3a87ad}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h4,h5,h6{margin-top:10px;margin-bottom:10px}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}h1 small,.h1 small{font-size:24px}h2 small,.h2 small{font-size:18px}h3 small,.h3 small,h4 small,.h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small{display:block;line-height:1.428571429;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:1.428571429}code,pre{font-family:Monaco,Menlo,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11{float:left}.col-xs-1{width:8.333333333333332%}.col-xs-2{width:16.666666666666664%}.col-xs-3{width:25%}.col-xs-4{width:33.33333333333333%}.col-xs-5{width:41.66666666666667%}.col-xs-6{width:50%}.col-xs-7{width:58.333333333333336%}.col-xs-8{width:66.66666666666666%}.col-xs-9{width:75%}.col-xs-10{width:83.33333333333334%}.col-xs-11{width:91.66666666666666%}.col-xs-12{width:100%}@media(min-width:768px){.container{max-width:750px}.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11{float:left}.col-sm-1{width:8.333333333333332%}.col-sm-2{width:16.666666666666664%}.col-sm-3{width:25%}.col-sm-4{width:33.33333333333333%}.col-sm-5{width:41.66666666666667%}.col-sm-6{width:50%}.col-sm-7{width:58.333333333333336%}.col-sm-8{width:66.66666666666666%}.col-sm-9{width:75%}.col-sm-10{width:83.33333333333334%}.col-sm-11{width:91.66666666666666%}.col-sm-12{width:100%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-11{left:91.66666666666666%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-11{margin-left:91.66666666666666%}}@media(min-width:992px){.container{max-width:970px}.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11{float:left}.col-md-1{width:8.333333333333332%}.col-md-2{width:16.666666666666664%}.col-md-3{width:25%}.col-md-4{width:33.33333333333333%}.col-md-5{width:41.66666666666667%}.col-md-6{width:50%}.col-md-7{width:58.333333333333336%}.col-md-8{width:66.66666666666666%}.col-md-9{width:75%}.col-md-10{width:83.33333333333334%}.col-md-11{width:91.66666666666666%}.col-md-12{width:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.333333333333332%}.col-md-push-2{left:16.666666666666664%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333333333333%}.col-md-push-5{left:41.66666666666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.333333333333336%}.col-md-push-8{left:66.66666666666666%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333333333334%}.col-md-push-11{left:91.66666666666666%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-11{right:91.66666666666666%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-11{margin-left:91.66666666666666%}}@media(min-width:1200px){.container{max-width:1170px}.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11{float:left}.col-lg-1{width:8.333333333333332%}.col-lg-2{width:16.666666666666664%}.col-lg-3{width:25%}.col-lg-4{width:33.33333333333333%}.col-lg-5{width:41.66666666666667%}.col-lg-6{width:50%}.col-lg-7{width:58.333333333333336%}.col-lg-8{width:66.66666666666666%}.col-lg-9{width:75%}.col-lg-10{width:83.33333333333334%}.col-lg-11{width:91.66666666666666%}.col-lg-12{width:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-11{left:91.66666666666666%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-11{margin-left:91.66666666666666%}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table thead>tr>th,.table tbody>tr>th,.table tfoot>tr>th,.table thead>tr>td,.table tbody>tr>td,.table tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table caption+thead tr:first-child th,.table colgroup+thead tr:first-child th,.table thead:first-child tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed thead>tr>th,.table-condensed tbody>tr>th,.table-condensed tfoot>tr>th,.table-condensed thead>tr>td,.table-condensed tbody>tr>td,.table-condensed tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8;border-color:#d6e9c6}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td{background-color:#d0e9c6;border-color:#c9e2b3}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede;border-color:#eed3d7}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td{background-color:#ebcccc;border-color:#e6c1c7}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3;border-color:#fbeed5}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td{background-color:#faf2cc;border-color:#f8e5be}@media(max-width:768px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0;background-color:#fff}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>thead>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>thead>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:45px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:45px;line-height:45px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label{color:#c09853}.has-warning .form-control{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.has-warning .input-group-addon{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.has-error .help-block,.has-error .control-label{color:#b94a48}.has-error .form-control{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.has-error .input-group-addon{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.has-success .help-block,.has-success .control-label{color:#468847}.has-success .form-control{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.has-success .input-group-addon{color:#468847;background-color:#dff0d8;border-color:#468847}.form-control-static{padding-top:7px;margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-xs{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-print:before{content:"\e045"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-briefcase:before{content:"\1f4bc"}.glyphicon-calendar:before{content:"\1f4c5"}.glyphicon-pushpin:before{content:"\1f4cc"}.glyphicon-paperclip:before{content:"\1f4ce"}.glyphicon-camera:before{content:"\1f4f7"}.glyphicon-lock:before{content:"\1f512"}.glyphicon-bell:before{content:"\1f514"}.glyphicon-bookmark:before{content:"\1f516"}.glyphicon-fire:before{content:"\1f525"}.glyphicon-wrench:before{content:"\1f527"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid #000;border-right:4px solid transparent;border-bottom:0 dotted;border-left:4px solid transparent;content:""}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#fff;text-decoration:none;background-color:#428bca}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0 dotted;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-default .caret{border-top-color:#333}.btn-primary .caret,.btn-success .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret{border-top-color:#fff}.dropup .btn-default .caret{border-bottom-color:#333}.dropup .btn-primary .caret,.dropup .btn-success .caret,.dropup .btn-warning .caret,.dropup .btn-danger .caret,.dropup .btn-info .caret{border-bottom-color:#fff}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:5px 10px;padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified .btn{display:table-cell;float:none;width:1%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group.col{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:45px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:45px;line-height:45px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}}.nav-tabs.nav-justified>li>a{margin-right:0;border-bottom:1px solid #ddd}.nav-tabs.nav-justified>.active>a{border-bottom-color:#fff}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:5px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-bottom:1px solid #ddd}.nav-tabs-justified>.active>a{border-bottom-color:#fff}.tabbable:before,.tabbable:after{display:table;content:" "}.tabbable:after{clear:both}.tabbable:before,.tabbable:after{display:table;content:" "}.tabbable:after{clear:both}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.nav .caret{border-top-color:#428bca;border-bottom-color:#428bca}.nav a:hover .caret{border-top-color:#2a6496;border-bottom-color:#2a6496}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;z-index:1000;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-collapse .navbar-nav.navbar-left:first-child{margin-left:-15px}.navbar-collapse .navbar-nav.navbar-right:last-child{margin-right:-15px}.navbar-collapse .navbar-text:last-child{margin-right:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;border-width:0 0 1px}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;z-index:1030}.navbar-fixed-bottom{bottom:0;margin-bottom:0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-text{float:left;margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{margin-right:15px;margin-left:15px}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e6e6e6}.navbar-default .navbar-nav>.dropdown>a:hover .caret,.navbar-default .navbar-nav>.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.open>a .caret,.navbar-default .navbar-nav>.open>a:hover .caret,.navbar-default .navbar-nav>.open>a:focus .caret{border-top-color:#555;border-bottom-color:#555}.navbar-default .navbar-nav>.dropdown>a .caret{border-top-color:#777;border-bottom-color:#777}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.dropdown>a:hover .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-nav>.dropdown>a .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .navbar-nav>.open>a .caret,.navbar-inverse .navbar-nav>.open>a:hover .caret,.navbar-inverse .navbar-nav>.open>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.btn .badge{position:relative;top:-1px}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.thumbnail{display:inline-block;display:block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img{display:block;height:auto;max-width:100%}a.thumbnail:hover,a.thumbnail:focus{border-color:#428bca}.thumbnail>img{margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#356635}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#2d6987}.alert-warning{color:#c09853;background-color:#fcf8e3;border-color:#fbeed5}.alert-warning hr{border-top-color:#f8e5be}.alert-warning .alert-link{color:#a47e3c}.alert-danger{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger hr{border-top-color:#e6c1c7}.alert-danger .alert-link{color:#953b39}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table{margin-bottom:0}.panel>.panel-body+.table{border-top:1px solid #ddd}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-title{margin-top:0;margin-bottom:0;font-size:16px}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#fbeed5}.panel-warning>.panel-heading{color:#c09853;background-color:#fcf8e3;border-color:#fbeed5}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#fbeed5}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#fbeed5}.panel-danger{border-color:#eed3d7}.panel-danger>.panel-heading{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#eed3d7}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#eed3d7}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}body.modal-open,.modal-open .navbar-fixed-top,.modal-open .navbar-fixed-bottom{margin-right:15px}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{z-index:1050;width:auto;padding:10px;margin-right:auto;margin-left:auto}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{right:auto;left:50%;width:600px;padding-top:30px;padding-bottom:30px}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-gradient(linear,0 top,100% top,from(rgba(0,0,0,0.5)),to(rgba(0,0,0,0.0001)));background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:-moz-linear-gradient(left,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-gradient(linear,0 top,100% top,from(rgba(0,0,0,0.0001)),to(rgba(0,0,0,0.5)));background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:-moz-linear-gradient(left,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;left:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.affix{position:fixed}@-ms-viewport{width:device-width}@media screen and (max-width:400px){@-ms-viewport{width:320px}}.hidden{display:none!important;visibility:hidden!important}.visible-xs{display:none!important}tr.visible-xs{display:none!important}th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm{display:none!important}tr.visible-sm{display:none!important}th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md{display:none!important}tr.visible-md{display:none!important}th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg{display:none!important}tr.visible-lg{display:none!important}th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs{display:none!important}tr.hidden-xs{display:none!important}th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm{display:none!important}tr.hidden-xs.hidden-sm{display:none!important}th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md{display:none!important}tr.hidden-xs.hidden-md{display:none!important}th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg{display:none!important}tr.hidden-xs.hidden-lg{display:none!important}th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs{display:none!important}tr.hidden-sm.hidden-xs{display:none!important}th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}tr.hidden-sm{display:none!important}th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md{display:none!important}tr.hidden-sm.hidden-md{display:none!important}th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg{display:none!important}tr.hidden-sm.hidden-lg{display:none!important}th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs{display:none!important}tr.hidden-md.hidden-xs{display:none!important}th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm{display:none!important}tr.hidden-md.hidden-sm{display:none!important}th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}tr.hidden-md{display:none!important}th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg{display:none!important}tr.hidden-md.hidden-lg{display:none!important}th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs{display:none!important}tr.hidden-lg.hidden-xs{display:none!important}th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm{display:none!important}tr.hidden-lg.hidden-sm{display:none!important}th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md{display:none!important}tr.hidden-lg.hidden-md{display:none!important}th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg{display:none!important}tr.hidden-lg{display:none!important}th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print{display:none!important}tr.visible-print{display:none!important}th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print{display:none!important}tr.hidden-print{display:none!important}th.hidden-print,td.hidden-print{display:none!important}} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/TabItem.cs b/VideoBrowser/Controls/CefSharpBrowser/TabItem.cs new file mode 100644 index 0000000..33a17a1 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/TabItem.cs @@ -0,0 +1,129 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using Dragablz; + using System; + using System.Windows.Media; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Controls.CefSharpBrowser.Views; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class TabItem : HeaderedItemViewModel, IDisposable + { + #region Fields + + private Geometry _icon; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public TabItem() : this(Guid.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier. + public TabItem(Guid guid) + { + this.Guid = guid; + this.HeaderViewModel = new WebBrowserTabHeaderViewModel { Header = "No Header" }; + UIThreadHelper.Invoke(() => + { + this.Header = new WebBrowserTabHeaderView { DataContext = this.HeaderViewModel }; + }); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets a value indicating whether Disposed. + /// + public bool Disposed { get; private set; } + + /// + /// Gets the Guid. + /// + public Guid Guid { get; } + + /// + /// Gets the HeaderViewModel. + /// + public WebBrowserTabHeaderViewModel HeaderViewModel { get; } + + /// + /// Gets or sets the Icon. + /// + public Geometry Icon + { + get => _icon; + set + { + if (_icon == value) + { + return; + } + + _icon = value; + this.OnPropertyChanged(); + } + } + + /// + /// Gets or sets the Title. + /// + public string Title + { + get => this.HeaderViewModel.Header; + set + { + if (this.Title == value) + { + return; + } + + this.HeaderViewModel.Header = value; + this.OnPropertyChanged(); + } + } + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + public void Dispose() + { + if (this.Disposed) + { + return; + } + + this.Disposed = true; + this.Dispose(true); + } + + /// + /// The Dispose. + /// + /// The disposing. + protected virtual void Dispose(bool disposing) + { + this.Content = null; + this.Header = null; + } + + #endregion Methods + } +} diff --git a/VideoBrowser/Controls/CefSharpBrowser/UrlTextBox.xaml b/VideoBrowser/Controls/CefSharpBrowser/UrlTextBox.xaml new file mode 100644 index 0000000..cc593eb --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/UrlTextBox.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/UrlTextBox.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/UrlTextBox.xaml.cs new file mode 100644 index 0000000..1be8904 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/UrlTextBox.xaml.cs @@ -0,0 +1,159 @@ +namespace VideoBrowser.Controls.CefSharpBrowser +{ + using System.Collections.ObjectModel; + using System.Windows; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.AddIns; + + /// + /// Interaction logic for UrlTextBox.xaml. + /// + public partial class UrlTextBox + { + #region Fields + + public static readonly DependencyProperty AddInButtonClickedProperty = + DependencyProperty.Register(nameof(AddInButtonClicked), typeof(ICommand), typeof(UrlTextBox), new PropertyMetadata(null)); + + public static readonly DependencyProperty LeftAddInButtonsProperty = + DependencyProperty.Register(nameof(LeftAddInButtons), typeof(ObservableCollection), typeof(UrlTextBox), new PropertyMetadata()); + + public static readonly DependencyProperty NavigateUrlCommandProperty = + DependencyProperty.Register(nameof(NavigateUrlCommand), typeof(ICommand), typeof(UrlTextBox), new PropertyMetadata(null)); + + public static readonly DependencyProperty NavigateUrlProperty = + DependencyProperty.Register(nameof(NavigateUrl), typeof(string), typeof(UrlTextBox), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnNavigateUrlChanged)); + + public static readonly DependencyProperty RightAddInButtonsProperty = + DependencyProperty.Register(nameof(RightAddInButtons), typeof(ObservableCollection), typeof(UrlTextBox), new PropertyMetadata()); + + public static readonly DependencyProperty UrlProperty = + DependencyProperty.Register(nameof(Url), typeof(string), typeof(UrlTextBox), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnUrlChanged)); + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public UrlTextBox() + { + this.BookmarkAddIn = new BookmarkAddIn(); + this.IsSecureAddIn = new IsSecureAddIn(); + this.InternalNavigateUrlCommand = new RelayCommand(this.OnNavigateUrl, nameof(this.InternalNavigateUrlCommand)); + this.InitializeComponent(); + this.LeftAddInButtons = new ObservableCollection { this.IsSecureAddIn }; + this.RightAddInButtons = new ObservableCollection { this.BookmarkAddIn }; + this.TextBox.InputBindings.Add(new KeyBinding(this.InternalNavigateUrlCommand, Key.Enter, ModifierKeys.None)); + this.GotFocus += this.OnUrlTextBox_GotFocus; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the AddInButtonClicked. + /// + public ICommand AddInButtonClicked + { + get { return (ICommand)GetValue(AddInButtonClickedProperty); } + set { SetValue(AddInButtonClickedProperty, value); } + } + + /// + /// Gets the BookmarkAddIn. + /// + public BookmarkAddIn BookmarkAddIn { get; } + + /// + /// Gets the IsSecureAddIn. + /// + public IsSecureAddIn IsSecureAddIn { get; } + + /// + /// Gets or sets the LeftAddInButtons. + /// + public ObservableCollection LeftAddInButtons { get => (ObservableCollection)GetValue(LeftAddInButtonsProperty); set => SetValue(LeftAddInButtonsProperty, value); } + + /// + /// Gets or sets the NavigateUrl. + /// + public string NavigateUrl { get => (string)GetValue(NavigateUrlProperty); set => SetValue(NavigateUrlProperty, value); } + + /// + /// Gets or sets the NavigateUrlCommand. + /// + public ICommand NavigateUrlCommand { get => (ICommand)GetValue(NavigateUrlCommandProperty); set => SetValue(NavigateUrlCommandProperty, value); } + + /// + /// Gets or sets the RightAddInButtons. + /// + public ObservableCollection RightAddInButtons { get => (ObservableCollection)GetValue(RightAddInButtonsProperty); set => SetValue(RightAddInButtonsProperty, value); } + + /// + /// Gets or sets the Url. + /// + public string Url { get => (string)GetValue(UrlProperty); set => SetValue(UrlProperty, value); } + + /// + /// Gets the InternalNavigateUrlCommand. + /// + private ICommand InternalNavigateUrlCommand { get; } + + #endregion Properties + + #region Methods + + /// + /// The OnNavigateUrlChanged. + /// + /// The d. + /// The e. + private static void OnNavigateUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var textBox = (UrlTextBox)d; + var navigateUrl = e.NewValue.ToString(); + textBox.BookmarkAddIn.Url = navigateUrl; + textBox.IsSecureAddIn.Url = navigateUrl; + textBox.Url = navigateUrl; + } + + /// + /// The OnUrlChanged. + /// + /// The d. + /// The e. + private static void OnUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var textBox = (UrlTextBox)d; + textBox.BookmarkAddIn.Url = string.Empty; + textBox.IsSecureAddIn.Url = string.Empty; + } + + /// + /// The OnNavigateUrl. + /// + /// The obj. + private void OnNavigateUrl(object obj) + { + this.BookmarkAddIn.Url = this.Url; + this.IsSecureAddIn.Url = this.Url; + this.NavigateUrlCommand?.Execute(obj); + } + + /// + /// The OnUrlTextBox_GotFocus. + /// + /// The sender. + /// The e. + private void OnUrlTextBox_GotFocus(object sender, RoutedEventArgs e) + { + this.TextBox.Focus(); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/ViewModels/DefaultTabHostViewModel.cs b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/DefaultTabHostViewModel.cs new file mode 100644 index 0000000..c9b98d2 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/DefaultTabHostViewModel.cs @@ -0,0 +1,30 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.ViewModels +{ + /// + /// Defines the . + /// + public class DefaultTabHostViewModel + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The globalData. + public DefaultTabHostViewModel(GlobalBrowserData globalData) + { + this.WebBrowserTabControlViewModel = new WebBrowserTabControlViewModel(globalData); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the WebBrowserTabControlViewModel. + /// + public WebBrowserTabControlViewModel WebBrowserTabControlViewModel { get; } + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/ViewModels/SettingsViewModel.cs b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..b244ef8 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/SettingsViewModel.cs @@ -0,0 +1,110 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.ViewModels +{ + using Ookii.Dialogs.Wpf; + using System.IO; + using System.Windows; + using System.Windows.Input; + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Extensions; + using VideoBrowser.Resources; + + /// + /// Defines the . + /// + public class SettingsViewModel : NotifyPropertyChanged + { + #region Fields + + private string _outputFolder; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal SettingsViewModel() + { + this.GetFolderCommand = new RelayCommand(this.OnGetFolder); + var settings = BrowserSettingsHelper.Load(); + if (settings != null) + { + this.BrowserSettings = settings; + } + + var outputFolder = this.BrowserSettings.OutputFolder; + this.OutputFolder = string.IsNullOrEmpty(outputFolder) || !Directory.Exists(outputFolder) ? AppEnvironment.UserVideoFolder : outputFolder; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the BrowserSettings. + /// + public BrowserSettings BrowserSettings { get; } = new BrowserSettings(); + + /// + /// Gets the GetFolderCommand. + /// + public ICommand GetFolderCommand { get; } + + /// + /// Gets or sets the Icon. + /// + public Geometry Icon { get; set; } = Icons.Settings; + + /// + /// Gets or sets the OutputFolder. + /// + public string OutputFolder + { + get => this._outputFolder; + set + { + this.Set(this.PropertyChangedHandler, ref this._outputFolder, value); + if (Directory.Exists(this.OutputFolder)) + { + this.BrowserSettings.OutputFolder = this.OutputFolder; + } + } + } + + /// + /// Gets or sets the OutputType. + /// + public string OutputType { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The OnGetFolder. + /// + /// The obj. + private void OnGetFolder(object obj) + { + var dialog = new VistaFolderBrowserDialog + { + Description = "Download Folder Location", + UseDescriptionForTitle = true, + SelectedPath = this.OutputFolder + }; + + var element = obj as FrameworkElement; + var window = Window.GetWindow(element); + if ((bool)dialog.ShowDialog(window)) + { + this.OutputFolder = dialog.SelectedPath; + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/ViewModels/VideoBrowserViewModel.cs b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/VideoBrowserViewModel.cs new file mode 100644 index 0000000..cf44626 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/VideoBrowserViewModel.cs @@ -0,0 +1,346 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.ViewModels +{ + using CefSharp; + using System; + using System.Collections.Generic; + using System.Windows.Input; + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + using VideoBrowser.Core; + using VideoBrowser.Extensions; + using VideoBrowser.ViewModels; + + /// + /// Defines the . + /// + public class VideoBrowserViewModel : NotifyPropertyChanged, IDisposable + { + #region Fields + + private ICommand _backwardCommand; + + private bool _canBackward; + + private bool _canForward; + + private CefWindowData _cefWindowData; + + private ICommand _forwardCommand; + + private string _header = "New Tab"; + + private string _navigateUrl = "youtube.com"; + + private ICommand _reloadCommand; + + private IWebBrowser _webBrowser; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The globalBrowserData. + /// The windowData. + internal VideoBrowserViewModel(GlobalBrowserData globalBrowserData, CefWindowData windowData) + { + this.GlobalBrowserData = globalBrowserData; + this.CefWindowData = windowData; + + // BackwardCommand and ForwardCommand are set by the View. + this.DownloadCommand = new RelayCommand(this.OnDownload, "Download", (o) => this.UrlReader.IsDownloadable); + this.HomeCommand = new RelayCommand(this.OnHome, "Home"); + this.NavigateUrlCommand = new RelayCommand(this.OnNavigateUrl, "NavigateUrl"); + this.OpenOutputFolderCommand = new RelayCommand(this.OnOpenOutputFolder, "Open output folder"); + this.IndicatorColor = new SolidColorBrush(Colors.DarkBlue); + this.UrlEditor = new UrlEditorViewModel(this.UrlReader, this.GlobalBrowserData.Settings) + { + NavigateUrlCommand = this.NavigateUrlCommand, + ShowMessageAsyncAction = this.ShowMessageAsync + }; + this.UrlEditor.PropertyChanged += this.OnUrlEditor_PropertyChanged; + this.PropertyChanged += this.OnPropertyChanged; + this.OnHome(null); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the AddInButtons. + /// + public ICollection AddInButtons => this.GlobalBrowserData.AddInButtons; + + /// + /// Gets or sets the BackwardCommand. + /// + public ICommand BackwardCommand { get => this._backwardCommand; set => this.Set(this.PropertyChangedHandler, ref this._backwardCommand, value); } + + /// + /// Gets or sets a value indicating whether CanBackward. + /// + public bool CanBackward { get => this._canBackward; set => this.Set(this.PropertyChangedHandler, ref this._canBackward, value); } + + /// + /// Gets or sets a value indicating whether CanForward. + /// + public bool CanForward { get => this._canForward; set => this.Set(this.PropertyChangedHandler, ref this._canForward, value); } + + /// + /// Gets or sets the CefWindowData. + /// + public CefWindowData CefWindowData + { + get => _cefWindowData; + internal set + { + if (!this.Set(this.PropertyChangedHandler, ref _cefWindowData, value)) + { + return; + }; + + this.InitializeHandlers(); + } + } + + /// + /// Gets the DownloadCommand. + /// + public ICommand DownloadCommand { get; } + + /// + /// Gets or sets the ForwardCommand. + /// + public ICommand ForwardCommand { get => this._forwardCommand; set => this.Set(this.PropertyChangedHandler, ref this._forwardCommand, value); } + + /// + /// Gets the GlobalBrowserData. + /// + public GlobalBrowserData GlobalBrowserData { get; } + + /// + /// Gets or sets the Header. + /// + public string Header { get => _header; set => this.Set(this.PropertyChangedHandler, ref _header, value); } + + /// + /// Gets the HomeCommand. + /// + public ICommand HomeCommand { get; } + + /// + /// Gets the IndicatorColor. + /// + public Brush IndicatorColor { get; private set; } + + /// + /// Sets the IsSuccessful. + /// + public bool? IsSuccessful + { + set + { + if (value == null) + IndicatorColor = new SolidColorBrush(Colors.LightBlue); + else if (value == false) + IndicatorColor = new SolidColorBrush(Colors.Red); + else + IndicatorColor = new SolidColorBrush(Colors.Green); + OnPropertyChanged(nameof(IndicatorColor)); + } + } + + /// + /// Gets or sets the NavigateUrl. + /// The current valid Url that is currently opened, + /// it is set by Url property if the Return key is pressed or link is clicked... + /// + public string NavigateUrl { get => this._navigateUrl; set => this.Set(this.PropertyChangedHandler, ref this._navigateUrl, value); } + + /// + /// Gets the NavigateUrlCommand. + /// + public ICommand NavigateUrlCommand { get; } + + /// + /// Gets the OpenOutputFolderCommand. + /// + public ICommand OpenOutputFolderCommand { get; } + + /// + /// Gets or sets the ReloadCommand. + /// + public ICommand ReloadCommand { get => this._reloadCommand; set => this.Set(this.PropertyChangedHandler, ref this._reloadCommand, value); } + + /// + /// Gets or sets the Url. + /// It is typed in the TextBox. + /// + public string Url { get => this.UrlEditor.Url; set => this.UrlEditor.Url = value; } + + /// + /// Gets the UrlEditor. + /// + public UrlEditorViewModel UrlEditor { get; } + + /// + /// Gets the UrlReader. + /// + public UrlReader UrlReader { get; } = new UrlReader(); + + /// + /// Gets or sets the WebBrowser. + /// + public IWebBrowser WebBrowser + { + get => _webBrowser; + set + { + if (this.WebBrowser == value) + { + return; + } + + _webBrowser = value; + this.InitializeHandlers(); + } + } + + /// + /// Gets or sets the DownloadAction. + /// + internal Action DownloadAction { get => this.UrlEditor.DownloadAction; set => this.UrlEditor.DownloadAction = value; } + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + public void Dispose() + { + this.UrlEditor.Dispose(); + this.UrlEditor.PropertyChanged -= this.OnUrlEditor_PropertyChanged; + this.UrlReader.Dispose(); + } + + /// + /// The InitializeHandlers. + /// + private void InitializeHandlers() + { + if (this.WebBrowser != null) + { + this.WebBrowser.DownloadHandler = this.GlobalBrowserData.DownloadHandler; + this.WebBrowser.MenuHandler = this.CefWindowData.CefContextMenuHandler; + this.WebBrowser.RequestHandler = this.CefWindowData.CefRequestHandler; + } + } + + /// + /// The IsUrlValid. + /// + /// The . + private bool IsUrlValid() + { + return true; + } + + /// + /// The OnDownload. + /// + /// The obj. + private void OnDownload(object obj) + { + if (!this.IsUrlValid() || !this.UrlEditor.DownloadCommand.CanExecute(null)) + { + return; + } + + this.UrlEditor.DownloadCommand.Execute(null); + } + + /// + /// The OnHome. + /// + /// The obj. + private void OnHome(object obj) + { + this.Url = AppEnvironment.HomeUrl; + this.NavigateUrl = this.Url; + } + + /// + /// The OnNavigateUrl called from Button. + /// + /// The obj. + private void OnNavigateUrl(object obj) + { + this.Url = VideoBrowser.Helpers.UrlHelper.GetValidUrl(this.Url); + this.NavigateUrl = this.Url; + this.CefWindowData.IsAirspaceVisible = false; + } + + /// + /// The OnOpenOutputFolder. + /// + /// The obj. + private void OnOpenOutputFolder(object obj) + { + ProcessHelper.Start(this.GlobalBrowserData.Settings.OutputFolder); + } + + /// + /// The OnPropertyChanged. + /// + /// The sender. + /// The e. + private void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(this.Url): + this.UrlReader.Url = this.Url; + break; + + case nameof(this.NavigateUrl): + this.Url = this.NavigateUrl; + this.UrlEditor.NavigateUrl = this.NavigateUrl; + break; + + default: + break; + } + } + + /// + /// The OnUrlEditor_PropertyChanged. + /// + /// The sender. + /// The e. + private void OnUrlEditor_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.IsMatch(nameof(this.UrlEditor.Url))) + { + this.OnPropertyChanged(nameof(this.Url)); + } + } + + /// + /// The ShowMessage. + /// + /// The title. + /// The message. + private void ShowMessageAsync(string title, string message) + { + this.CefWindowData.ShowMessageAsync(title, message); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserHeaderedItemViewModel.cs b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserHeaderedItemViewModel.cs new file mode 100644 index 0000000..3d8aab9 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserHeaderedItemViewModel.cs @@ -0,0 +1,97 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.ViewModels +{ + using System; + using System.ComponentModel; + using VideoBrowser.Controls.CefSharpBrowser.Views; + using VideoBrowser.Core; + using VideoBrowser.Extensions; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class WebBrowserHeaderedItemViewModel : TabItem + { + #region Fields + + private VideoBrowserViewModel _videoBrowserViewModel; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The globalBrowserData. + /// The cefWindowData. + /// The downloadAction. + internal WebBrowserHeaderedItemViewModel(GlobalBrowserData globalBrowserData, CefWindowData cefWindowData, Action downloadAction) + { + this.VideoBrowserViewModel = new VideoBrowserViewModel(globalBrowserData, cefWindowData) + { + DownloadAction = downloadAction + }; + + this.VideoBrowserViewModel.PropertyChanged += this.OnVideoBrowserViewModel_PropertyChanged; + this.Title = this.VideoBrowserViewModel.Header; + UIThreadHelper.Invoke(() => + { + this.VideoBrowserView = new VideoBrowserView { DataContext = this.VideoBrowserViewModel }; + this.Content = this.VideoBrowserView; + }); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the VideoBrowserView. + /// + public VideoBrowserView VideoBrowserView { get; private set; } + + /// + /// Gets the VideoBrowserViewModel. + /// + public VideoBrowserViewModel VideoBrowserViewModel { get => _videoBrowserViewModel; private set => this.Set(this.OnPropertyChanged, ref _videoBrowserViewModel, value); } + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + /// The disposing. + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (this.VideoBrowserViewModel == null) + { + return; + } + + this.VideoBrowserViewModel.PropertyChanged -= this.OnVideoBrowserViewModel_PropertyChanged; + this.VideoBrowserViewModel.Dispose(); + this.VideoBrowserViewModel = null; + this.VideoBrowserView.DataContext = null; + this.VideoBrowserView = null; + } + + /// + /// The OnVideoBrowserViewModel_PropertyChanged. + /// + /// The sender. + /// The e. + private void OnVideoBrowserViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.IsMatch(nameof(this.VideoBrowserViewModel.Header))) + { + this.Title = this.VideoBrowserViewModel.Header; + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserTabControlViewModel.cs b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserTabControlViewModel.cs new file mode 100644 index 0000000..2b255f2 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserTabControlViewModel.cs @@ -0,0 +1,348 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.ViewModels +{ + using Dragablz; + using Dragablz.Dockablz; + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Linq; + using System.Threading.Tasks; + using System.Windows; + using System.Windows.Input; + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + using VideoBrowser.Extensions; + using VideoBrowser.Helpers; + using VideoBrowser.Resources; + + /// + /// Defines the . + /// + public class WebBrowserTabControlViewModel : INotifyPropertyChanged, IDisposable + { + #region Fields + + private static int IdCounter; + + private int _selectedTabIndex; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The globalBrowserData. + public WebBrowserTabControlViewModel(GlobalBrowserData globalBrowserData) + { + this.GlobalBrowserData = globalBrowserData; + this.CefWindowData = new CefWindowData(); + this.TabItems = new ObservableCollection(); + this.TabItems.CollectionChanged += this.OnTabItems_CollectionChanged; + this.CreateBrowserFunc = this.CreateBrowser; + this.CefWindowData.CefRequestHandler.OpenUrlFromTabAction = this.OnOpenUrlFromTab; + this.CefWindowData.CefContextMenuHandler.OpenInNewTabAction = this.OnOpenUrlFromTab; + this.CefWindowData.CefContextMenuHandler.OpenInNewWindowAction = this.OnOpenUrlFromWindow; + this.LoadedCommand = new RelayCommand(this.OnLoaded, nameof(this.LoadedCommand)); + } + + #endregion Constructors + + #region Events + + /// + /// Defines the PropertyChanged. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion Events + + #region Properties + + /// + /// Gets the CefWindowData. + /// + public CefWindowData CefWindowData { get; } + + /// + /// Gets the ClosingFloatingItemHandler. + /// + public ClosingFloatingItemCallback ClosingFloatingItemHandler => ClosingFloatingItemHandlerImpl; + + /// + /// Gets the ClosingTabItemHandler. + /// + public ItemActionCallback ClosingTabItemHandler => ClosingTabItemHandlerImpl; + + /// + /// Gets or sets the CreateBrowserFunc. + /// + public Func CreateBrowserFunc { get; set; } + + /// + /// Gets the GlobalBrowserData. + /// + public GlobalBrowserData GlobalBrowserData { get; } + + /// + /// Gets or sets the Icon. + /// + public Geometry Icon { get; set; } = Icons.SearchVideo; + + /// + /// Gets the Id. + /// + public int Id { get; } = IdCounter++; + + /// + /// Gets the InterTabClient. + /// + public IInterTabClient InterTabClient => this.GlobalBrowserData.InterTabClient; + + /// + /// Gets or sets the LoadedCommand. + /// + public ICommand LoadedCommand { get; set; } + + /// + /// Gets or sets the SelectedTabIndex. + /// + public int SelectedTabIndex { get => _selectedTabIndex; set => this.Set(this.PropertyChanged, ref _selectedTabIndex, value); } + + /// + /// Gets the TabItems. + /// + public ObservableCollection TabItems { get; } + + /// + /// Gets the ToolItems. + /// + public ObservableCollection ToolItems { get; } = new ObservableCollection(); + + #endregion Properties + + #region Methods + + /// + /// The AddTab. + /// + /// The item. + public void AddTab(TabItem item) + { + if (this.IsTabItemExist(item.Guid)) + { + this.SetActiveTab(item.Guid); + Logger.Info($"Add tab canceled: Tab {item.Title} exists."); + return; + } + + this.TabItems.Add(item); + Logger.Info($"Tab {item.Title} is added."); + this.SelectedTabIndex = this.TabItems.Count - 1; + } + + /// + /// The Dispose. + /// + public void Dispose() + { + var settings = this.GlobalBrowserData.BrowserSettings; + BrowserSettingsHelper.Save(settings, this); + + this.TabItems.CollectionChanged -= this.OnTabItems_CollectionChanged; + this.TabItems.ClearAndDispose(); + } + + /// + /// The IsTabItemExist. + /// + /// The guid. + /// The . + public bool IsTabItemExist(Guid guid) + { + return this.TabItems.Any((o) => o.Guid == guid) && guid != Guid.Empty; + } + + /// + /// Returns a that represents this instance. + /// + /// The . + public override string ToString() + { + var message = $"{nameof(WebBrowserTabControlViewModel)} {this.Id}"; + return message; + } + + /// + /// The SetActiveTab. + /// + /// The guid. + internal void SetActiveTab(Guid guid) + { + for (var i = 0; i < this.TabItems.Count; i++) + { + if (this.TabItems[i].Guid == guid) + { + this.SelectedTabIndex = i; + break; + } + } + } + + /// + /// Callback to handle floating toolbar/MDI window closing. + /// + /// The args. + private static void ClosingFloatingItemHandlerImpl(ItemActionCallbackArgs args) + { + //in here you can dispose stuff or cancel the close + + //here's your view model: + if (args.DragablzItem.DataContext is IDisposable disposable) + { + disposable.Dispose(); + } + } + + /// + /// Callback to handle tab closing. + /// + /// The args. + private static void ClosingTabItemHandlerImpl(ItemActionCallbackArgs args) + { + //in here you can dispose stuff or cancel the close + + //here's your view model: + var viewModel = args.DragablzItem.DataContext as HeaderedItemViewModel; + if (viewModel.Content is FrameworkElement element && element.DataContext is IDisposable disposable) + { + disposable?.Dispose(); + } + + //here's how you can cancel stuff: + //args.Cancel(); + System.Diagnostics.Debug.Assert(viewModel != null); + } + + /// + /// The CreateBrowser. + /// + /// The . + private HeaderedItemViewModel CreateBrowser() + { + var model = new WebBrowserHeaderedItemViewModel(this.GlobalBrowserData, this.CefWindowData, null); + return model; + } + + /// + /// The OnLoaded. + /// + /// The obj. + private void OnLoaded(object obj) + { + if (!this.GlobalBrowserData.IsSettingsLoaded) + { + this.GlobalBrowserData.IsSettingsLoaded = true; + var settings = this.GlobalBrowserData.BrowserSettings; + var tabSettings = settings.TabSettingModels; + if (tabSettings.Any()) + { + var firstTab = (this.TabItems.First().Content as FrameworkElement).DataContext as VideoBrowserViewModel; + firstTab.NavigateUrl = tabSettings.First().Url; + firstTab.Header = tabSettings.First().Title; + for (var i = 1; i < tabSettings.Count; i++) + { + var tabItemSetting = tabSettings[i]; + this.OnOpenUrlFromTab(tabItemSetting.Url, tabItemSetting.Title); + } + + Task.Run(async () => + { + await Task.Delay(1000); + this.SelectedTabIndex = settings.SelectedTabSettingIndex; + }); + } + + foreach (var downloadItem in settings.DownloadItems) + { + if (System.IO.File.Exists(downloadItem.OutputPath)) + { + this.GlobalBrowserData.DownloadItemModels.Add(downloadItem); + } + } + } + } + + /// + /// The OnOpenUrlFromTab. + /// + /// The url. + private void OnOpenUrlFromTab(string url) + { + OnOpenUrlFromTab(url, string.Empty); + } + + /// + /// The OnOpenUrlFromTab. + /// + /// The url. + /// The title. + private void OnOpenUrlFromTab(string url, string title) + { + UIThreadHelper.InvokeAsync(() => + { + var browser = this.CreateBrowserFunc() as WebBrowserHeaderedItemViewModel; + browser.VideoBrowserViewModel.NavigateUrl = url; + if (!string.IsNullOrEmpty(title)) + { + browser.VideoBrowserViewModel.Header = title; + } + this.AddTab(browser); + }); + } + + /// + /// The OnOpenUrlFromWindow. + /// + /// The url. + private void OnOpenUrlFromWindow(string url) + { + var browser = this.CreateBrowserFunc() as WebBrowserHeaderedItemViewModel; + browser.VideoBrowserViewModel.NavigateUrl = url; + UIThreadHelper.Invoke(() => + { + var (Window, TabablzControl) = this.GlobalBrowserData.InterTabClient.CreateWindow(); + Window.Show(); + if (TabablzControl.ItemsSource is ICollection items) + { + items.Add(browser); + } + }); + } + + /// + /// The OnTabItems_CollectionChanged. + /// + /// The sender. + /// The e. + private void OnTabItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (var item in e.NewItems) + { + if (item is WebBrowserHeaderedItemViewModel browserTabItem) + { + browserTabItem.VideoBrowserViewModel.CefWindowData = this.CefWindowData; + } + } + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserTabHeaderViewModel.cs b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserTabHeaderViewModel.cs new file mode 100644 index 0000000..5100012 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/ViewModels/WebBrowserTabHeaderViewModel.cs @@ -0,0 +1,84 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.ViewModels +{ + using Dragablz; + using System; + using System.Windows.Input; + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class WebBrowserTabHeaderViewModel : NotifyPropertyChanged + { + #region Fields + + private string _header; + + private ImageSource _image; + + private bool _isSelected; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public WebBrowserTabHeaderViewModel() + { + this.MouseUpCommand = new RelayCommand(this.OnMouseUp, nameof(this.MouseUpCommand)); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the Header. + /// + public string Header { get => _header; set => this.Set(this.PropertyChangedHandler, ref _header, value); } + + /// + /// Gets or sets the Image. + /// + public ImageSource Image { get => _image; set => this.Set(this.PropertyChangedHandler, ref _image, value); } + + /// + /// Gets or sets a value indicating whether IsSelected. + /// + public bool IsSelected { get => _isSelected; set => this.Set(this.PropertyChangedHandler, ref _isSelected, value); } + + /// + /// Gets the MouseUpCommand. + /// + public ICommand MouseUpCommand { get; } + + #endregion Properties + + #region Methods + + /// + /// The OnMouseUp. + /// + /// The obj. + private void OnMouseUp(object obj) + { + Logger.Info($"Middle Mouse clicked on a browser tab to close it."); + var (_, args, commandParameter) = (ValueTuple)obj; + if (args is MouseButtonEventArgs mouseEventArgs && mouseEventArgs.ChangedButton == MouseButton.Middle) + { + var dragablzItem = commandParameter as System.Windows.FrameworkElement; + var closeCommand = TabablzControl.CloseItemCommand; + if (closeCommand != null && closeCommand.CanExecute(commandParameter, dragablzItem)) + { + closeCommand.Execute(dragablzItem, dragablzItem); + } + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/DefaultTabHostWindow.xaml b/VideoBrowser/Controls/CefSharpBrowser/Views/DefaultTabHostWindow.xaml new file mode 100644 index 0000000..cbdaacc --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/DefaultTabHostWindow.xaml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/DefaultTabHostWindow.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/Views/DefaultTabHostWindow.xaml.cs new file mode 100644 index 0000000..2e0f40d --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/DefaultTabHostWindow.xaml.cs @@ -0,0 +1,20 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Views +{ + /// + /// Interaction logic for DefaultTabHostWindow.xaml. + /// + public partial class DefaultTabHostWindow + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public DefaultTabHostWindow() + { + this.InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/SettingsView.xaml b/VideoBrowser/Controls/CefSharpBrowser/Views/SettingsView.xaml new file mode 100644 index 0000000..8f4ee7b --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/SettingsView.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/SettingsView.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/Views/SettingsView.xaml.cs new file mode 100644 index 0000000..a78ef81 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/SettingsView.xaml.cs @@ -0,0 +1,22 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Views +{ + using System.Windows.Controls; + + /// + /// Interaction logic for SettingsView.xaml + /// + public partial class SettingsView : UserControl + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public SettingsView() + { + InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/VideoBrowserView.xaml b/VideoBrowser/Controls/CefSharpBrowser/Views/VideoBrowserView.xaml new file mode 100644 index 0000000..83f312b --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/VideoBrowserView.xaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/VideoBrowserView.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/Views/VideoBrowserView.xaml.cs new file mode 100644 index 0000000..a405fbb --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/VideoBrowserView.xaml.cs @@ -0,0 +1,22 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Views +{ + using System.Windows.Controls; + + /// + /// Interaction logic for VideoBrowserView.xaml + /// + public partial class VideoBrowserView : UserControl + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public VideoBrowserView() + { + InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabControlView.xaml b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabControlView.xaml new file mode 100644 index 0000000..91a7781 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabControlView.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabControlView.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabControlView.xaml.cs new file mode 100644 index 0000000..5774490 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabControlView.xaml.cs @@ -0,0 +1,20 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Views +{ + /// + /// Interaction logic for WebBrowserTabControlView.xaml. + /// + public partial class WebBrowserTabControlView + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public WebBrowserTabControlView() + { + this.InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabHeaderView.xaml b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabHeaderView.xaml new file mode 100644 index 0000000..bcec60e --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabHeaderView.xaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabHeaderView.xaml.cs b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabHeaderView.xaml.cs new file mode 100644 index 0000000..ca65f09 --- /dev/null +++ b/VideoBrowser/Controls/CefSharpBrowser/Views/WebBrowserTabHeaderView.xaml.cs @@ -0,0 +1,20 @@ +namespace VideoBrowser.Controls.CefSharpBrowser.Views +{ + /// + /// Interaction logic for WebBrowserTabHeaderView.xaml. + /// + public partial class WebBrowserTabHeaderView + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public WebBrowserTabHeaderView() + { + this.InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Converters/BoolToValueConverter.cs b/VideoBrowser/Converters/BoolToValueConverter.cs new file mode 100644 index 0000000..666be8a --- /dev/null +++ b/VideoBrowser/Converters/BoolToValueConverter.cs @@ -0,0 +1,376 @@ +namespace VideoBrowser.Converters +{ + using System; + using System.Windows; + using System.Windows.Media; + + /// + /// Defines the + /// + public class BoolToBoolConverter : BoolToValueConverter + { + #region Fields + + private static readonly Lazy _instance = new Lazy(() => new BoolToBoolConverter()); + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public BoolToBoolConverter() + { + TrueValue = false; + FalseValue = true; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the Instance + /// + public static ValueConverter Instance + { + get { return _instance.Value; } + } + + #endregion Properties + } + + /// + /// Defines the + /// + public class BoolToBrushConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToByteConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToColorConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToDecimalConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToDoubleConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToHorizontalAlignmentConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToInt16Converter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToInt32Converter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToInt64Converter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToObjectConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToPointConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToRectConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToSByteConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToSingleConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToStringConverter : BoolToValueConverter + { + } + + /// + /// Defines the + /// + public class BoolToStyleConverter : BoolToValueConverter + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/VideoBrowser.csproj b/VideoBrowser/VideoBrowser.csproj new file mode 100644 index 0000000..f740e94 --- /dev/null +++ b/VideoBrowser/VideoBrowser.csproj @@ -0,0 +1,473 @@ + + + + + Debug + AnyCPU + {2D8225BC-F5B6-4F29-A9E3-F4694D260597} + WinExe + VideoBrowser + VideoBrowser + v4.8 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 0.1.1.%2a + false + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + Icon.ico + + + true + + + + + + + false + + + LocalIntranet + + + true + + + Properties\app.manifest + + + + ..\..\..\bin\CefSharp.dll + + + ..\..\..\bin\CefSharp.Core.dll + + + False + ..\..\..\bin\ControlzEx.dll + + + False + ..\..\..\bin\Dragablz.dll + + + ..\..\..\bin\log4net.dll + + + False + ..\..\..\bin\MahApps.Metro.dll + + + False + False + False + + + False + ..\..\..\bin\Newtonsoft.Json.dll + + + ..\..\..\bin\Ookii.Dialogs.Wpf.dll + + + + + + + + + + + ..\..\..\bin\System.Windows.Interactivity.dll + + + + + + + + + 4.0 + + + + + + + + + MSBuild:Compile + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CefSharpBrowser.xaml + + + + + + + + + + + + + + + + + + + + + + DefaultTabHostWindow.xaml + + + UrlTextBox.xaml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AboutView.xaml + + + DownloadFlyoutView.xaml + + + DownloadQueueView.xaml + + + SettingsView.xaml + + + UrlEditorView.xaml + + + VideoBrowserView.xaml + + + WebBrowserTabControlView.xaml + + + WebBrowserTabHeaderView.xaml + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + MainWindow.xaml + Code + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + Designer + + + + + 99.2.120 + + + 10.0.19041.1 + + + 4.7.0 + + + 4.7.0 + + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + PreserveNewest + + + + + False + Microsoft .NET Framework 4.6.2 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + + PreserveNewest + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/ViewModels/AboutViewModel.cs b/VideoBrowser/ViewModels/AboutViewModel.cs new file mode 100644 index 0000000..b87a3b7 --- /dev/null +++ b/VideoBrowser/ViewModels/AboutViewModel.cs @@ -0,0 +1,109 @@ +namespace VideoBrowser.ViewModels +{ + using System.Windows.Input; + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Helpers; + using VideoBrowser.Resources; + + /// + /// Defines the . + /// + public class AboutViewModel + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal AboutViewModel() + { + this.ApplicationName = AppEnvironment.LongName; + this.ProjectUrlClickedCommand = new RelayCommand(this.OnProjectUrlClicked, nameof(this.ProjectUrlClickedCommand)); + this.LinkedInCommand = new RelayCommand(this.OnLinkedIn, nameof(this.TwitterCommand)); + this.TwitterCommand = new RelayCommand(this.OnTwitter, nameof(this.TwitterCommand)); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the AppIcon. + /// + public ImageSource AppIcon { get; } = Helpers.ImageHelper.ToImageSource(Properties.Resources.Icon); + + /// + /// Gets the ApplicationName. + /// + public string ApplicationName { get; } + + /// + /// Gets the Author. + /// + public string Author => $"By {AppEnvironment.Author}"; + + /// + /// Gets the Icon. + /// + public Geometry Icon => Icons.Info; + + /// + /// Gets the LinkedInCommand. + /// + public ICommand LinkedInCommand { get; } + + /// + /// Gets the ProjectUrl. + /// + public string ProjectUrl => AppEnvironment.ProjectUrl; + + /// + /// Gets the ProjectUrlClickedCommand. + /// + public ICommand ProjectUrlClickedCommand { get; } + + /// + /// Gets the TwitterCommand. + /// + public ICommand TwitterCommand { get; } + + /// + /// Gets the Version. + /// + public string Version => $"Version {AppEnvironment.Version}"; + + #endregion Properties + + #region Methods + + /// + /// The OnLinkedIn. + /// + /// The obj. + private void OnLinkedIn(object obj) + { + ProcessHelper.OpenUrl(@"https://www.linkedin.com/in/ynurcahyo/"); + } + + /// + /// The OnProjectUrlClicked. + /// + /// The obj. + private void OnProjectUrlClicked(object obj) + { + ProcessHelper.OpenUrl(this.ProjectUrl); + } + + /// + /// The OnTwitter. + /// + /// The obj. + private void OnTwitter(object obj) + { + ProcessHelper.OpenUrl(@"https://twitter.com/txtnurcahyo"); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/ViewModels/DownloadFlyoutViewModel.cs b/VideoBrowser/ViewModels/DownloadFlyoutViewModel.cs new file mode 100644 index 0000000..a486763 --- /dev/null +++ b/VideoBrowser/ViewModels/DownloadFlyoutViewModel.cs @@ -0,0 +1,94 @@ +namespace VideoBrowser.ViewModels +{ + using System; + using System.Collections.ObjectModel; + using System.Collections.Specialized; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Extensions; + + /// + /// Defines the . + /// + public class DownloadFlyoutViewModel : NotifyPropertyChanged + { + #region Fields + + private bool _isOpen; + + private string _message; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The models. + internal DownloadFlyoutViewModel(ObservableCollection models) + { + this.DownloadItemModels = models; + this.DownloadItemModels.CollectionChanged += this.OnDownloadItemModels_CollectionChanged; + this.ShowDownloadTabCommand = new RelayCommand(this.OnShowDownloadTab, nameof(this.ShowDownloadTabCommand)); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets or sets the DownloadItemModels. + /// + public ObservableCollection DownloadItemModels { get; internal set; } + + /// + /// Gets or sets a value indicating whether IsOpen. + /// + public bool IsOpen { get => _isOpen; set => this.Set(this.PropertyChangedHandler, ref _isOpen, value); } + + /// + /// Gets or sets the Message. + /// + public string Message { get => _message; internal set => this.Set(this.PropertyChangedHandler, ref _message, value); } + + /// + /// Gets or sets the ShowDownloadTabAction. + /// + public Action ShowDownloadTabAction { get; set; } + + /// + /// Gets the ShowDownloadTabCommand. + /// + public ICommand ShowDownloadTabCommand { get; } + + #endregion Properties + + #region Methods + + /// + /// The OnDownloadItemModels_CollectionChanged. + /// + /// The sender. + /// The e. + private void OnDownloadItemModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + this.IsOpen = true; + } + } + + /// + /// The OnShowDownloadTab. + /// + /// The obj. + private void OnShowDownloadTab(object obj) + { + this.ShowDownloadTabAction?.Invoke(); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/ViewModels/DownloadQueueViewModel.cs b/VideoBrowser/ViewModels/DownloadQueueViewModel.cs new file mode 100644 index 0000000..98f86ab --- /dev/null +++ b/VideoBrowser/ViewModels/DownloadQueueViewModel.cs @@ -0,0 +1,142 @@ +namespace VideoBrowser.ViewModels +{ + using System; + using System.Collections.ObjectModel; + using System.ComponentModel; + using System.IO; + using System.Windows.Data; + using System.Windows.Input; + using System.Windows.Media; + using System.Windows.Threading; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.Models; + using VideoBrowser.Core; + using VideoBrowser.Helpers; + using VideoBrowser.Models; + using VideoBrowser.Resources; + + /// + /// Defines the . + /// + public class DownloadQueueViewModel + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The downloadItemModels. + internal DownloadQueueViewModel(ObservableCollection downloadItemModels) + { + this.DownloadItemModels = downloadItemModels; + this.OperationCollectionView = CollectionViewSource.GetDefaultView(this.DownloadItemModels); + this.RemoveOperationCommand = new RelayCommand(this.OnRemoveOperation); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the DownloadItemModels. + /// + public ObservableCollection DownloadItemModels { get; } + + /// + /// Gets or sets the Icon. + /// + public Geometry Icon { get; set; } = Icons.Download; + + /// + /// Gets the OperationCollectionView. + /// + public ICollectionView OperationCollectionView { get; } + + /// + /// Gets the RemoveOperationCommand. + /// + public ICommand RemoveOperationCommand { get; } + + /// + /// Gets or sets the ShowMessageAsync. + /// + public Action ShowMessageAsync { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The Download. + /// + /// The operation. + public void Download(Operation operation) + { + var operationModel = new OperationModel(operation) { PauseDownloadAction = this.OnPauseDownloadCalled, CancelDownloadAction = this.OnCancelDownloadCalled }; + var element = (DispatcherObject)this.OperationCollectionView; + element.InvokeAsync(() => + { + if (!this.DownloadItemModels.Contains(operationModel)) + { + this.DownloadItemModels.Insert(0, operationModel); + DownloadQueueHandler.Add(operation); + } + else + { + var output = Path.GetFileName(operationModel.Operation.Output); + this.ShowMessageAsync("Download Canceled", $"The video/audio {output} is already downloaded"); + } + }); + } + + /// + /// The OnCancelDownloadCalled. + /// + /// The model. + internal void OnCancelDownloadCalled(DownloadItemModel model) + { + model.Dispose(); + this.DownloadItemModels.Remove(model); + } + + /// + /// The OnPauseDownloadCalled. + /// + /// The model. + internal void OnPauseDownloadCalled(DownloadItemModel model) + { + if (!(model is OperationModel operationModel)) + { + return; + } + + var operation = operationModel.Operation; + var status = operation.Status; + if (status != OperationStatus.Paused && status != OperationStatus.Queued && status != OperationStatus.Working) + { + return; + } + + if (status == OperationStatus.Paused) + { + operation.Resume(); + model.PauseText = "Pause"; + } + else + { + operation.Pause(); + model.PauseText = "Resume"; + } + } + + /// + /// The OnRemoveVideo. + /// + /// The obj. + private void OnRemoveOperation(object obj) + { + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/ViewModels/MainWindowViewModel.cs b/VideoBrowser/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..f87a5c9 --- /dev/null +++ b/VideoBrowser/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,250 @@ +namespace VideoBrowser.ViewModels +{ + using Dragablz; + using System; + using System.Linq; + using System.Windows; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Core; + using VideoBrowser.Models; + using VideoBrowser.Views; + + /// + /// Defines the . + /// + public class MainWindowViewModel : NotifyPropertyChanged + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The globalData. + /// The globalBrowserData. + public MainWindowViewModel(GlobalBrowserData globalBrowserData) + { + this.ClosingCommand = new RelayCommand(this.OnClosing); + this.LoadedCommand = new RelayCommand(this.OnLoaded); + this.PressEscCommand = new RelayCommand(this.OnPressEsc); + this.PressF2Command = new RelayCommand(this.OnPressF2); + this.About = new AboutViewModel(); + var downloadItemModels = globalBrowserData.DownloadItemModels; + this.DownloadQueueViewModel = new DownloadQueueViewModel(downloadItemModels) { ShowMessageAsync = this.ShowMessageAsync }; + this.DownloadFlyoutViewModel = new DownloadFlyoutViewModel(downloadItemModels) { ShowDownloadTabAction = this.ShowDownloadTabAction }; + this.WebBrowserTabControlViewModel = new WebBrowserTabControlViewModel(globalBrowserData) + { + CreateBrowserFunc = this.CreateBrowser + }; + + this.Initialize(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the About. + /// + public AboutViewModel About { get; } + + /// + /// Gets the CefWindowData. + /// + public CefWindowData CefWindowData => this.WebBrowserTabControlViewModel.CefWindowData; + + /// + /// Gets the ClosingCommand. + /// + public RelayCommand ClosingCommand { get; } + + /// + /// Gets the DownloadFlyoutViewModel. + /// + public DownloadFlyoutViewModel DownloadFlyoutViewModel { get; } + + /// + /// Gets the DownloadQueueViewModel. + /// + public DownloadQueueViewModel DownloadQueueViewModel { get; } + + /// + /// Gets the GlobalBrowserData. + /// + public GlobalBrowserData GlobalBrowserData => this.WebBrowserTabControlViewModel.GlobalBrowserData; + + /// + /// Gets the LoadedCommand. + /// + public ICommand LoadedCommand { get; } + + /// + /// Gets the PressEscCommand. + /// + public ICommand PressEscCommand { get; } + + /// + /// Gets the PressF2Command. + /// + public ICommand PressF2Command { get; } + + /// + /// Gets the Settings. + /// + public SettingsViewModel Settings => this.GlobalBrowserData.Settings; + + /// + /// Gets the Title. + /// + public string Title => $"{AppEnvironment.Name}"; + + /// + /// Gets the WebBrowserTabControlViewModel. + /// + public WebBrowserTabControlViewModel WebBrowserTabControlViewModel { get; } + + /// + /// Gets the DownloadAction. + /// + private Action DownloadAction => this.DownloadQueueViewModel.Download; + + #endregion Properties + + #region Methods + + /// + /// The CreateBrowser. + /// + /// The . + internal TabItem CreateBrowser() + { + var model = new WebBrowserHeaderedItemViewModel(this.GlobalBrowserData, this.CefWindowData, this.DownloadAction); + return model; + } + + /// + /// The Dispose. + /// + private void Dispose() + { + this.WebBrowserTabControlViewModel.Dispose(); + var viewModels = this.GlobalBrowserData.WindowViewModels; + if (viewModels.Contains(this)) + { + viewModels.Remove(this); + } + + if (!viewModels.Any()) + { + this.GlobalBrowserData.Dispose(); + } + } + + /// + /// The Initialize. + /// + private void Initialize() + { + var args = AppEnvironment.Arguments; + var url = Properties.Settings.Default.LastUrl; + if (args != null && args.Any()) + { + url = args.First(); + } + + if (this.WebBrowserTabControlViewModel.TabItems.Any()) + { + var browser = this.WebBrowserTabControlViewModel.TabItems.First(); + if (browser is WebBrowserHeaderedItemViewModel model) + { + model.VideoBrowserViewModel.NavigateUrl = url; + } + } + } + + /// + /// The OnClosing. + /// + /// The obj. + private void OnClosing(object obj) + { + var (_, _, commandParameter) = (ValueTuple)obj; + var window = (Window)commandParameter; + var settings = Properties.Settings.Default; + settings.WindowPosition = new Point(window.Left, window.Top); + settings.WindowWidth = window.ActualWidth; + settings.WindowHeight = window.Height; + settings.WindowState = window.WindowState; + settings.Save(); + DownloadQueueHandler.Stop(); + this.Dispose(); + } + + /// + /// The OnLoaded. + /// + /// The obj. + private void OnLoaded(object obj) + { + var (_, _, commandParameter) = (ValueTuple)obj; + var settings = Properties.Settings.Default; + var window = (MainWindow)commandParameter; + window.Left = settings.WindowPosition.X; + window.Top = settings.WindowPosition.Y; + window.Width = settings.WindowWidth; + window.Height = settings.WindowHeight; + window.WindowState = settings.WindowState; + this.CefWindowData.MainWindow = window; + DownloadQueueHandler.LimitDownloads = settings.ShowMaxSimDownloads; + DownloadQueueHandler.StartWatching(settings.MaxSimDownloads); + } + + /// + /// The OnPressEsc. + /// + /// The obj. + private void OnPressEsc(object obj) + { + // Leave full screen. + if (this.CefWindowData.IsFullScreen) + { + this.CefWindowData.IsFullScreenCommand.Execute(false); + } + } + + /// + /// The OnPressF2. + /// + /// The obj. + private void OnPressF2(object obj) + { + var window = this.CefWindowData.MainWindow; + window.Width = 1022; + window.Height = 900; + } + + /// + /// The ShowDownloadTabAction. + /// + private void ShowDownloadTabAction() + { + var button = this.GlobalBrowserData.GetAddInButton(typeof(DownloadQueueButton)); + button?.Command.Execute(this.WebBrowserTabControlViewModel); + } + + /// + /// The ShowMessageAsync. + /// + /// The title. + /// The message. + private void ShowMessageAsync(string title, string message) + { + this.CefWindowData.ShowMessageAsync(title, message); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/ViewModels/UrlEditorViewModel.cs b/VideoBrowser/ViewModels/UrlEditorViewModel.cs new file mode 100644 index 0000000..5d23cd2 --- /dev/null +++ b/VideoBrowser/ViewModels/UrlEditorViewModel.cs @@ -0,0 +1,386 @@ +namespace VideoBrowser.ViewModels +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using System.Windows.Input; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser.ViewModels; + using VideoBrowser.Core; + using VideoBrowser.Extensions; + using VideoBrowser.Helpers; + + /// + /// Defines the . + /// + public class UrlEditorViewModel : NotifyPropertyChanged, IDisposable + { + #region Fields + + private string _duration; + + private string _fileName; + + private string _fileSize; + + private IList _formats; + + private string _imageUrl; + + private bool _isBusy; + + private bool _isDownloadable; + + private bool _isFocused; + + private bool _isFormatComboBoxVisible; + + private bool _isTextBoxFocused; + + private bool _isVisible; + + private string _navigateUrl; + + private VideoFormat _selectedFormat; + + private int _selectedFormatIndex; + + private string _url; + + private VideoInfo _videoInfo; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The reader. + /// The settings. + internal UrlEditorViewModel(UrlReader reader, SettingsViewModel settings) + { + this.UrlReader = reader; + this.UrlReader.PropertyChanged += this.OnUrlReader_PropertyChanged; + this.Settings = settings; + this.DownloadCommand = new RelayCommand(this.OnDownload); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the DownloadCommand. + /// + public ICommand DownloadCommand { get; } + + /// + /// Gets or sets the Duration. + /// + public string Duration { get => this._duration; internal set => this.Set(this.PropertyChangedHandler, ref this._duration, value); } + + /// + /// Gets or sets the FileName. + /// + public string FileName { get => this._fileName; internal set => this.Set(this.PropertyChangedHandler, ref this._fileName, value); } + + /// + /// Gets or sets the FileSize. + /// + public string FileSize { get => _fileSize; internal set => this.Set(this.PropertyChangedHandler, ref _fileSize, value); } + + /// + /// Gets or sets the Formats. + /// + public IList Formats { get => this._formats; internal set => this.Set(this.PropertyChangedHandler, ref this._formats, value); } + + /// + /// Gets the GetFolderCommand. + /// + public ICommand GetFolderCommand => this.Settings.GetFolderCommand; + + /// + /// Gets or sets the ImageUrl. + /// + public string ImageUrl { get => this._imageUrl; set => this.Set(this.PropertyChangedHandler, ref this._imageUrl, value); } + + /// + /// Gets or sets a value indicating whether IsBusy. + /// + public bool IsBusy { get => _isBusy; set => this.Set(this.PropertyChangedHandler, ref _isBusy, value); } + + /// + /// Gets or sets a value indicating whether IsDownloadable. + /// + public bool IsDownloadable { get => _isDownloadable; set => this.Set(this.PropertyChangedHandler, ref _isDownloadable, value); } + + /// + /// Gets or sets a value indicating whether IsFocused. + /// + public bool IsFocused + { + get => _isFocused; + set + { + if (_isFocused == value) + { + return; + } + + _isFocused = value; + this.InvokePropertiesChanged(this.OnPropertyChanged, nameof(this.IsFocused), nameof(this.IsVisible)); + } + } + + /// + /// Gets or sets a value indicating whether IsFormatComboBoxVisible. + /// + public bool IsFormatComboBoxVisible { get => _isFormatComboBoxVisible; set => this.Set(this.PropertyChangedHandler, ref _isFormatComboBoxVisible, value); } + + /// + /// Gets or sets a value indicating whether IsTextBoxFocused. + /// + public bool IsTextBoxFocused { get => _isTextBoxFocused; set => this.Set(this.PropertyChangedHandler, ref _isTextBoxFocused, value); } + + /// + /// Gets or sets a value indicating whether IsVisible. + /// + public bool IsVisible { get => this._isVisible && this.IsFocused; internal set => this.Set(this.PropertyChangedHandler, ref this._isVisible, value); } + + /// + /// Gets or sets the NavigateUrl. + /// + public string NavigateUrl + { + get => this._navigateUrl; + set + { + if (this._navigateUrl == value) + { + return; + } + + this._navigateUrl = value; + this.IsDownloadable = false; + if (this.UrlReader.IsDownloadable) + { + this.IsBusy = true; + Task.Run(() => + { + this.VideoInfo = YoutubeDl.GetVideoInfo(this.NavigateUrl); + }).ContinueWith(this.LoadVideoInfo); + } + + this.IsVisible = this.UrlReader.IsDownloadable; + this.OnPropertyChanged(); + } + } + + /// + /// Gets or sets the NavigateUrlCommand. + /// + public ICommand NavigateUrlCommand { get; internal set; } + + /// + /// Gets or sets the SelectedFormat. + /// + public VideoFormat SelectedFormat + { + get => this._selectedFormat; + set + { + if (!this.Set(this.PropertyChangedHandler, ref this._selectedFormat, value)) + { + return; + } + + this.UpdateFileSize(); + } + } + + /// + /// Gets or sets the SelectedFormatIndex. + /// + public int SelectedFormatIndex { get => _selectedFormatIndex; set => this.Set(this.PropertyChangedHandler, ref _selectedFormatIndex, value); } + + /// + /// Gets the Settings. + /// + public SettingsViewModel Settings { get; } + + /// + /// Gets or sets the Url. + /// + public string Url { get => this._url; set => this.Set(this.PropertyChangedHandler, ref this._url, value); } + + /// + /// Gets the UrlReader. + /// + public UrlReader UrlReader { get; } + + /// + /// Gets or sets the VideoInfo. + /// + public VideoInfo VideoInfo + { + get => this._videoInfo; + set + { + if (this.VideoInfo != null) + { + this.VideoInfo.FileSizeUpdated -= this.OnVideoInfo_FileSizeUpdated; + } + + this.Set(this.PropertyChangedHandler, ref this._videoInfo, value); + } + } + + /// + /// Gets or sets the DownloadAction. + /// + internal Action DownloadAction { get; set; } + + /// + /// Gets or sets the ShowMessageAsyncAction. + /// + internal Action ShowMessageAsyncAction { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The Dispose. + /// + public void Dispose() + { + this.UrlReader.PropertyChanged -= this.OnUrlReader_PropertyChanged; + } + + /// + /// The LoadVideoInfo. + /// + /// The task. + private void LoadVideoInfo(Task task) + { + this.IsBusy = false; + this.IsFocused = true; + this.IsTextBoxFocused = true; + this.IsDownloadable = false; + this.IsFormatComboBoxVisible = false; + + if (this.VideoInfo.RequiresAuthentication) + { + ////var auth = Dialogs.LoginDialog.Show(this); + } + else if (this.VideoInfo.Failure) + { + var message = "Couldn't retrieve video. Reason:\n\n" + this.VideoInfo.FailureReason; + this.ShowMessageAsyncAction.Invoke("The Video Not Downloadable", message); + Logger.Info(message); + return; + } + else + { + this.FileName = YoutubeHelper.FormatTitle(this.VideoInfo.Title); + this.VideoInfo.FileSizeUpdated += this.OnVideoInfo_FileSizeUpdated; + this.Duration = this.VideoInfo.Duration.FormatVideoLength(); + this.Formats = YoutubeHelper.CheckFormats(this.VideoInfo.Formats); + this.ImageUrl = this.VideoInfo.ThumbnailUrl; + if (this.Formats.Any()) + { + this.SelectedFormatIndex = this.Formats.Count - 1; + } + else + { + return; + } + } + + this.IsFormatComboBoxVisible = this.VideoInfo.Formats.Count > 0; + this.IsDownloadable = true; + Properties.Settings.Default.LastUrl = this.NavigateUrl; + } + + /// + /// The OnDownload. + /// + /// The o. + private void OnDownload(object o) + { + var format = this.SelectedFormat; + var formatTitle = format.AudioOnly ? format.AudioBitRate.ToString() : format.Format.ToString(); + this.FileName = FileHelper.GetValidFilename(this.FileName); + var fileName = $"{this.FileName}-{formatTitle}.{format.Extension}"; + var output = Path.Combine(this.Settings.OutputFolder, fileName); + if (File.Exists(output)) + { + var message = $@"File ""{fileName}"" is already downloaded. If it was failed, delete it and try again."; + this.ShowMessageAsyncAction("Download Canceled", message); + return; + } + + DownloadOperation operation = format.AudioOnly || format.HasAudioAndVideo + ? new DownloadOperation(format, output) + : new DownloadOperation(format, YoutubeHelper.GetAudioFormat(format), output); + Task.Run(() => this.DownloadAction?.Invoke(operation)); + } + + /// + /// The OnUrlReader_PropertyChanged. + /// + /// The sender. + /// The e. + private void OnUrlReader_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(this.UrlReader.IsDownloadable): + break; + } + } + + /// + /// The OnVideoInfo_FileSizeUpdated. + /// + /// The sender. + /// The e. + private void OnVideoInfo_FileSizeUpdated(object sender, FileSizeUpdateEventArgs e) + { + this.FileSize = FormatString.FormatFileSize(e.VideoFormat.FileSize); + } + + /// + /// The UpdateFileSize. + /// + private void UpdateFileSize() + { + if (this.SelectedFormat == null) + { + this.FileSize = "Unkown size"; + } + else if (this.SelectedFormat.FileSize == 0) + { + this.FileSize = "Getting file size..."; + } + else + { + var total = this.SelectedFormat.FileSize; + + // If the format is VideoOnly, combine audio and video size. + if (this.SelectedFormat.VideoOnly) + { + total += YoutubeHelper.GetAudioFormat(this.SelectedFormat).FileSize; + } + + this.FileSize = FormatString.FormatFileSize(total); + } + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/ViewModels/VideoViewModel.cs b/VideoBrowser/ViewModels/VideoViewModel.cs new file mode 100644 index 0000000..fed3010 --- /dev/null +++ b/VideoBrowser/ViewModels/VideoViewModel.cs @@ -0,0 +1,41 @@ +namespace VideoBrowser.ViewModels +{ + using System.Windows.Media; + using VideoBrowser.Common; + using VideoBrowser.Extensions; + using VideoBrowser.Models; + using VideoBrowser.Resources; + + /// + /// Defines the + /// + public class VideoViewModel : NotifyPropertyChanged + { + #region Fields + + private double _downloadProgress; + + private Video _video; + + #endregion Fields + + #region Properties + + /// + /// Gets or sets the DownloadProgress + /// + public double DownloadProgress { get => this._downloadProgress; set => this.Set(this.PropertyChangedHandler, ref this._downloadProgress, value); } + + /// + /// Gets or sets the Icon + /// + public Geometry Icon { get; set; } = Icons.SearchVideo; + + /// + /// Gets or sets the Video + /// + public Video Video { get => this._video; set => this.Set(this.PropertyChangedHandler, ref this._video, value); } + + #endregion Properties + } +} \ No newline at end of file diff --git a/VideoBrowser/Views/AboutView.xaml b/VideoBrowser/Views/AboutView.xaml new file mode 100644 index 0000000..16e72d1 --- /dev/null +++ b/VideoBrowser/Views/AboutView.xaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Views/AboutView.xaml.cs b/VideoBrowser/Views/AboutView.xaml.cs new file mode 100644 index 0000000..3a6eedf --- /dev/null +++ b/VideoBrowser/Views/AboutView.xaml.cs @@ -0,0 +1,22 @@ +namespace VideoBrowser.Views +{ + using System.Windows.Controls; + + /// + /// Interaction logic for AboutView.xaml. + /// + public partial class AboutView : UserControl + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public AboutView() + { + InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Views/DownloadFlyoutView.xaml b/VideoBrowser/Views/DownloadFlyoutView.xaml new file mode 100644 index 0000000..199f804 --- /dev/null +++ b/VideoBrowser/Views/DownloadFlyoutView.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Views/DownloadQueueView.xaml.cs b/VideoBrowser/Views/DownloadQueueView.xaml.cs new file mode 100644 index 0000000..ff07105 --- /dev/null +++ b/VideoBrowser/Views/DownloadQueueView.xaml.cs @@ -0,0 +1,20 @@ +namespace VideoBrowser.Views +{ + /// + /// Interaction logic for DownloadQueueView.xaml + /// + public partial class DownloadQueueView + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public DownloadQueueView() + { + this.InitializeComponent(); + } + + #endregion Constructors + } +} \ No newline at end of file diff --git a/VideoBrowser/Views/MainWindow.xaml b/VideoBrowser/Views/MainWindow.xaml new file mode 100644 index 0000000..fb6d7d0 --- /dev/null +++ b/VideoBrowser/Views/MainWindow.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VideoBrowser/Views/MainWindow.xaml.cs b/VideoBrowser/Views/MainWindow.xaml.cs new file mode 100644 index 0000000..2b1426d --- /dev/null +++ b/VideoBrowser/Views/MainWindow.xaml.cs @@ -0,0 +1,128 @@ +namespace VideoBrowser.Views +{ + using Dragablz; + using System.Windows; + using VideoBrowser.Common; + using VideoBrowser.Controls.CefSharpBrowser; + using VideoBrowser.Extensions; + using VideoBrowser.Helpers; + using VideoBrowser.Models; + using VideoBrowser.ViewModels; + + /// + /// Interaction logic for MainWindow.xaml. + /// + public partial class MainWindow + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public MainWindow() : this(new GlobalBrowserData()) + { + // This constructor is called once per application instance. + var addIns = this.GlobalBrowserData.AddInButtons; + addIns.Add(new DownloadQueueButton(this.GlobalBrowserData.DownloadItemModels)); + addIns.Add(new OpenOutputFolderButton(this.GlobalBrowserData.Settings)); + addIns.Add(new SettingsButton(this.GlobalBrowserData.Settings)); + addIns.Add(new AboutButton()); + if (DebugHelper.IsDebug) + { + addIns.Add(new TestButton()); + } + + // Register create browser tab. + this.GlobalBrowserData.InterTabClient.CreateWindow = this.CreateWindow; + + // Add the first browser tab. + var browserTabModel = this.MainWindowViewModel.WebBrowserTabControlViewModel; + browserTabModel.TabItems.Add(this.MainWindowViewModel.CreateBrowser()); + } + + /// + /// Initializes a new instance of the class. + /// + /// The globalBrowserData. + internal MainWindow(GlobalBrowserData globalBrowserData) + { + // This constructor is intended to create new window after dragging the browser tab. + Logger.Info($"Start {nameof(VideoBrowser)}"); + this.GlobalBrowserData = globalBrowserData; + this.MainWindowViewModel = new MainWindowViewModel(globalBrowserData); + this.MainWindowViewModel.CefWindowData.PropertyChanged += this.CefWindowData_PropertyChanged; + this.GlobalBrowserData.WindowViewModels.Add(this.MainWindowViewModel); + this.DataContext = this.MainWindowViewModel; + this.InitializeComponent(); + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the GlobalBrowserData. + /// + internal GlobalBrowserData GlobalBrowserData { get; } + + /// + /// Gets or sets the LastWindowState. + /// + private WindowState LastWindowState { get; set; } + + /// + /// Gets or sets the LastWindowStyle. + /// + private WindowStyle LastWindowStyle { get; set; } + + /// + /// Gets or sets the MainWindowViewModel. + /// + private MainWindowViewModel MainWindowViewModel { get; set; } + + #endregion Properties + + #region Methods + + /// + /// The OnGlobalData_PropertyChanged. + /// + /// The sender. + /// The e. + private void CefWindowData_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var cefWindowData = this.MainWindowViewModel.CefWindowData; + if (e.IsMatch(nameof(cefWindowData.IsFullScreen))) + { + if (cefWindowData.IsFullScreen) + { + this.LastWindowState = this.WindowState; + this.LastWindowStyle = this.WindowStyle; + this.WindowState = WindowState.Maximized; + this.WindowStyle = WindowStyle.None; + this.ShowTitleBar = false; + } + else + { + this.WindowState = this.LastWindowState; + this.WindowStyle = this.LastWindowStyle; + this.ShowTitleBar = true; + } + } + } + + /// + /// The CreateWindow. + /// + /// The . + private (Window, TabablzControl) CreateWindow() + { + var viewModel = new MainWindowViewModel(this.GlobalBrowserData); + var window = new MainWindow(this.GlobalBrowserData) { DataContext = viewModel }; + var initialTabablzControl = window.WebBrowserTabControlView.InitialTabablzControl; + return (window, initialTabablzControl); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/VideoBrowser/Views/UrlEditorView.xaml b/VideoBrowser/Views/UrlEditorView.xaml new file mode 100644 index 0000000..87ca328 --- /dev/null +++ b/VideoBrowser/Views/UrlEditorView.xaml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +