From a0283d61687ae52aad23b96ecf912bcb58643740 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 2 Oct 2019 14:55:44 +0100 Subject: [PATCH 01/29] Implementation using WebViewControl - issues with Loopback policy. xibosignage/xibo#86 --- Control/EmbeddedServer.cs | 6 +- Control/Region.cs | 8 +- Logic/ApplicationSettings.cs | 1 + Logic/MediaOption.cs | 9 + Media/EdgeWebMedia.cs | 160 +++++++++++ Media/HtmlPackage.cs | 102 ------- Media/IeWebMedia.cs | 334 +--------------------- Media/WebMedia.cs | 476 +++++++++++++++++++++++++++++++ Properties/Resources.Designer.cs | 2 +- Properties/Settings.Designer.cs | 2 +- Web References/xmds/Reference.cs | 46 +-- XiboClient.csproj | 55 ++-- app.config | 2 +- default.config.xml | 1 + packages.config | 17 +- 15 files changed, 739 insertions(+), 482 deletions(-) create mode 100644 Media/EdgeWebMedia.cs delete mode 100644 Media/HtmlPackage.cs create mode 100644 Media/WebMedia.cs diff --git a/Control/EmbeddedServer.cs b/Control/EmbeddedServer.cs index 561dd279..765cc292 100644 --- a/Control/EmbeddedServer.cs +++ b/Control/EmbeddedServer.cs @@ -54,9 +54,9 @@ public void Run() { Dictionary headers = new Dictionary() { - { Constants.HeaderCacheControl, "no-cache, no-store, must-revalidate" }, - { Constants.HeaderPragma, "no-cache" }, - { Constants.HeaderExpires, "0" } + { "Cache-Control", "no-cache, no-store, must-revalidate" }, + { "Pragma", "no-cache" }, + { "Expires", "0" } }; server.RegisterModule(new StaticFilesModule(ApplicationSettings.Default.LibraryPath, headers)); diff --git a/Control/Region.cs b/Control/Region.cs index 19812734..213f8fff 100644 --- a/Control/Region.cs +++ b/Control/Region.cs @@ -610,7 +610,7 @@ private Media CreateNextMediaNode(RegionOptions options) if (options.render == "html") { - media = new IeWebMedia(options); + media = WebMedia.GetConfiguredWebMedia(options); } else { @@ -658,8 +658,7 @@ private Media CreateNextMediaNode(RegionOptions options) case "ticker": case "text": case "webpage": - media = new IeWebMedia(options); - + media = WebMedia.GetConfiguredWebMedia(options); break; case "flash": @@ -672,7 +671,8 @@ private Media CreateNextMediaNode(RegionOptions options) break; case "htmlpackage": - media = new HtmlPackage(options); + media = WebMedia.GetConfiguredWebMedia(options); + ((WebMedia)media).ConfigureForHtmlPackage(); break; default: diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 95637838..3f5481ab 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -502,6 +502,7 @@ public bool InDownloadWindow public int MaxConcurrentDownloads { get; set; } public int ScreenShotRequestInterval { get; set; } public int ScreenShotSize { get; set; } + public string BrowserType { get; set; } private int _maxLogFileUploads; public int MaxLogFileUploads { get { return ((_maxLogFileUploads == 0) ? 10 : _maxLogFileUploads); } set { _maxLogFileUploads = value; } } diff --git a/Logic/MediaOption.cs b/Logic/MediaOption.cs index ceb220d7..2b4eea4f 100644 --- a/Logic/MediaOption.cs +++ b/Logic/MediaOption.cs @@ -49,6 +49,15 @@ public void Add(string name, string value) _options.Add(option); } + public void Replace(string name, string value) + { + int optIndex = _options.FindIndex(o => o.Name == name); + if (optIndex > -1) + _options.RemoveAt(optIndex); + + Add(name, value); + } + public void Clear() { _options.Clear(); diff --git a/Media/EdgeWebMedia.cs b/Media/EdgeWebMedia.cs new file mode 100644 index 00000000..3d9f9843 --- /dev/null +++ b/Media/EdgeWebMedia.cs @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2019 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using Microsoft.Toolkit.Forms.UI.Controls; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient +{ + class EdgeWebMedia : WebMedia + { + private bool _disposed; + + private WebView mWebView; + + public EdgeWebMedia(RegionOptions options) + : base(options) + { + } + + /// + /// Render Media + /// + public override void RenderMedia() + { + // Create the web view we will use + mWebView = new WebView(); + + ((ISupportInitialize)mWebView).BeginInit(); + + mWebView.Dock = System.Windows.Forms.DockStyle.Fill; + mWebView.Size = Size; + mWebView.Visible = false; + mWebView.IsPrivateNetworkClientServerCapabilityEnabled = true; + mWebView.NavigationCompleted += MWebView_NavigationCompleted; + + Controls.Add(mWebView); + + ((ISupportInitialize)mWebView).EndInit(); + + // _webBrowser.ScrollBarsEnabled = false; + // _webBrowser.ScriptErrorsSuppressed = true; + + HtmlUpdatedEvent += IeWebMedia_HtmlUpdatedEvent; + + if (IsNativeOpen()) + { + // Navigate directly + mWebView.Navigate(_filePath); + } + else if (HtmlReady()) + { + // Write to temporary file + ReadControlMeta(); + + // Navigate to temp file + mWebView.Navigate(_localWebPath); + } + else + { + Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); + } + + // Render media shows the controls and starts timers, etc + base.RenderMedia(); + } + + private void MWebView_NavigationCompleted(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlNavigationCompletedEventArgs e) + { + + Debug.WriteLine("Navigate Completed to " + e.Uri + " " + e.WebErrorStatus.ToString(), "EdgeWebView"); + + if (e.IsSuccess) + { + DocumentCompleted(); + + if (!IsDisposed) + { + // Show the browser + mWebView.Visible = true; + } + } + else + { + Trace.WriteLine(new LogMessage("EdgeWebMedia", "Cannot navigate to " + e.Uri + ". e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); + + // This should exipre the media + Duration = 5; + base.RenderMedia(); + } + } + + private void IeWebMedia_HtmlUpdatedEvent(string url) + { + if (mWebView != null) + { + mWebView.Navigate(url); + } + } + + /// + /// Dispose of this text item + /// + /// + protected override void Dispose(bool disposing) + { + Debug.WriteLine("Disposing of " + _filePath, "IeWebMedia - Dispose"); + + if (disposing) + { + // Remove the webbrowser control + try + { + // Remove the web browser control + Controls.Remove(mWebView); + + // Workaround to remove COM object + PerformLayout(); + + // Detatch event and remove + if (mWebView != null && !_disposed) + { + mWebView.NavigationCompleted -= MWebView_NavigationCompleted; + mWebView.Dispose(); + + _disposed = true; + } + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("IeWebMedia - Dispose", "Cannot dispose of web browser. E = " + e.Message), LogType.Info.ToString()); + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Media/HtmlPackage.cs b/Media/HtmlPackage.cs deleted file mode 100644 index b3a727f0..00000000 --- a/Media/HtmlPackage.cs +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (C) 2019 Xibo Signage Ltd - * - * Xibo - Digital Signage - http://www.xibo.org.uk - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Windows.Forms; - -namespace XiboClient -{ - class HtmlPackage : IeWebMedia - { - public HtmlPackage(RegionOptions options) - : base(options) - { - string pathToMediaFile = Path.Combine(ApplicationSettings.Default.LibraryPath, options.uri); - string pathToPackageFolder = Path.Combine(ApplicationSettings.Default.LibraryPath, "package_" + options.FileId); - string pathToStatusFile = Path.Combine(pathToPackageFolder, "_updated"); - - // Configure the file path to indicate which file should be opened by the browser - _filePath = ApplicationSettings.Default.EmbeddedServerAddress + "package_" + options.FileId + "/" + options.Dictionary.Get("nominatedFile", "index.html"); - - // Check to see if our package has been extracted already - // if not, then extract it - if (!(Directory.Exists(pathToPackageFolder) && IsUpdated(pathToStatusFile, File.GetLastWriteTime(pathToMediaFile)))) - { - // Extract our file into the specified folder. - ZipFile.ExtractToDirectory(pathToMediaFile, pathToPackageFolder); - - // Add in our extraction date. - WriteUpdatedFlag(pathToStatusFile); - } - } - - protected override bool IsNativeOpen() - { - return true; - } - - /// - /// Updated Flag - /// - /// - private void WriteUpdatedFlag(string path) - { - try - { - File.WriteAllText(path, DateTime.Now.ToString()); - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("HtmlPackage", "WriteUpdatedFlag: Failed to update status file: " + path + ". e = " + e.Message), LogType.Error.ToString()); - } - } - - /// - /// Check whether we've updated recently - /// - /// - /// - /// - private bool IsUpdated(string path, DateTime lastModified) - { - // Check that it is up to date by looking for our special file. - try - { - string flag = File.ReadAllText(path); - DateTime updated = DateTime.Parse(flag); - - return updated > lastModified; - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("HtmlPackage", "IsUpdated: Failed to read status file: " + path + ". e = " + e.Message), LogType.Error.ToString()); - return false; - } - } - } -} diff --git a/Media/IeWebMedia.cs b/Media/IeWebMedia.cs index 34e404ab..c1bb705f 100644 --- a/Media/IeWebMedia.cs +++ b/Media/IeWebMedia.cs @@ -21,54 +21,22 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; -using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Windows.Forms; namespace XiboClient { - class IeWebMedia : Media + class IeWebMedia : WebMedia { - private bool _disposed = false; - protected string _filePath; - private string _localWebPath; - private RegionOptions _options; + private bool _disposed; + private WebBrowser _webBrowser; - private int _documentCompletedCount = 0; - private bool _reloadOnXmdsRefresh = false; public IeWebMedia(RegionOptions options) - : base(options.width, options.height, options.top, options.left) + : base(options) { - // Collect some options from the Region Options passed in - // and store them in member variables. - _options = options; - - // Set the file path/local web path - if (IsNativeOpen()) - { - // If we are modeid == 1, then just open the webpage without adjusting the file path - _filePath = Uri.UnescapeDataString(_options.uri).Replace('+', ' '); - } - else - { - // Set the file path - _filePath = ApplicationSettings.Default.LibraryPath + @"\" + _options.mediaid + ".htm"; - _localWebPath = ApplicationSettings.Default.EmbeddedServerAddress + _options.mediaid + ".htm"; - } - } - - /// - /// Is this a native open widget - /// - /// - protected virtual bool IsNativeOpen() - { - string modeId = _options.Dictionary.Get("modeid"); - return modeId != string.Empty && modeId == "1"; } /// @@ -84,6 +52,8 @@ public override void RenderMedia() _webBrowser.ScriptErrorsSuppressed = true; _webBrowser.Visible = false; + HtmlUpdatedEvent += IeWebMedia_HtmlUpdatedEvent; + if (IsNativeOpen()) { // Navigate directly @@ -113,296 +83,22 @@ public override void RenderMedia() /// /// /// - void _webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) - { - _documentCompletedCount++; - - // Prevent double document completions - if (_documentCompletedCount > 1) - return; - - // Start the timer - base.RestartTimer(); - - // Don't do anything if we are already disposed - if (_disposed) - return; - - // Show the browser - _webBrowser.Visible = true; - } - - /// - /// Is the cached HTML ready - /// - /// true if there is something to show, false if nothing - private bool HtmlReady() - { - // Check for cached resource files in the library - // We want to check the file exists first - if (!File.Exists(_filePath)) - { - // File doesn't exist at all. - _reloadOnXmdsRefresh = true; - - // Refresh - RefreshFromXmds(); - - // Return false - return false; - } - - // It exists - therefore we want to get the last time it was updated - DateTime lastWriteDate = File.GetLastWriteTime(_filePath); - - // Does it update every time? - if (_options.updateInterval == 0) - { - // Comment in to force a re-request with each reload of the widget - //_reloadOnXmdsRefresh = true; - - // File exists but needs updating - RefreshFromXmds(); - - // Comment in to force a re-request with each reload of the widget - //return false; - } - // Compare the last time it was updated to the layout modified time (always refresh when the layout has been modified) - // Also compare to the update interval (refresh if it has not been updated for longer than the update interval) - else if (_options.LayoutModifiedDate.CompareTo(lastWriteDate) > 0 || DateTime.Now.CompareTo(lastWriteDate.AddMinutes(_options.updateInterval)) > 0) - { - // File exists but needs updating. - RefreshFromXmds(); - } - else - { - // File exists and is in-date - nothing to do - } - - // Refresh the local file cache with any new dimensions, etc. - UpdateCacheIfNecessary(); - - return true; - } - - /// - /// Pulls the duration out of the temporary file and sets the media Duration to the same - /// - private void ReadControlMeta() - { - // read the contents of the file - using (StreamReader reader = new StreamReader(_filePath)) - { - string html = reader.ReadToEnd(); - - // Parse out the duration using a regular expression - try - { - Match match = Regex.Match(html, ""); - - if (match.Success) - { - // We have a match, so override our duration. - Duration = Convert.ToInt32(match.Groups[1].Value); - } - } - catch - { - Trace.WriteLine(new LogMessage("Html - ReadControlMeta", "Unable to pull duration using RegEx").ToString()); - } - } - } - - /// - /// Refresh the Local cache of the DataSetView HTML - /// - private void RefreshFromXmds() - { - xmds.xmds xmds = new XiboClient.xmds.xmds(); - xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=getResource"; - xmds.GetResourceCompleted += new XiboClient.xmds.GetResourceCompletedEventHandler(xmds_GetResourceCompleted); - - xmds.GetResourceAsync(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, _options.layoutId, _options.regionId, _options.mediaid, ApplicationSettings.Default.Version); - } - - /// - /// Refresh Complete - /// - /// - /// - private void xmds_GetResourceCompleted(object sender, XiboClient.xmds.GetResourceCompletedEventArgs e) + private void _webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) { - try - { - // Success / Failure - if (e.Error != null) - { - Trace.WriteLine(new LogMessage("xmds_GetResource", "Unable to get Resource: " + e.Error.Message), LogType.Info.ToString()); + DocumentCompleted(); - // We have failed to update from XMDS - // id we have been asked to reload on XmdsRefresh, check to see if we have a file to load, - // if not expire on a short timer. - if (_reloadOnXmdsRefresh) - { - if (File.Exists(_filePath)) - { - // Cached file to revert to - UpdateCacheIfNecessary(); - - // Navigate to the file - _webBrowser.Navigate(_localWebPath); - } - else - { - // No cache to revert to - Trace.WriteLine(new LogMessage("xmds_GetResource", "We haven't been able to download this widget and there isn't a pre-cached one to use. Skipping."), LogType.Info.ToString()); - - // Start the timer so that we expire - Duration = 2; - base.RenderMedia(); - } - } - } - else - { - // Ammend the resource file so that we can open it directly from the library (this is better than using a tempoary file) - string cachedFile = e.Result; - - // Handle the background - String bodyStyle; - String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); - - if (_options.backgroundImage == null || _options.backgroundImage == "") - { - bodyStyle = "background-color:" + backgroundColor + " ;"; - } - else - { - bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; - } - - string html = cachedFile.Replace("", ""); - html = html.Replace("[[ViewPortWidth]]", _width.ToString()); - html += ""; - html += ""; - - // Comment in to write out the update date at the end of the file (in the body) - // This is useful if you want to check how frequently the file is updating - //html = html.Replace("", "

" + DateTime.Now.ToString() + "

"); - - // Write to the library - using (FileStream fileStream = File.Open(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - using (StreamWriter sw = new StreamWriter(fileStream)) - { - sw.Write(html); - sw.Close(); - } - } - - if (_reloadOnXmdsRefresh) - { - // Read the control meta back out - ReadControlMeta(); - - // Handle Navigate in here because we will not have done it during first load - _webBrowser.Navigate(_localWebPath); - } - } - } - catch (ObjectDisposedException) - { - Trace.WriteLine(new LogMessage("WebMedia", "Retrived the resource, stored the document but the media has already expired."), LogType.Error.ToString()); - } - catch (Exception ex) + if (!IsDisposed) { - Trace.WriteLine(new LogMessage("WebMedia", "Unknown exception " + ex.Message), LogType.Error.ToString()); - - // This should exipre the media - Duration = 5; - base.RenderMedia(); + // Show the browser + _webBrowser.Visible = true; } } - /// - /// Updates the Cache File with the necessary client side injected items - /// - private void UpdateCacheIfNecessary() + private void IeWebMedia_HtmlUpdatedEvent(string url) { - // Ammend the resource file so that we can open it directly from the library (this is better than using a tempoary file) - string cachedFile = ""; - - using (FileStream fileStream = File.Open(_filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - using (StreamReader reader = new StreamReader(fileStream)) - { - cachedFile = reader.ReadToEnd(); - } - } - - // Compare the cached dimensions in the file with the dimensions now, and - // regenerate if they are different. - if (cachedFile.Contains("[[ViewPortWidth]]") || !ReadCachedViewPort(cachedFile).Equals(_width.ToString() + "x" + _height.ToString())) - { - // Regex out the existing replacement if present - cachedFile = Regex.Replace(cachedFile, "(.*)", ""); - cachedFile = Regex.Replace(cachedFile, "", ""); - cachedFile = Regex.Replace(cachedFile, "", ""); - cachedFile = Regex.Replace(cachedFile, "", ""); - - // Handle the background - String bodyStyle; - String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); - - if (_options.backgroundImage == null || _options.backgroundImage == "") - { - bodyStyle = "background-color:" + backgroundColor + " ;"; - } - else - { - bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; - } - - string html = cachedFile.Replace("", ""); - html = html.Replace("[[ViewPortWidth]]", _width.ToString()); - html += ""; - html += ""; - - // Write to the library - using (FileStream fileStream = File.Open(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - using (StreamWriter sw = new StreamWriter(fileStream)) - { - sw.Write(html); - sw.Close(); - } - } - } - } - - /// - /// Pulls the duration out of the temporary file and sets the media Duration to the same - /// - private string ReadCachedViewPort(string html) - { - // Parse out the duration using a regular expression - try - { - Match match = Regex.Match(html, ""); - - if (match.Success) - { - // We have a match, so override our duration. - return match.Groups[1].Value; - } - else - { - return string.Empty; - } - } - catch + if (_webBrowser != null) { - return string.Empty; + _webBrowser.Navigate(url); } } diff --git a/Media/WebMedia.cs b/Media/WebMedia.cs new file mode 100644 index 00000000..516e307c --- /dev/null +++ b/Media/WebMedia.cs @@ -0,0 +1,476 @@ +/** + * Copyright (C) 2019 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +namespace XiboClient +{ + abstract class WebMedia : Media + { + /// + /// The file path + /// + protected string _filePath; + + /// + /// The local web file path + /// + protected string _localWebPath; + + /// + /// Region Optiosn + /// + private RegionOptions _options; + + /// + /// Count of times DocumentComplete has been called + /// + private int _documentCompletedCount = 0; + + /// + /// Reload on Xmds Refresh or not. + /// + private bool _reloadOnXmdsRefresh = false; + + // Events + public delegate void HtmlUpdatedDelegate(string url); + public event HtmlUpdatedDelegate HtmlUpdatedEvent; + + // Class Methods + + /// + /// Constructor + /// + /// + public WebMedia(RegionOptions options) + : base(options.width, options.height, options.top, options.left) + { + // Collect some options from the Region Options passed in + // and store them in member variables. + _options = options; + + // Set the file path/local web path + if (IsNativeOpen()) + { + // If we are modeid == 1, then just open the webpage without adjusting the file path + _filePath = Uri.UnescapeDataString(_options.uri).Replace('+', ' '); + } + else + { + // Set the file path + _filePath = ApplicationSettings.Default.LibraryPath + @"\" + _options.mediaid + ".htm"; + _localWebPath = ApplicationSettings.Default.EmbeddedServerAddress + _options.mediaid + ".htm"; + } + } + + /// + /// Is this a native open widget + /// + /// + protected virtual bool IsNativeOpen() + { + string modeId = _options.Dictionary.Get("modeid"); + return modeId != string.Empty && modeId == "1"; + } + + /// + /// Configure this web media for use as a HTML package + /// + public void ConfigureForHtmlPackage() + { + // Force native rendering + _options.Dictionary.Replace("modeid", "1"); + + string pathToMediaFile = Path.Combine(ApplicationSettings.Default.LibraryPath, _options.uri); + string pathToPackageFolder = Path.Combine(ApplicationSettings.Default.LibraryPath, "package_" + _options.FileId); + string pathToStatusFile = Path.Combine(pathToPackageFolder, "_updated"); + + // Configure the file path to indicate which file should be opened by the browser + _filePath = ApplicationSettings.Default.EmbeddedServerAddress + "package_" + _options.FileId + "/" + _options.Dictionary.Get("nominatedFile", "index.html"); + + // Check to see if our package has been extracted already + // if not, then extract it + if (!(Directory.Exists(pathToPackageFolder) && IsUpdated(pathToStatusFile, File.GetLastWriteTime(pathToMediaFile)))) + { + // Extract our file into the specified folder. + ZipFile.ExtractToDirectory(pathToMediaFile, pathToPackageFolder); + + // Add in our extraction date. + WriteUpdatedFlag(pathToStatusFile); + } + } + + /// + /// Updated Flag + /// + /// + private void WriteUpdatedFlag(string path) + { + try + { + File.WriteAllText(path, DateTime.Now.ToString()); + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("HtmlPackage", "WriteUpdatedFlag: Failed to update status file: " + path + ". e = " + e.Message), LogType.Error.ToString()); + } + } + + /// + /// Check whether we've updated recently + /// + /// + /// + /// + private bool IsUpdated(string path, DateTime lastModified) + { + // Check that it is up to date by looking for our special file. + try + { + string flag = File.ReadAllText(path); + DateTime updated = DateTime.Parse(flag); + + return updated > lastModified; + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("HtmlPackage", "IsUpdated: Failed to read status file: " + path + ". e = " + e.Message), LogType.Error.ToString()); + return false; + } + } + + /// + /// Web Browser finished loading document + /// + protected void DocumentCompleted() + { + _documentCompletedCount++; + + // Prevent double document completions + if (_documentCompletedCount > 1) + return; + + // Start the timer + base.RestartTimer(); + } + + /// + /// Is the cached HTML ready + /// + /// true if there is something to show, false if nothing + protected bool HtmlReady() + { + // Check for cached resource files in the library + // We want to check the file exists first + if (!File.Exists(_filePath)) + { + // File doesn't exist at all. + _reloadOnXmdsRefresh = true; + + // Refresh + RefreshFromXmds(); + + // Return false + return false; + } + + // It exists - therefore we want to get the last time it was updated + DateTime lastWriteDate = File.GetLastWriteTime(_filePath); + + // Does it update every time? + if (_options.updateInterval == 0) + { + // Comment in to force a re-request with each reload of the widget + //_reloadOnXmdsRefresh = true; + + // File exists but needs updating + RefreshFromXmds(); + + // Comment in to force a re-request with each reload of the widget + //return false; + } + // Compare the last time it was updated to the layout modified time (always refresh when the layout has been modified) + // Also compare to the update interval (refresh if it has not been updated for longer than the update interval) + else if (_options.LayoutModifiedDate.CompareTo(lastWriteDate) > 0 || DateTime.Now.CompareTo(lastWriteDate.AddMinutes(_options.updateInterval)) > 0) + { + // File exists but needs updating. + RefreshFromXmds(); + } + else + { + // File exists and is in-date - nothing to do + } + + // Refresh the local file cache with any new dimensions, etc. + UpdateCacheIfNecessary(); + + return true; + } + + /// + /// Pulls the duration out of the temporary file and sets the media Duration to the same + /// + protected void ReadControlMeta() + { + // read the contents of the file + using (StreamReader reader = new StreamReader(_filePath)) + { + string html = reader.ReadToEnd(); + + // Parse out the duration using a regular expression + try + { + Match match = Regex.Match(html, ""); + + if (match.Success) + { + // We have a match, so override our duration. + Duration = Convert.ToInt32(match.Groups[1].Value); + } + } + catch + { + Trace.WriteLine(new LogMessage("Html - ReadControlMeta", "Unable to pull duration using RegEx").ToString()); + } + } + } + + /// + /// Refresh the Local cache of the DataSetView HTML + /// + private void RefreshFromXmds() + { + xmds.xmds xmds = new XiboClient.xmds.xmds(); + xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=getResource"; + xmds.GetResourceCompleted += new XiboClient.xmds.GetResourceCompletedEventHandler(xmds_GetResourceCompleted); + + xmds.GetResourceAsync(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, _options.layoutId, _options.regionId, _options.mediaid, ApplicationSettings.Default.Version); + } + + /// + /// Refresh Complete + /// + /// + /// + private void xmds_GetResourceCompleted(object sender, XiboClient.xmds.GetResourceCompletedEventArgs e) + { + try + { + // Success / Failure + if (e.Error != null) + { + Trace.WriteLine(new LogMessage("xmds_GetResource", "Unable to get Resource: " + e.Error.Message), LogType.Info.ToString()); + + // We have failed to update from XMDS + // id we have been asked to reload on XmdsRefresh, check to see if we have a file to load, + // if not expire on a short timer. + if (_reloadOnXmdsRefresh) + { + if (File.Exists(_filePath)) + { + // Cached file to revert to + UpdateCacheIfNecessary(); + + // Navigate to the file + HtmlUpdatedEvent?.Invoke(_localWebPath); + } + else + { + // No cache to revert to + Trace.WriteLine(new LogMessage("xmds_GetResource", "We haven't been able to download this widget and there isn't a pre-cached one to use. Skipping."), LogType.Info.ToString()); + + // Start the timer so that we expire + Duration = 2; + base.RenderMedia(); + } + } + } + else + { + // Ammend the resource file so that we can open it directly from the library (this is better than using a tempoary file) + string cachedFile = e.Result; + + // Handle the background + String bodyStyle; + String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); + + if (_options.backgroundImage == null || _options.backgroundImage == "") + { + bodyStyle = "background-color:" + backgroundColor + " ;"; + } + else + { + bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; + } + + string html = cachedFile.Replace("", ""); + html = html.Replace("[[ViewPortWidth]]", _width.ToString()); + html += ""; + html += ""; + + // Comment in to write out the update date at the end of the file (in the body) + // This is useful if you want to check how frequently the file is updating + //html = html.Replace("", "

" + DateTime.Now.ToString() + "

"); + + // Write to the library + using (FileStream fileStream = File.Open(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + using (StreamWriter sw = new StreamWriter(fileStream)) + { + sw.Write(html); + sw.Close(); + } + } + + if (_reloadOnXmdsRefresh) + { + // Read the control meta back out + ReadControlMeta(); + + // Handle Navigate in here because we will not have done it during first load + HtmlUpdatedEvent?.Invoke(_localWebPath); + } + } + } + catch (ObjectDisposedException) + { + Trace.WriteLine(new LogMessage("WebMedia", "Retrived the resource, stored the document but the media has already expired."), LogType.Error.ToString()); + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("WebMedia", "Unknown exception " + ex.Message), LogType.Error.ToString()); + + // This should exipre the media + Duration = 5; + base.RenderMedia(); + } + } + + /// + /// Updates the Cache File with the necessary client side injected items + /// + private void UpdateCacheIfNecessary() + { + // Ammend the resource file so that we can open it directly from the library (this is better than using a tempoary file) + string cachedFile = ""; + + using (FileStream fileStream = File.Open(_filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + using (StreamReader reader = new StreamReader(fileStream)) + { + cachedFile = reader.ReadToEnd(); + } + } + + // Compare the cached dimensions in the file with the dimensions now, and + // regenerate if they are different. + if (cachedFile.Contains("[[ViewPortWidth]]") || !ReadCachedViewPort(cachedFile).Equals(_width.ToString() + "x" + _height.ToString())) + { + // Regex out the existing replacement if present + cachedFile = Regex.Replace(cachedFile, "(.*)", ""); + cachedFile = Regex.Replace(cachedFile, "", ""); + cachedFile = Regex.Replace(cachedFile, "", ""); + cachedFile = Regex.Replace(cachedFile, "", ""); + + // Handle the background + String bodyStyle; + String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); + + if (_options.backgroundImage == null || _options.backgroundImage == "") + { + bodyStyle = "background-color:" + backgroundColor + " ;"; + } + else + { + bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; + } + + string html = cachedFile.Replace("", ""); + html = html.Replace("[[ViewPortWidth]]", _width.ToString()); + html += ""; + html += ""; + + // Write to the library + using (FileStream fileStream = File.Open(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + using (StreamWriter sw = new StreamWriter(fileStream)) + { + sw.Write(html); + sw.Close(); + } + } + } + } + + /// + /// Pulls the duration out of the temporary file and sets the media Duration to the same + /// + private string ReadCachedViewPort(string html) + { + // Parse out the duration using a regular expression + try + { + Match match = Regex.Match(html, ""); + + if (match.Success) + { + // We have a match, so override our duration. + return match.Groups[1].Value; + } + else + { + return string.Empty; + } + } + catch + { + return string.Empty; + } + } + + /// + /// Get the configured web media engine + /// + /// + /// + public static WebMedia GetConfiguredWebMedia(RegionOptions options) + { + WebMedia media; + if (ApplicationSettings.Default.BrowserType.Equals("edge", StringComparison.InvariantCultureIgnoreCase)) + { + media = new EdgeWebMedia(options); + } + else + { + media = new IeWebMedia(options); + } + return media; + } + } +} diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index fcfa888c..fc080467 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace XiboClient.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index dd458bf3..77ca5d59 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace XiboClient.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/Web References/xmds/Reference.cs b/Web References/xmds/Reference.cs index 736ba66f..c34cbb91 100644 --- a/Web References/xmds/Reference.cs +++ b/Web References/xmds/Reference.cs @@ -23,7 +23,7 @@ namespace XiboClient.xmds { /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="xmdsBinding", Namespace="urn:xmds")] @@ -540,11 +540,11 @@ private bool IsLocalFileSystemWebService(string url) { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void RegisterDisplayCompletedEventHandler(object sender, RegisterDisplayCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class RegisterDisplayCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -566,11 +566,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void RequiredFilesCompletedEventHandler(object sender, RequiredFilesCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class RequiredFilesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -592,11 +592,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void GetFileCompletedEventHandler(object sender, GetFileCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class GetFileCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -618,11 +618,11 @@ public byte[] Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void ScheduleCompletedEventHandler(object sender, ScheduleCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class ScheduleCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -644,11 +644,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void BlackListCompletedEventHandler(object sender, BlackListCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class BlackListCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -670,11 +670,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void SubmitLogCompletedEventHandler(object sender, SubmitLogCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitLogCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -696,11 +696,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void SubmitStatsCompletedEventHandler(object sender, SubmitStatsCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitStatsCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -722,11 +722,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void MediaInventoryCompletedEventHandler(object sender, MediaInventoryCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class MediaInventoryCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -748,11 +748,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void GetResourceCompletedEventHandler(object sender, GetResourceCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class GetResourceCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -774,11 +774,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void NotifyStatusCompletedEventHandler(object sender, NotifyStatusCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class NotifyStatusCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -800,11 +800,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void SubmitScreenShotCompletedEventHandler(object sender, SubmitScreenShotCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitScreenShotCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { diff --git a/XiboClient.csproj b/XiboClient.csproj index f93ba629..db5d7f99 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -20,7 +20,7 @@ 3.5 - v4.5.2 + v4.7.2 new-icon.ico @@ -81,55 +81,65 @@ false - - packages\AsyncIO.0.1.26.0\lib\net40\AsyncIO.dll - True + + packages\AsyncIO.0.1.69\lib\net40\AsyncIO.dll False wmpdll\AxInterop.WMPLib.dll False - - packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll - True + + packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll False False wmpdll\Interop.WMPLib.dll - - packages\NetMQ.4.0.0.1\lib\net40\NetMQ.dll - True + + + packages\Microsoft.Toolkit.Forms.UI.Controls.WebView.5.1.1\lib\net462\Microsoft.Toolkit.Forms.UI.Controls.WebView.dll - - packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + + packages\NetMQ.4.0.0.207\lib\net40\NetMQ.dll - - packages\NodaTime.2.0.2\lib\net45\NodaTime.dll - True + + packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + packages\NodaTime.2.4.7\lib\net45\NodaTime.dll + + + packages\Unosquare.Swan.Lite.2.3.0\lib\netstandard2.0\Swan.Lite.dll + + + + packages\System.Runtime.CompilerServices.Unsafe.4.6.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Text.Encoding.CodePages.4.6.0\lib\net461\System.Text.Encoding.CodePages.dll + + - - packages\EmbedIO.1.6.9\lib\net452\Unosquare.Labs.EmbedIO.dll - - - packages\Unosquare.Swan.0.14.2\lib\net452\Unosquare.Swan.dll + + packages\EmbedIO.2.9.2\lib\netstandard2.0\Unosquare.Labs.EmbedIO.dll + @@ -163,12 +173,15 @@ Form - + Form Form + + Form + Form diff --git a/app.config b/app.config index 9ae09c53..788bfbed 100644 --- a/app.config +++ b/app.config @@ -14,4 +14,4 @@ - + diff --git a/default.config.xml b/default.config.xml index edcc42e3..ceb496fe 100644 --- a/default.config.xml +++ b/default.config.xml @@ -53,4 +53,5 @@ 9696 0 + mshtml \ No newline at end of file diff --git a/packages.config b/packages.config index 0e43a549..1340f25d 100644 --- a/packages.config +++ b/packages.config @@ -1,10 +1,13 @@  - - - - - - - + + + + + + + + + + \ No newline at end of file From 6bc1aa4276afac75698166a23165b10989eb0652 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 2 Oct 2019 18:10:22 +0100 Subject: [PATCH 02/29] Fix Swan.Lite package version --- XiboClient.csproj | 6 +++--- packages.config | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/XiboClient.csproj b/XiboClient.csproj index db5d7f99..68ad093e 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -110,9 +110,6 @@ packages\NodaTime.2.4.7\lib\net45\NodaTime.dll - - packages\Unosquare.Swan.Lite.2.3.0\lib\netstandard2.0\Swan.Lite.dll - @@ -139,6 +136,9 @@ packages\EmbedIO.2.9.2\lib\netstandard2.0\Unosquare.Labs.EmbedIO.dll + + packages\Unosquare.Swan.Lite.1.3.1\lib\net461\Unosquare.Swan.Lite.dll + diff --git a/packages.config b/packages.config index 1340f25d..bca34805 100644 --- a/packages.config +++ b/packages.config @@ -9,5 +9,5 @@ - + \ No newline at end of file From 50e32bdfd2da7ae99904a67bc1b979c9dfa94ab1 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Tue, 8 Oct 2019 15:50:55 +0100 Subject: [PATCH 03/29] Bump versions to 202 --- Logic/ApplicationSettings.cs | 4 ++-- Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 8130b46c..d57cbed1 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -41,9 +41,9 @@ public class ApplicationSettings private List _globalProperties; // Application Specific Settings we want to protect - private readonly string _clientVersion = "2 R201"; + private readonly string _clientVersion = "2 R202"; private readonly string _version = "5"; - private readonly int _clientCodeVersion = 201; + private readonly int _clientCodeVersion = 202; public string ClientVersion { get { return _clientVersion; } } public string Version { get { return _version; } } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 47d0ddb6..e13c8cfc 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -30,6 +30,6 @@ // Build Number // Revision // -[assembly: AssemblyVersion("2.0.0.201")] -[assembly: AssemblyFileVersion("2.0.0.201")] +[assembly: AssemblyVersion("2.0.0.202")] +[assembly: AssemblyFileVersion("2.0.0.202")] [assembly: NeutralResourcesLanguageAttribute("en-GB")] From 0ca40ee89a38c5b3960e66ef06c81b13a4b69320 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 29 Jan 2020 10:40:22 +0000 Subject: [PATCH 04/29] Prepare for a R201 edge release --- Logic/ApplicationSettings.cs | 4 ++-- Properties/AssemblyInfo.cs | 6 +++--- default.config.xml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index d57cbed1..04bfcb6f 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -41,9 +41,9 @@ public class ApplicationSettings private List _globalProperties; // Application Specific Settings we want to protect - private readonly string _clientVersion = "2 R202"; + private readonly string _clientVersion = "2 R201-edge"; private readonly string _version = "5"; - private readonly int _clientCodeVersion = 202; + private readonly int _clientCodeVersion = 201; public string ClientVersion { get { return _clientVersion; } } public string Version { get { return _version; } } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index e13c8cfc..1b58a94f 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -11,7 +11,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Xibo Digital Signage")] [assembly: AssemblyProduct("Xibo")] -[assembly: AssemblyCopyright("Copyright Xibo Signage Ltd © 2008-2019")] +[assembly: AssemblyCopyright("Copyright Xibo Signage Ltd © 2008-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -30,6 +30,6 @@ // Build Number // Revision // -[assembly: AssemblyVersion("2.0.0.202")] -[assembly: AssemblyFileVersion("2.0.0.202")] +[assembly: AssemblyVersion("2.0.1.201")] +[assembly: AssemblyFileVersion("2.0.1.201")] [assembly: NeutralResourcesLanguageAttribute("en-GB")] diff --git a/default.config.xml b/default.config.xml index ceb496fe..64d27b27 100644 --- a/default.config.xml +++ b/default.config.xml @@ -53,5 +53,5 @@ 9696 0 - mshtml + edge \ No newline at end of file From d6b83c7255ca3bb6fd18d1c5ba4ce0d84cfb76e1 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 5 Feb 2020 15:13:27 +0000 Subject: [PATCH 05/29] WIP: migrate to WPF --- App.xaml | 9 + Program.cs => App.xaml.cs | 95 +- Control/EmbeddedServer.cs | 33 +- Control/WatchDogManager.cs | 28 +- Error/DefaultLayoutException.cs | 21 +- Forms/About.Designer.cs | 126 -- Forms/About.cs | 61 - Forms/About.resx | 120 -- Forms/OptionForm.Designer.cs | 522 ------- Forms/OptionForm.cs | 288 ---- Forms/OptionForm.resx | 132 -- Log/ClientInfo.Designer.cs | 271 ---- Log/ClientInfo.cs | 267 +--- Log/ClientInfo.resx | 138 -- Log/XiboTraceListener.cs | 2 +- Logic/ApplicationSettings.cs | 32 +- Logic/BlackList.cs | 47 +- Logic/CacheManager.cs | 57 +- Logic/KeyInterceptor.cs | 25 +- Logic/MediaOption.cs | 11 +- Logic/MouseInterceptor.cs | 6 +- Logic/RegionOptions.cs | 5 +- Logic/Schedule.cs | 39 +- Logic/ScheduleManager.cs | 12 +- MainForm.Designer.cs | 58 - MainForm.cs | 1374 ----------------- MainForm.resx | 124 -- MainWindow.xaml | 20 + MainWindow.xaml.cs | 921 +++++++++++ Media/Audio.cs | 148 -- Media/EdgeWebMedia.cs | 160 -- Media/Flash.cs | 151 -- Media/IeWebMedia.cs | 143 -- Media/Image.cs | 159 -- Media/Media.cs | 285 ---- Media/PowerPoint.cs | 102 -- Media/ShellCommand.cs | 223 --- Media/TemporaryHtml.cs | 111 -- Media/Video.cs | 228 --- Media/VideoDS.cs | 139 -- Media/VideoPlayer.Designer.cs | 64 - Media/VideoPlayer.cs | 226 --- Media/VideoPlayer.resx | 131 -- OptionsForm.xaml | 12 + OptionsForm.xaml.cs | 85 + Properties/AssemblyInfo.cs | 43 +- Properties/Resources.Designer.cs | 31 +- Properties/Resources.resx | 8 +- Properties/Settings.Designer.cs | 12 +- Properties/Settings.settings | 8 +- Properties/app.manifest | 11 - README.md | 33 +- Rendering/Audio.cs | 131 ++ Rendering/Image.cs | 36 + Rendering/Layout.xaml | 12 + Rendering/Layout.xaml.cs | 473 ++++++ Rendering/Media.xaml | 12 + Rendering/Media.xaml.cs | 218 +++ Rendering/Region.xaml | 12 + Control/Region.cs => Rendering/Region.xaml.cs | 650 ++++---- Rendering/Transitions.cs | 283 ++++ Rendering/WebEdge.cs | 120 ++ Rendering/WebIe.cs | 82 + {Media => Rendering}/WebMedia.cs | 25 +- Resources/licence.txt | 2 +- Stats/Stat.cs | 57 + {Log => Stats}/StatLog.cs | 50 +- Web References/xmds/Reference.cs | 2 +- Web References/xmds/Reference.map | 2 +- .../xmds/{xmds.wsdl => service_v5.wsdl} | 2 +- XiboClient.csproj | 413 ++--- XiboClient.csproj.user | 21 - XiboClient.sln | 23 +- XmdsAgents/FileAgent.cs | 7 +- XmdsAgents/LogAgent.cs | 2 +- XmdsAgents/ScheduleAgent.cs | 2 +- app.config | 33 +- default.config.xml | 1 - packages.config | 15 +- wmpdll/AxInterop.WMPLib.dll | Bin 61440 -> 0 bytes wmpdll/Interop.WMPLib.dll | Bin 339968 -> 0 bytes 81 files changed, 3346 insertions(+), 6697 deletions(-) create mode 100644 App.xaml rename Program.cs => App.xaml.cs (74%) delete mode 100644 Forms/About.Designer.cs delete mode 100644 Forms/About.cs delete mode 100644 Forms/About.resx delete mode 100644 Forms/OptionForm.Designer.cs delete mode 100644 Forms/OptionForm.cs delete mode 100644 Forms/OptionForm.resx delete mode 100644 Log/ClientInfo.Designer.cs delete mode 100644 Log/ClientInfo.resx delete mode 100644 MainForm.Designer.cs delete mode 100644 MainForm.cs delete mode 100644 MainForm.resx create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs delete mode 100644 Media/Audio.cs delete mode 100644 Media/EdgeWebMedia.cs delete mode 100644 Media/Flash.cs delete mode 100644 Media/IeWebMedia.cs delete mode 100644 Media/Image.cs delete mode 100644 Media/Media.cs delete mode 100644 Media/PowerPoint.cs delete mode 100644 Media/ShellCommand.cs delete mode 100644 Media/TemporaryHtml.cs delete mode 100644 Media/Video.cs delete mode 100644 Media/VideoDS.cs delete mode 100644 Media/VideoPlayer.Designer.cs delete mode 100644 Media/VideoPlayer.cs delete mode 100644 Media/VideoPlayer.resx create mode 100644 OptionsForm.xaml create mode 100644 OptionsForm.xaml.cs delete mode 100644 Properties/app.manifest create mode 100644 Rendering/Audio.cs create mode 100644 Rendering/Image.cs create mode 100644 Rendering/Layout.xaml create mode 100644 Rendering/Layout.xaml.cs create mode 100644 Rendering/Media.xaml create mode 100644 Rendering/Media.xaml.cs create mode 100644 Rendering/Region.xaml rename Control/Region.cs => Rendering/Region.xaml.cs (57%) create mode 100644 Rendering/Transitions.cs create mode 100644 Rendering/WebEdge.cs create mode 100644 Rendering/WebIe.cs rename {Media => Rendering}/WebMedia.cs (96%) create mode 100644 Stats/Stat.cs rename {Log => Stats}/StatLog.cs (74%) rename Web References/xmds/{xmds.wsdl => service_v5.wsdl} (99%) delete mode 100644 XiboClient.csproj.user delete mode 100644 wmpdll/AxInterop.WMPLib.dll delete mode 100644 wmpdll/Interop.WMPLib.dll diff --git a/App.xaml b/App.xaml new file mode 100644 index 00000000..407d88ed --- /dev/null +++ b/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Program.cs b/App.xaml.cs similarity index 74% rename from Program.cs rename to App.xaml.cs index c722ba4c..53b6339f 100644 --- a/Program.cs +++ b/App.xaml.cs @@ -1,39 +1,24 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2015 Daniel Garner and the Xibo Developers - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; +using System; using System.Collections.Generic; -using System.Windows.Forms; -using System.Runtime.InteropServices; +using System.Configuration; +using System.Data; using System.Diagnostics; -using XiboClient.Logic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; using System.Threading.Tasks; +using System.Windows; +using XiboClient.Logic; namespace XiboClient { - static class Program + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application { - /// - /// The main entry point for the application. - /// [STAThread] - static int Main(string[] args) + protected override void OnStartup(StartupEventArgs e) { NativeMethods.SetErrorMode(NativeMethods.SetErrorMode(0) | ErrorModes.SEM_NOGPFAULTERRORBOX | @@ -44,8 +29,6 @@ static int Main(string[] args) // Ensure our process has the highest priority Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime; - Application.SetCompatibleTextRenderingDefault(false); - #if !DEBUG // Catch unhandled exceptions AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); @@ -59,22 +42,22 @@ static int Main(string[] args) try { // Check for any passed arguments - if (args.Length > 0) + if (e.Args.Length > 0) { - if (args[0].ToString() == "o") + if (e.Args[0].ToString() == "o") { RunSettings(); } else { - switch (args[0].ToLower().Trim().Substring(0, 2)) + switch (e.Args[0].ToLower().Trim().Substring(0, 2)) { // Preview the screen saver case "/p": // args[1] is the handle to the preview window KeyInterceptor.SetHook(); MouseInterceptor.SetHook(); - RunClient(new IntPtr(long.Parse(args[1]))); + RunClient(new IntPtr(long.Parse(e.Args[1]))); KeyInterceptor.UnsetHook(); MouseInterceptor.UnsetHook(); break; @@ -107,10 +90,6 @@ static int Main(string[] args) } else { - // Add a message filter - Application.AddMessageFilter(KeyStore.Instance); - - // No arguments were passed - we run the usual client RunClient(); } } @@ -122,35 +101,30 @@ static int Main(string[] args) // Always flush at the end Trace.WriteLine(new LogMessage("Main", "Application Finished"), LogType.Info.ToString()); Trace.Flush(); - - return 0; } private static void RunClient() { Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); - Application.Run(new MainForm()); + MainWindow windowMain = new MainWindow(); + windowMain.ShowDialog(); } - private static void RunClient(bool screenSaver) + private static void RunSettings() { - Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); - Application.Run(new MainForm(screenSaver)); + // If we are showing the options form, enable visual styles + OptionsForm windowMain = new OptionsForm(); + windowMain.ShowDialog(); } - private static void RunClient(IntPtr previewWindow) + private static void RunClient(bool screenSaver) { Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); - Application.Run(new MainForm(previewWindow)); } - private static void RunSettings() + private static void RunClient(IntPtr previewWindow) { - // If we are showing the options form, enable visual styles - Application.EnableVisualStyles(); - - Trace.WriteLine(new LogMessage("Main", "Options Started"), LogType.Info.ToString()); - Application.Run(new OptionForm()); + Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); } static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) @@ -182,12 +156,17 @@ static void HandleUnhandledException(Object o) try { + string productName = ApplicationSettings.GetProductNameFromAssembly(); + // Also write to the event log try { - if (!EventLog.SourceExists(Application.ProductName)) - EventLog.CreateEventSource(Application.ProductName, "Xibo"); - EventLog.WriteEntry(Application.ProductName, e.ToString(), EventLogEntryType.Error); + if (!EventLog.SourceExists(productName)) + { + EventLog.CreateEventSource(productName, "Xibo"); + } + + EventLog.WriteEntry(productName, e.ToString(), EventLogEntryType.Error); } catch (Exception ex) { @@ -202,11 +181,11 @@ static void HandleUnhandledException(Object o) } // Exit the application and allow it to be restarted by the Watchdog. - Application.Exit(); + Environment.Exit(0); } [DllImport("User32.dll")] - public static extern int ShowWindowAsync(IntPtr hWnd , int swCommand); + public static extern int ShowWindowAsync(IntPtr hWnd, int swCommand); internal static class NativeMethods { @@ -224,4 +203,4 @@ internal enum ErrorModes : uint SEM_NOOPENFILEERRORBOX = 0x8000 } } -} \ No newline at end of file +} diff --git a/Control/EmbeddedServer.cs b/Control/EmbeddedServer.cs index 765cc292..5f3745f8 100644 --- a/Control/EmbeddedServer.cs +++ b/Control/EmbeddedServer.cs @@ -1,11 +1,10 @@ -using System; +using EmbedIO; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; -using Unosquare.Labs.EmbedIO; -using Unosquare.Labs.EmbedIO.Modules; using XiboClient.Log; namespace XiboClient.Control @@ -50,19 +49,8 @@ public void Run() // If we are restarting, reset _manualReset.Reset(); - using (WebServer server = new WebServer(ApplicationSettings.Default.EmbeddedServerAddress)) + using (WebServer server = CreateWebServer(ApplicationSettings.Default.EmbeddedServerAddress)) { - Dictionary headers = new Dictionary() - { - { "Cache-Control", "no-cache, no-store, must-revalidate" }, - { "Pragma", "no-cache" }, - { "Expires", "0" } - }; - - server.RegisterModule(new StaticFilesModule(ApplicationSettings.Default.LibraryPath, headers)); - server.Module().UseRamCache = true; - server.Module().DefaultExtension = ".html"; - server.RunAsync(); // Wait @@ -79,5 +67,20 @@ public void Run() if (OnServerClosed != null) OnServerClosed(); } + + /// + /// Create WebServer + /// + /// + /// + private WebServer CreateWebServer(string url) + { + var server = new WebServer(o => o + .WithUrlPrefix(url) + .WithMode(HttpListenerMode.EmbedIO)) + .WithStaticFolder("/", ApplicationSettings.Default.LibraryPath, false); + + return server; + } } } diff --git a/Control/WatchDogManager.cs b/Control/WatchDogManager.cs index e7b4d22c..d6839825 100644 --- a/Control/WatchDogManager.cs +++ b/Control/WatchDogManager.cs @@ -1,10 +1,28 @@ -using System; +/* + * Xibo - Digitial Signage - http://www.xibo.org.uk + * Copyright (C) 2020 Xibo Signage Ltd + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; -using System.Windows.Forms; namespace XiboClient.Control { @@ -15,8 +33,10 @@ public static void Start() // Check to see if the WatchDog EXE exists where we expect it to be // Uncomment to test local watchdog install. //string path = @"C:\Program Files (x86)\Xibo Player\watchdog\x86\XiboClientWatchdog.exe"; - string path = Path.GetDirectoryName(Application.ExecutablePath) + @"\watchdog\x86\" + ((Application.ProductName != "Xibo") ? Application.ProductName + "Watchdog.exe" : "XiboClientWatchdog.exe"); - string args = "-p \"" + Application.ExecutablePath + "\" -l \"" + ApplicationSettings.Default.LibraryPath + "\""; + string executablePath = Process.GetCurrentProcess().MainModule.FileName; + string productName = ApplicationSettings.GetProductNameFromAssembly(); + string path = Path.GetDirectoryName(executablePath) + @"\watchdog\x86\" + ((productName != "Xibo") ? productName + "Watchdog.exe" : "XiboClientWatchdog.exe"); + string args = "-p \"" + executablePath + "\" -l \"" + ApplicationSettings.Default.LibraryPath + "\""; // Start it if (File.Exists(path)) diff --git a/Error/DefaultLayoutException.cs b/Error/DefaultLayoutException.cs index ac196cba..e9b89c76 100644 --- a/Error/DefaultLayoutException.cs +++ b/Error/DefaultLayoutException.cs @@ -1,4 +1,23 @@ -using System; +/* + * Xibo - Digitial Signage - http://www.xibo.org.uk + * Copyright (C) 2020 Xibo Signage Ltd + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/Forms/About.Designer.cs b/Forms/About.Designer.cs deleted file mode 100644 index 87c65277..00000000 --- a/Forms/About.Designer.cs +++ /dev/null @@ -1,126 +0,0 @@ -namespace XiboClient -{ - partial class About - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.pictureBox1 = new System.Windows.Forms.PictureBox(); - this.buttonHelp = new System.Windows.Forms.Button(); - this.richTextBox1 = new System.Windows.Forms.RichTextBox(); - this.label1 = new System.Windows.Forms.Label(); - this.label2 = new System.Windows.Forms.Label(); - this.buttonClose = new System.Windows.Forms.Button(); - ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); - this.SuspendLayout(); - // - // pictureBox1 - // - this.pictureBox1.Image = global::XiboClient.Properties.Resources.logo; - this.pictureBox1.Location = new System.Drawing.Point(12, 6); - this.pictureBox1.Name = "pictureBox1"; - this.pictureBox1.Size = new System.Drawing.Size(160, 82); - this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; - this.pictureBox1.TabIndex = 0; - this.pictureBox1.TabStop = false; - // - // buttonHelp - // - this.buttonHelp.Location = new System.Drawing.Point(12, 277); - this.buttonHelp.Name = "buttonHelp"; - this.buttonHelp.Size = new System.Drawing.Size(75, 23); - this.buttonHelp.TabIndex = 1; - this.buttonHelp.Text = "Online Help"; - this.buttonHelp.UseVisualStyleBackColor = true; - this.buttonHelp.Click += new System.EventHandler(this.buttonHelp_Click); - // - // richTextBox1 - // - this.richTextBox1.Location = new System.Drawing.Point(12, 97); - this.richTextBox1.Name = "richTextBox1"; - this.richTextBox1.Size = new System.Drawing.Size(397, 174); - this.richTextBox1.TabIndex = 2; - this.richTextBox1.Text = ""; - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(296, 9); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(35, 13); - this.label1.TabIndex = 3; - this.label1.Text = "label1"; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(296, 45); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(35, 13); - this.label2.TabIndex = 4; - this.label2.Text = "label2"; - // - // buttonClose - // - this.buttonClose.Location = new System.Drawing.Point(333, 276); - this.buttonClose.Name = "buttonClose"; - this.buttonClose.Size = new System.Drawing.Size(75, 23); - this.buttonClose.TabIndex = 5; - this.buttonClose.Text = "Close"; - this.buttonClose.UseVisualStyleBackColor = true; - this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click); - // - // About - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(421, 312); - this.Controls.Add(this.buttonClose); - this.Controls.Add(this.label2); - this.Controls.Add(this.label1); - this.Controls.Add(this.richTextBox1); - this.Controls.Add(this.buttonHelp); - this.Controls.Add(this.pictureBox1); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.Name = "About"; - this.Text = "About"; - ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.PictureBox pictureBox1; - private System.Windows.Forms.Button buttonHelp; - private System.Windows.Forms.RichTextBox richTextBox1; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.Button buttonClose; - - } -} \ No newline at end of file diff --git a/Forms/About.cs b/Forms/About.cs deleted file mode 100644 index da650bce..00000000 --- a/Forms/About.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2014 Daniel Garner and James Packer - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.IO; -using System.Text; -using System.Windows.Forms; - -namespace XiboClient -{ - public partial class About : Form - { - public About() - { - InitializeComponent(); - Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); - label1.Text = Application.ProductName; - label2.Text = ApplicationSettings.Default.ClientVersion; - - richTextBox1.Text = XiboClient.Properties.Resources.licence; - } - - private void buttonHelp_Click(object sender, EventArgs e) - { - // open URL in separate instance of default browser - try - { - System.Diagnostics.Process.Start(Properties.Resources.SupportUrl); - } - catch - { - MessageBox.Show("No web browser installed"); - } - } - - private void buttonClose_Click(object sender, EventArgs e) - { - this.Close(); - } - } -} \ No newline at end of file diff --git a/Forms/About.resx b/Forms/About.resx deleted file mode 100644 index d58980a3..00000000 --- a/Forms/About.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Forms/OptionForm.Designer.cs b/Forms/OptionForm.Designer.cs deleted file mode 100644 index 4c0c5ff0..00000000 --- a/Forms/OptionForm.Designer.cs +++ /dev/null @@ -1,522 +0,0 @@ -namespace XiboClient -{ - partial class OptionForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.label1 = new System.Windows.Forms.Label(); - this.label2 = new System.Windows.Forms.Label(); - this.label3 = new System.Windows.Forms.Label(); - this.buttonSaveSettings = new System.Windows.Forms.Button(); - this.label9 = new System.Windows.Forms.Label(); - this.buttonLibrary = new System.Windows.Forms.Button(); - this.tbHardwareKey = new System.Windows.Forms.TextBox(); - this.textBoxXmdsUri = new System.Windows.Forms.TextBox(); - this.textBoxServerKey = new System.Windows.Forms.TextBox(); - this.textBoxLibraryPath = new System.Windows.Forms.TextBox(); - this.buttonDisplayAdmin = new System.Windows.Forms.Button(); - this.textBoxProxyDomain = new System.Windows.Forms.TextBox(); - this.maskedTextBoxProxyPass = new System.Windows.Forms.MaskedTextBox(); - this.textBoxProxyUser = new System.Windows.Forms.TextBox(); - this.labelProxyDomain = new System.Windows.Forms.Label(); - this.labelProxyPass = new System.Windows.Forms.Label(); - this.labelProxyUser = new System.Windows.Forms.Label(); - this.splashButtonBrowse = new System.Windows.Forms.Button(); - this.linkLabel1 = new System.Windows.Forms.LinkLabel(); - this.label17 = new System.Windows.Forms.Label(); - this.label16 = new System.Windows.Forms.Label(); - this.splashOverride = new System.Windows.Forms.TextBox(); - this.menuStrip1 = new System.Windows.Forms.MenuStrip(); - this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.aboutToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - this.onlineHelpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.folderBrowserLibrary = new System.Windows.Forms.FolderBrowserDialog(); - this.splashScreenOverride = new System.Windows.Forms.OpenFileDialog(); - this.tbStatus = new System.Windows.Forms.TextBox(); - this.xmds1 = new XiboClient.xmds.xmds(); - this.buttonExit = new System.Windows.Forms.Button(); - this.button1 = new System.Windows.Forms.Button(); - this.tabControl1 = new System.Windows.Forms.TabControl(); - this.tabPage1 = new System.Windows.Forms.TabPage(); - this.label5 = new System.Windows.Forms.Label(); - this.tabPage2 = new System.Windows.Forms.TabPage(); - this.label4 = new System.Windows.Forms.Label(); - this.menuStrip1.SuspendLayout(); - this.tabControl1.SuspendLayout(); - this.tabPage1.SuspendLayout(); - this.tabPage2.SuspendLayout(); - this.SuspendLayout(); - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label1.Location = new System.Drawing.Point(99, 37); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(128, 22); - this.label1.TabIndex = 1; - this.label1.Text = "CMS Address"; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label2.Location = new System.Drawing.Point(184, 87); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(43, 22); - this.label2.TabIndex = 3; - this.label2.Text = "Key"; - // - // label3 - // - this.label3.AutoSize = true; - this.label3.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label3.Location = new System.Drawing.Point(109, 141); - this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(118, 22); - this.label3.TabIndex = 5; - this.label3.Text = "Local Library"; - // - // buttonSaveSettings - // - this.buttonSaveSettings.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.buttonSaveSettings.Location = new System.Drawing.Point(12, 508); - this.buttonSaveSettings.Name = "buttonSaveSettings"; - this.buttonSaveSettings.Size = new System.Drawing.Size(387, 41); - this.buttonSaveSettings.TabIndex = 6; - this.buttonSaveSettings.Text = "Save"; - this.buttonSaveSettings.UseVisualStyleBackColor = true; - this.buttonSaveSettings.Click += new System.EventHandler(this.buttonSaveSettings_Click); - // - // label9 - // - this.label9.AutoSize = true; - this.label9.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label9.Location = new System.Drawing.Point(46, 174); - this.label9.Name = "label9"; - this.label9.Size = new System.Drawing.Size(97, 22); - this.label9.TabIndex = 13; - this.label9.Text = "Display ID"; - // - // buttonLibrary - // - this.buttonLibrary.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.buttonLibrary.Location = new System.Drawing.Point(441, 136); - this.buttonLibrary.Name = "buttonLibrary"; - this.buttonLibrary.Size = new System.Drawing.Size(109, 31); - this.buttonLibrary.TabIndex = 10; - this.buttonLibrary.Text = "Browse"; - this.buttonLibrary.UseVisualStyleBackColor = true; - this.buttonLibrary.Click += new System.EventHandler(this.buttonLibrary_Click); - // - // tbHardwareKey - // - this.tbHardwareKey.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.tbHardwareKey.Location = new System.Drawing.Point(148, 171); - this.tbHardwareKey.Name = "tbHardwareKey"; - this.tbHardwareKey.Size = new System.Drawing.Size(497, 29); - this.tbHardwareKey.TabIndex = 14; - // - // textBoxXmdsUri - // - this.textBoxXmdsUri.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.textBoxXmdsUri.Location = new System.Drawing.Point(233, 34); - this.textBoxXmdsUri.Name = "textBoxXmdsUri"; - this.textBoxXmdsUri.Size = new System.Drawing.Size(317, 29); - this.textBoxXmdsUri.TabIndex = 0; - this.textBoxXmdsUri.Text = "http://localhost/xibo"; - // - // textBoxServerKey - // - this.textBoxServerKey.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.textBoxServerKey.Location = new System.Drawing.Point(233, 84); - this.textBoxServerKey.Name = "textBoxServerKey"; - this.textBoxServerKey.Size = new System.Drawing.Size(317, 29); - this.textBoxServerKey.TabIndex = 2; - // - // textBoxLibraryPath - // - this.textBoxLibraryPath.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.textBoxLibraryPath.Location = new System.Drawing.Point(233, 138); - this.textBoxLibraryPath.Name = "textBoxLibraryPath"; - this.textBoxLibraryPath.Size = new System.Drawing.Size(202, 29); - this.textBoxLibraryPath.TabIndex = 4; - this.textBoxLibraryPath.Text = "DEFAULT"; - // - // buttonDisplayAdmin - // - this.buttonDisplayAdmin.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.buttonDisplayAdmin.Location = new System.Drawing.Point(501, 508); - this.buttonDisplayAdmin.Name = "buttonDisplayAdmin"; - this.buttonDisplayAdmin.Size = new System.Drawing.Size(90, 41); - this.buttonDisplayAdmin.TabIndex = 7; - this.buttonDisplayAdmin.Text = "Display Admin"; - this.buttonDisplayAdmin.UseVisualStyleBackColor = true; - this.buttonDisplayAdmin.Click += new System.EventHandler(this.buttonDisplayAdmin_Click); - // - // textBoxProxyDomain - // - this.textBoxProxyDomain.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.textBoxProxyDomain.Location = new System.Drawing.Point(148, 120); - this.textBoxProxyDomain.Name = "textBoxProxyDomain"; - this.textBoxProxyDomain.Size = new System.Drawing.Size(497, 29); - this.textBoxProxyDomain.TabIndex = 7; - // - // maskedTextBoxProxyPass - // - this.maskedTextBoxProxyPass.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.maskedTextBoxProxyPass.Location = new System.Drawing.Point(148, 83); - this.maskedTextBoxProxyPass.Name = "maskedTextBoxProxyPass"; - this.maskedTextBoxProxyPass.Size = new System.Drawing.Size(497, 29); - this.maskedTextBoxProxyPass.TabIndex = 5; - this.maskedTextBoxProxyPass.UseSystemPasswordChar = true; - // - // textBoxProxyUser - // - this.textBoxProxyUser.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.textBoxProxyUser.Location = new System.Drawing.Point(148, 49); - this.textBoxProxyUser.Name = "textBoxProxyUser"; - this.textBoxProxyUser.Size = new System.Drawing.Size(497, 29); - this.textBoxProxyUser.TabIndex = 3; - // - // labelProxyDomain - // - this.labelProxyDomain.AutoSize = true; - this.labelProxyDomain.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.labelProxyDomain.Location = new System.Drawing.Point(65, 123); - this.labelProxyDomain.Name = "labelProxyDomain"; - this.labelProxyDomain.Size = new System.Drawing.Size(75, 22); - this.labelProxyDomain.TabIndex = 2; - this.labelProxyDomain.Text = "Domain"; - // - // labelProxyPass - // - this.labelProxyPass.AutoSize = true; - this.labelProxyPass.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.labelProxyPass.Location = new System.Drawing.Point(46, 86); - this.labelProxyPass.Name = "labelProxyPass"; - this.labelProxyPass.Size = new System.Drawing.Size(94, 22); - this.labelProxyPass.TabIndex = 1; - this.labelProxyPass.Text = "Password"; - // - // labelProxyUser - // - this.labelProxyUser.AutoSize = true; - this.labelProxyUser.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.labelProxyUser.Location = new System.Drawing.Point(43, 52); - this.labelProxyUser.Name = "labelProxyUser"; - this.labelProxyUser.Size = new System.Drawing.Size(97, 22); - this.labelProxyUser.TabIndex = 0; - this.labelProxyUser.Text = "Username"; - // - // splashButtonBrowse - // - this.splashButtonBrowse.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.splashButtonBrowse.Location = new System.Drawing.Point(536, 211); - this.splashButtonBrowse.Name = "splashButtonBrowse"; - this.splashButtonBrowse.Size = new System.Drawing.Size(109, 29); - this.splashButtonBrowse.TabIndex = 14; - this.splashButtonBrowse.Text = "Browse"; - this.splashButtonBrowse.UseVisualStyleBackColor = true; - this.splashButtonBrowse.Click += new System.EventHandler(this.splashButtonBrowse_Click); - // - // linkLabel1 - // - this.linkLabel1.AutoSize = true; - this.linkLabel1.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.linkLabel1.Location = new System.Drawing.Point(249, 283); - this.linkLabel1.Name = "linkLabel1"; - this.linkLabel1.Size = new System.Drawing.Size(177, 18); - this.linkLabel1.TabIndex = 13; - this.linkLabel1.TabStop = true; - this.linkLabel1.Text = "http://xibo.org.uk/donate/"; - // - // label17 - // - this.label17.AutoSize = true; - this.label17.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label17.Location = new System.Drawing.Point(98, 252); - this.label17.Name = "label17"; - this.label17.Size = new System.Drawing.Size(509, 18); - this.label17.TabIndex = 12; - this.label17.Text = "If you override the splash screen please consider donating to the project. "; - // - // label16 - // - this.label16.AutoSize = true; - this.label16.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label16.Location = new System.Drawing.Point(6, 214); - this.label16.Name = "label16"; - this.label16.Size = new System.Drawing.Size(134, 22); - this.label16.TabIndex = 11; - this.label16.Text = "Splash Screen"; - // - // splashOverride - // - this.splashOverride.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.splashOverride.Location = new System.Drawing.Point(148, 211); - this.splashOverride.Name = "splashOverride"; - this.splashOverride.Size = new System.Drawing.Size(382, 29); - this.splashOverride.TabIndex = 10; - // - // menuStrip1 - // - this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.fileToolStripMenuItem, - this.aboutToolStripMenuItem}); - this.menuStrip1.Location = new System.Drawing.Point(0, 0); - this.menuStrip1.Name = "menuStrip1"; - this.menuStrip1.Size = new System.Drawing.Size(698, 24); - this.menuStrip1.TabIndex = 8; - this.menuStrip1.Text = "menuStrip1"; - // - // fileToolStripMenuItem - // - this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.exitToolStripMenuItem}); - this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; - this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20); - this.fileToolStripMenuItem.Text = "File"; - // - // exitToolStripMenuItem - // - this.exitToolStripMenuItem.Name = "exitToolStripMenuItem"; - this.exitToolStripMenuItem.Size = new System.Drawing.Size(92, 22); - this.exitToolStripMenuItem.Text = "Exit"; - this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click); - // - // aboutToolStripMenuItem - // - this.aboutToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.aboutToolStripMenuItem1, - this.onlineHelpToolStripMenuItem}); - this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem"; - this.aboutToolStripMenuItem.Size = new System.Drawing.Size(44, 20); - this.aboutToolStripMenuItem.Text = "Help"; - // - // aboutToolStripMenuItem1 - // - this.aboutToolStripMenuItem1.Name = "aboutToolStripMenuItem1"; - this.aboutToolStripMenuItem1.Size = new System.Drawing.Size(137, 22); - this.aboutToolStripMenuItem1.Text = "About"; - this.aboutToolStripMenuItem1.Click += new System.EventHandler(this.aboutToolStripMenuItem1_Click); - // - // onlineHelpToolStripMenuItem - // - this.onlineHelpToolStripMenuItem.Name = "onlineHelpToolStripMenuItem"; - this.onlineHelpToolStripMenuItem.Size = new System.Drawing.Size(137, 22); - this.onlineHelpToolStripMenuItem.Text = "Online Help"; - this.onlineHelpToolStripMenuItem.Click += new System.EventHandler(this.onlineHelpToolStripMenuItem_Click); - // - // splashScreenOverride - // - this.splashScreenOverride.FileName = "Splash Screen.jpg"; - // - // tbStatus - // - this.tbStatus.BackColor = System.Drawing.SystemColors.Control; - this.tbStatus.BorderStyle = System.Windows.Forms.BorderStyle.None; - this.tbStatus.Cursor = System.Windows.Forms.Cursors.Default; - this.tbStatus.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.tbStatus.Location = new System.Drawing.Point(12, 391); - this.tbStatus.Multiline = true; - this.tbStatus.Name = "tbStatus"; - this.tbStatus.Size = new System.Drawing.Size(674, 100); - this.tbStatus.TabIndex = 15; - // - // xmds1 - // - this.xmds1.Credentials = null; - this.xmds1.Url = "http://localhost/Xibo/server/xmds.php"; - this.xmds1.UseDefaultCredentials = false; - // - // buttonExit - // - this.buttonExit.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.buttonExit.Location = new System.Drawing.Point(597, 508); - this.buttonExit.Name = "buttonExit"; - this.buttonExit.Size = new System.Drawing.Size(90, 41); - this.buttonExit.TabIndex = 16; - this.buttonExit.Text = "Exit"; - this.buttonExit.UseVisualStyleBackColor = true; - this.buttonExit.Click += new System.EventHandler(this.buttonExit_Click); - // - // button1 - // - this.button1.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.button1.Location = new System.Drawing.Point(405, 508); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(90, 41); - this.button1.TabIndex = 17; - this.button1.Text = "Launch Client"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // tabControl1 - // - this.tabControl1.Controls.Add(this.tabPage1); - this.tabControl1.Controls.Add(this.tabPage2); - this.tabControl1.Location = new System.Drawing.Point(12, 40); - this.tabControl1.Name = "tabControl1"; - this.tabControl1.SelectedIndex = 0; - this.tabControl1.Size = new System.Drawing.Size(675, 345); - this.tabControl1.TabIndex = 18; - // - // tabPage1 - // - this.tabPage1.Controls.Add(this.label5); - this.tabPage1.Controls.Add(this.label1); - this.tabPage1.Controls.Add(this.textBoxLibraryPath); - this.tabPage1.Controls.Add(this.textBoxServerKey); - this.tabPage1.Controls.Add(this.label3); - this.tabPage1.Controls.Add(this.label2); - this.tabPage1.Controls.Add(this.textBoxXmdsUri); - this.tabPage1.Controls.Add(this.buttonLibrary); - this.tabPage1.Location = new System.Drawing.Point(4, 22); - this.tabPage1.Name = "tabPage1"; - this.tabPage1.Padding = new System.Windows.Forms.Padding(3); - this.tabPage1.Size = new System.Drawing.Size(667, 319); - this.tabPage1.TabIndex = 0; - this.tabPage1.Text = "Connect"; - this.tabPage1.UseVisualStyleBackColor = true; - // - // label5 - // - this.label5.AutoSize = true; - this.label5.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label5.Location = new System.Drawing.Point(33, 231); - this.label5.Name = "label5"; - this.label5.Size = new System.Drawing.Size(604, 24); - this.label5.TabIndex = 15; - this.label5.Text = "Enter the CMS Address, Key and Local Library Location and click Save."; - // - // tabPage2 - // - this.tabPage2.Controls.Add(this.label4); - this.tabPage2.Controls.Add(this.textBoxProxyDomain); - this.tabPage2.Controls.Add(this.labelProxyUser); - this.tabPage2.Controls.Add(this.labelProxyDomain); - this.tabPage2.Controls.Add(this.maskedTextBoxProxyPass); - this.tabPage2.Controls.Add(this.splashButtonBrowse); - this.tabPage2.Controls.Add(this.textBoxProxyUser); - this.tabPage2.Controls.Add(this.label9); - this.tabPage2.Controls.Add(this.tbHardwareKey); - this.tabPage2.Controls.Add(this.labelProxyPass); - this.tabPage2.Controls.Add(this.linkLabel1); - this.tabPage2.Controls.Add(this.label16); - this.tabPage2.Controls.Add(this.label17); - this.tabPage2.Controls.Add(this.splashOverride); - this.tabPage2.Location = new System.Drawing.Point(4, 22); - this.tabPage2.Name = "tabPage2"; - this.tabPage2.Padding = new System.Windows.Forms.Padding(3); - this.tabPage2.Size = new System.Drawing.Size(667, 319); - this.tabPage2.TabIndex = 1; - this.tabPage2.Text = "Advanced"; - this.tabPage2.UseVisualStyleBackColor = true; - // - // label4 - // - this.label4.AutoSize = true; - this.label4.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label4.Location = new System.Drawing.Point(144, 13); - this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(215, 24); - this.label4.TabIndex = 15; - this.label4.Text = "Proxy Server Information"; - // - // OptionForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(698, 561); - this.Controls.Add(this.tabControl1); - this.Controls.Add(this.button1); - this.Controls.Add(this.buttonExit); - this.Controls.Add(this.tbStatus); - this.Controls.Add(this.buttonDisplayAdmin); - this.Controls.Add(this.menuStrip1); - this.Controls.Add(this.buttonSaveSettings); - this.MainMenuStrip = this.menuStrip1; - this.Name = "OptionForm"; - this.Text = "Player Options"; - this.menuStrip1.ResumeLayout(false); - this.menuStrip1.PerformLayout(); - this.tabControl1.ResumeLayout(false); - this.tabPage1.ResumeLayout(false); - this.tabPage1.PerformLayout(); - this.tabPage2.ResumeLayout(false); - this.tabPage2.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private XiboClient.xmds.xmds xmds1; - private System.Windows.Forms.TextBox textBoxXmdsUri; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.TextBox textBoxServerKey; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.TextBox textBoxLibraryPath; - private System.Windows.Forms.Label label3; - private System.Windows.Forms.Button buttonSaveSettings; - private System.Windows.Forms.MenuStrip menuStrip1; - private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem1; - private System.Windows.Forms.FolderBrowserDialog folderBrowserLibrary; - private System.Windows.Forms.Button buttonLibrary; - private System.Windows.Forms.ToolStripMenuItem onlineHelpToolStripMenuItem; - private System.Windows.Forms.Button buttonDisplayAdmin; - private System.Windows.Forms.MaskedTextBox maskedTextBoxProxyPass; - private System.Windows.Forms.TextBox textBoxProxyUser; - private System.Windows.Forms.Label labelProxyDomain; - private System.Windows.Forms.Label labelProxyPass; - private System.Windows.Forms.Label labelProxyUser; - private System.Windows.Forms.TextBox textBoxProxyDomain; - private System.Windows.Forms.TextBox tbHardwareKey; - private System.Windows.Forms.Label label9; - private System.Windows.Forms.OpenFileDialog splashScreenOverride; - private System.Windows.Forms.Button splashButtonBrowse; - private System.Windows.Forms.LinkLabel linkLabel1; - private System.Windows.Forms.Label label17; - private System.Windows.Forms.Label label16; - private System.Windows.Forms.TextBox splashOverride; - private System.Windows.Forms.TextBox tbStatus; - private System.Windows.Forms.Button buttonExit; - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TabControl tabControl1; - private System.Windows.Forms.TabPage tabPage1; - private System.Windows.Forms.Label label5; - private System.Windows.Forms.TabPage tabPage2; - private System.Windows.Forms.Label label4; - } -} \ No newline at end of file diff --git a/Forms/OptionForm.cs b/Forms/OptionForm.cs deleted file mode 100644 index 8088392f..00000000 --- a/Forms/OptionForm.cs +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2014 Daniel Garner - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Security.Cryptography.X509Certificates; -using System.Data; -using System.Drawing; -using System.Text; -using System.Net; -using System.Windows.Forms; -using XiboClient.Properties; -using System.Diagnostics; -using System.Xml; -using XiboClient.XmdsAgents; - -namespace XiboClient -{ - public partial class OptionForm : Form - { - private HardwareKey _hardwareKey; - - - public OptionForm() - { - InitializeComponent(); - - // Set the icon - Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); - - // Hide unnecessary fields - if (Application.ProductName != "Xibo") - { - label17.Hide(); - linkLabel1.Hide(); - } - - // Get a hardware key here, just in case we havent been able to get one before - _hardwareKey = new HardwareKey(); - - // XMDS completed event - xmds1.RegisterDisplayCompleted += new XiboClient.xmds.RegisterDisplayCompletedEventHandler(xmds1_RegisterDisplayCompleted); - - // Set global proxy information - OptionForm.SetGlobalProxy(); - - // Settings Tab - textBoxXmdsUri.Text = ApplicationSettings.Default.ServerUri; - textBoxServerKey.Text = ApplicationSettings.Default.ServerKey; - textBoxLibraryPath.Text = ApplicationSettings.Default.LibraryPath; - tbHardwareKey.Text = ApplicationSettings.Default.HardwareKey; - - // Proxy Tab - textBoxProxyUser.Text = ApplicationSettings.Default.ProxyUser; - maskedTextBoxProxyPass.Text = ApplicationSettings.Default.ProxyPassword; - textBoxProxyDomain.Text = ApplicationSettings.Default.ProxyDomain; - - // Appearance Tab - splashOverride.Text = ApplicationSettings.Default.SplashOverride; - - // Switch to TLS 2.1 - System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - - Debug.WriteLine("Loaded Options Form", "OptionForm"); - } - - /// - /// Register display completed - /// - /// - /// - void xmds1_RegisterDisplayCompleted(object sender, XiboClient.xmds.RegisterDisplayCompletedEventArgs e) - { - tbStatus.ResetText(); - - if (e.Error != null) - { - tbStatus.AppendText("Status" + Environment.NewLine); - tbStatus.AppendText(e.Error.Message); - - Debug.WriteLine("Error returned from Call to XMDS Register Display.", "xmds1_RegisterDisplayCompleted"); - Debug.WriteLine(e.Error.Message, "xmds1_RegisterDisplayCompleted"); - Debug.WriteLine(e.Error.StackTrace, "xmds1_RegisterDisplayCompleted"); - } - else - { - tbStatus.AppendText(RegisterAgent.ProcessRegisterXml(e.Result)); - } - } - - /// - /// Save settings - /// - /// - /// - private void buttonSaveSettings_Click(object sender, EventArgs e) - { - tbStatus.ResetText(); - try - { - tbStatus.AppendText("Saving with CMS... Please wait..."); - - // Simple settings - ApplicationSettings.Default.ServerKey = textBoxServerKey.Text; - ApplicationSettings.Default.LibraryPath = textBoxLibraryPath.Text.TrimEnd('\\'); - ApplicationSettings.Default.ServerUri = textBoxXmdsUri.Text; - ApplicationSettings.Default.HardwareKey = tbHardwareKey.Text; - - // Also tweak the address of the xmds1 - xmds1.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=registerDisplay"; - - // Proxy Settings - ApplicationSettings.Default.ProxyUser = textBoxProxyUser.Text; - ApplicationSettings.Default.ProxyPassword = maskedTextBoxProxyPass.Text; - ApplicationSettings.Default.ProxyDomain = textBoxProxyDomain.Text; - - // Change the default Proxy class - OptionForm.SetGlobalProxy(); - - // Client settings - ApplicationSettings.Default.SplashOverride = splashOverride.Text; - - // Commit these changes back to the user settings - ApplicationSettings.Default.Save(); - - // Call register - xmds1.RegisterDisplayAsync( - ApplicationSettings.Default.ServerKey, - ApplicationSettings.Default.HardwareKey, - ApplicationSettings.Default.DisplayName, - "windows", - ApplicationSettings.Default.ClientVersion, - ApplicationSettings.Default.ClientCodeVersion, - Environment.OSVersion.ToString(), - _hardwareKey.MacAddress, - _hardwareKey.Channel, - _hardwareKey.getXmrPublicKey()); - } - catch (Exception ex) - { - tbStatus.AppendText(ex.Message); - } - } - - private void exitToolStripMenuItem_Click(object sender, EventArgs e) - { - this.Close(); - } - - - private void buttonLibrary_Click(object sender, EventArgs e) - { - // Set the dialog - folderBrowserLibrary.SelectedPath = textBoxLibraryPath.Text; - - // Open the dialog - if (folderBrowserLibrary.ShowDialog() == DialogResult.OK) - { - textBoxLibraryPath.Text = folderBrowserLibrary.SelectedPath; - } - } - - private void splashButtonBrowse_Click(object sender, EventArgs e) - { - // Set the dialog - splashScreenOverride.FileName = splashOverride.Text; - - if (splashScreenOverride.ShowDialog() == DialogResult.OK) - { - splashOverride.Text = splashScreenOverride.FileName; - } - } - - private void onlineHelpToolStripMenuItem_Click(object sender, EventArgs e) - { - // open URL in separate instance of default browser - try - { - Process.Start(Properties.Resources.SupportUrl); - } - catch - { - MessageBox.Show("No web browser installed"); - } - } - - private void aboutToolStripMenuItem1_Click(object sender, EventArgs e) - { - About about = new About(); - about.ShowDialog(); - } - - private void buttonDisplayAdmin_Click(object sender, EventArgs e) - { - // open URL in separate instance of default browser - try - { - System.Diagnostics.Process.Start(ApplicationSettings.Default.ServerUri + @"/index.php?p=display"); - } - catch - { - MessageBox.Show("No web browser installed"); - } - } - - /// - /// Sets up the global proxy - /// - public static void SetGlobalProxy() - { - Debug.WriteLine("[IN]", "SetGlobalProxy"); - - Debug.WriteLine("Trying to detect a proxy.", "SetGlobalProxy"); - - if (ApplicationSettings.Default.ProxyUser != "") - { - // disable expect100Continue - ServicePointManager.Expect100Continue = false; - - Debug.WriteLine("Creating a network credential using the Proxy User.", "SetGlobalProxy"); - - NetworkCredential nc = new NetworkCredential(ApplicationSettings.Default.ProxyUser, ApplicationSettings.Default.ProxyPassword); - - if (ApplicationSettings.Default.ProxyDomain != "") - nc.Domain = ApplicationSettings.Default.ProxyDomain; - - WebRequest.DefaultWebProxy.Credentials = nc; - } - else - { - Debug.WriteLine("No Proxy.", "SetGlobalProxy"); - WebRequest.DefaultWebProxy.Credentials = null; - } - - // What if the URL for XMDS has a SSL certificate? - ServicePointManager.ServerCertificateValidationCallback += delegate(object sender, X509Certificate certificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors) - { - Debug.WriteLine("[IN]", "ServerCertificateValidationCallback"); - bool validationResult = false; - - Debug.WriteLine(certificate.Subject); - Debug.WriteLine(certificate.Issuer); - - if (sslPolicyErrors != System.Net.Security.SslPolicyErrors.None) - { - Debug.WriteLine(sslPolicyErrors.ToString()); - } - - validationResult = true; - - Debug.WriteLine("[OUT]", "ServerCertificateValidationCallback"); - return validationResult; - }; - - Debug.WriteLine("[OUT]", "SetGlobalProxy"); - - return; - } - - private void buttonExit_Click(object sender, EventArgs e) - { - Close(); - } - - private void button1_Click(object sender, EventArgs e) - { - Close(); - Process.Start(Application.ExecutablePath); - } - } -} \ No newline at end of file diff --git a/Forms/OptionForm.resx b/Forms/OptionForm.resx deleted file mode 100644 index ab6231ed..00000000 --- a/Forms/OptionForm.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 101, 17 - - - 210, 17 - - - 374, 17 - - - 17, 17 - - \ No newline at end of file diff --git a/Log/ClientInfo.Designer.cs b/Log/ClientInfo.Designer.cs deleted file mode 100644 index 39c8d078..00000000 --- a/Log/ClientInfo.Designer.cs +++ /dev/null @@ -1,271 +0,0 @@ -namespace XiboClient.Log -{ - partial class ClientInfo - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); - System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); - System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle(); - this.logDataGridView = new System.Windows.Forms.DataGridView(); - this.Thread = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Date = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Type = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Method = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Message = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.label1 = new System.Windows.Forms.Label(); - this.scheduleStatusLabel = new System.Windows.Forms.Label(); - this.label2 = new System.Windows.Forms.Label(); - this.requiredFilesStatus = new System.Windows.Forms.Label(); - this.requiredFilesTextBox = new System.Windows.Forms.TextBox(); - this.scheduleManagerStatus = new System.Windows.Forms.TextBox(); - this.saveLogToDisk = new System.Windows.Forms.Button(); - this.saveFileDialog = new System.Windows.Forms.SaveFileDialog(); - this.label3 = new System.Windows.Forms.Label(); - this.xmrStatus = new System.Windows.Forms.Label(); - this.controlCountLabel = new System.Windows.Forms.Label(); - ((System.ComponentModel.ISupportInitialize)(this.logDataGridView)).BeginInit(); - this.SuspendLayout(); - // - // logDataGridView - // - this.logDataGridView.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.DisplayedCells; - this.logDataGridView.ClipboardCopyMode = System.Windows.Forms.DataGridViewClipboardCopyMode.Disable; - dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; - dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Control; - dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.WindowText; - dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight; - dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText; - dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; - this.logDataGridView.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1; - this.logDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.logDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { - this.Thread, - this.Date, - this.Type, - this.Method, - this.Message}); - dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; - dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window; - dataGridViewCellStyle2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText; - dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight; - dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText; - dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.False; - this.logDataGridView.DefaultCellStyle = dataGridViewCellStyle2; - this.logDataGridView.Location = new System.Drawing.Point(12, 343); - this.logDataGridView.Name = "logDataGridView"; - this.logDataGridView.ReadOnly = true; - dataGridViewCellStyle3.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; - dataGridViewCellStyle3.BackColor = System.Drawing.SystemColors.Control; - dataGridViewCellStyle3.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - dataGridViewCellStyle3.ForeColor = System.Drawing.SystemColors.WindowText; - dataGridViewCellStyle3.SelectionBackColor = System.Drawing.SystemColors.Highlight; - dataGridViewCellStyle3.SelectionForeColor = System.Drawing.SystemColors.HighlightText; - dataGridViewCellStyle3.WrapMode = System.Windows.Forms.DataGridViewTriState.True; - this.logDataGridView.RowHeadersDefaultCellStyle = dataGridViewCellStyle3; - this.logDataGridView.Size = new System.Drawing.Size(786, 324); - this.logDataGridView.TabIndex = 0; - // - // Thread - // - this.Thread.HeaderText = "Thread"; - this.Thread.Name = "Thread"; - this.Thread.ReadOnly = true; - this.Thread.Width = 66; - // - // Date - // - this.Date.HeaderText = "Date"; - this.Date.Name = "Date"; - this.Date.ReadOnly = true; - this.Date.Width = 55; - // - // Type - // - this.Type.HeaderText = "Type"; - this.Type.Name = "Type"; - this.Type.ReadOnly = true; - this.Type.Width = 56; - // - // Method - // - this.Method.HeaderText = "Method"; - this.Method.Name = "Method"; - this.Method.ReadOnly = true; - this.Method.Width = 68; - // - // Message - // - this.Message.HeaderText = "Message"; - this.Message.Name = "Message"; - this.Message.ReadOnly = true; - this.Message.Width = 75; - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(9, 11); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(88, 13); - this.label1.TabIndex = 1; - this.label1.Text = "Schedule Status:"; - // - // scheduleStatusLabel - // - this.scheduleStatusLabel.AutoSize = true; - this.scheduleStatusLabel.Location = new System.Drawing.Point(103, 11); - this.scheduleStatusLabel.Name = "scheduleStatusLabel"; - this.scheduleStatusLabel.Size = new System.Drawing.Size(61, 13); - this.scheduleStatusLabel.TabIndex = 2; - this.scheduleStatusLabel.Text = "Not Started"; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(403, 11); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(110, 13); - this.label2.TabIndex = 3; - this.label2.Text = "Required Files Status:"; - // - // requiredFilesStatus - // - this.requiredFilesStatus.AutoSize = true; - this.requiredFilesStatus.Location = new System.Drawing.Point(519, 11); - this.requiredFilesStatus.Name = "requiredFilesStatus"; - this.requiredFilesStatus.Size = new System.Drawing.Size(61, 13); - this.requiredFilesStatus.TabIndex = 4; - this.requiredFilesStatus.Text = "Not Started"; - // - // requiredFilesTextBox - // - this.requiredFilesTextBox.Location = new System.Drawing.Point(406, 27); - this.requiredFilesTextBox.Multiline = true; - this.requiredFilesTextBox.Name = "requiredFilesTextBox"; - this.requiredFilesTextBox.ReadOnly = true; - this.requiredFilesTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.requiredFilesTextBox.Size = new System.Drawing.Size(392, 281); - this.requiredFilesTextBox.TabIndex = 5; - // - // scheduleManagerStatus - // - this.scheduleManagerStatus.Location = new System.Drawing.Point(12, 27); - this.scheduleManagerStatus.Multiline = true; - this.scheduleManagerStatus.Name = "scheduleManagerStatus"; - this.scheduleManagerStatus.ReadOnly = true; - this.scheduleManagerStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.scheduleManagerStatus.Size = new System.Drawing.Size(387, 281); - this.scheduleManagerStatus.TabIndex = 8; - // - // saveLogToDisk - // - this.saveLogToDisk.Location = new System.Drawing.Point(12, 314); - this.saveLogToDisk.Name = "saveLogToDisk"; - this.saveLogToDisk.Size = new System.Drawing.Size(75, 23); - this.saveLogToDisk.TabIndex = 9; - this.saveLogToDisk.Text = "Save Log"; - this.saveLogToDisk.UseVisualStyleBackColor = true; - this.saveLogToDisk.Click += new System.EventHandler(this.saveLogToDisk_Click); - // - // saveFileDialog - // - this.saveFileDialog.FileOk += new System.ComponentModel.CancelEventHandler(this.saveFileDialog_FileOk); - // - // label3 - // - this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(102, 319); - this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(67, 13); - this.label3.TabIndex = 10; - this.label3.Text = "XMR Status:"; - // - // xmrStatus - // - this.xmrStatus.AutoSize = true; - this.xmrStatus.Location = new System.Drawing.Point(175, 319); - this.xmrStatus.Name = "xmrStatus"; - this.xmrStatus.Size = new System.Drawing.Size(61, 13); - this.xmrStatus.TabIndex = 11; - this.xmrStatus.Text = "Not Started"; - // - // controlCountLabel - // - this.controlCountLabel.AutoSize = true; - this.controlCountLabel.Location = new System.Drawing.Point(785, 319); - this.controlCountLabel.Name = "controlCountLabel"; - this.controlCountLabel.Size = new System.Drawing.Size(13, 13); - this.controlCountLabel.TabIndex = 12; - this.controlCountLabel.Text = "0"; - // - // ClientInfo - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(810, 676); - this.Controls.Add(this.controlCountLabel); - this.Controls.Add(this.xmrStatus); - this.Controls.Add(this.label3); - this.Controls.Add(this.saveLogToDisk); - this.Controls.Add(this.scheduleManagerStatus); - this.Controls.Add(this.requiredFilesTextBox); - this.Controls.Add(this.requiredFilesStatus); - this.Controls.Add(this.label2); - this.Controls.Add(this.scheduleStatusLabel); - this.Controls.Add(this.label1); - this.Controls.Add(this.logDataGridView); - this.Name = "ClientInfo"; - this.Text = "Player Information and Status"; - ((System.ComponentModel.ISupportInitialize)(this.logDataGridView)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.DataGridView logDataGridView; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.Label scheduleStatusLabel; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.Label requiredFilesStatus; - private System.Windows.Forms.TextBox requiredFilesTextBox; - private System.Windows.Forms.DataGridViewTextBoxColumn Thread; - private System.Windows.Forms.DataGridViewTextBoxColumn Date; - private System.Windows.Forms.DataGridViewTextBoxColumn Type; - private System.Windows.Forms.DataGridViewTextBoxColumn Method; - private System.Windows.Forms.DataGridViewTextBoxColumn Message; - private System.Windows.Forms.TextBox scheduleManagerStatus; - private System.Windows.Forms.Button saveLogToDisk; - private System.Windows.Forms.SaveFileDialog saveFileDialog; - private System.Windows.Forms.Label label3; - private System.Windows.Forms.Label xmrStatus; - private System.Windows.Forms.Label controlCountLabel; - } -} \ No newline at end of file diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index 941a8e02..a02ac96d 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -1,184 +1,67 @@ +using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Text; -using System.Windows.Forms; using System.Diagnostics; -using System.Xml.Serialization; -using XiboClient.Properties; using System.IO; -using Newtonsoft.Json; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Threading; namespace XiboClient.Log { - public partial class ClientInfo : Form + public sealed class ClientInfo { - public delegate void StatusDelegate(string status); - public delegate void AddLogMessage(string message, LogType logType); + private static readonly Lazy + lazy = + new Lazy + (() => new ClientInfo()); - // Delegate for updating the status file - public delegate void UpdateStatusFile(); + public static ClientInfo Instance { get { return lazy.Value; } } /// /// Set the schedule status /// - public string ScheduleStatus - { - set - { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetScheduleStatus), value); - } - else - { - SetScheduleStatus(value); - } - } - } + public string ScheduleStatus; /// /// Set the required files status /// - public string RequiredFilesStatus - { - set - { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetRequiredFilesStatus), value); - } - else - { - SetRequiredFilesStatus(value); - } - } - } + public string RequiredFilesStatus; /// /// Set the schedule manager status /// - public string ScheduleManagerStatus - { - set - { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetScheduleManagerStatus), value); - } - else - { - SetScheduleManagerStatus(value); - } - } - } + public string ScheduleManagerStatus; /// /// Current Layout Id /// - public string CurrentLayoutId - { - set - { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetCurrentlyPlaying), value); - } - else - { - SetCurrentlyPlaying(value); - } - } - } + public string CurrentLayoutId; /// /// XMR Status /// - public string XmrSubscriberStatus - { - set - { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetXmrStatus), value); - } - else - { - SetXmrStatus(value); - } - } - } + public string XmrSubscriberStatus; /// /// Control Count /// - public int ControlCount - { - set - { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetControlCount), "" + value); - } - else - { - SetControlCount("" + value); - } - } - } - - /// - /// Client Info Object - /// - public ClientInfo() - { - InitializeComponent(); - Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); - MaximizeBox = false; - MinimizeBox = false; - - FormClosing += new FormClosingEventHandler(ClientInfo_FormClosing); - - // Put the XMDS url on the title window - Text = "Client Information and Status - " + ApplicationSettings.Default.ServerUri; - } - - /// - /// Set the schedule status - /// - /// - public void SetScheduleStatus(string status) - { - scheduleStatusLabel.Text = status; - } + public int ControlCount; /// - /// Set the schedule status + /// The title /// - /// - public void SetRequiredFilesStatus(string status) - { - requiredFilesStatus.Text = status; - } + private string Title; /// - /// Set the required files textbox - /// - /// - public void SetRequiredFilesTextBox(string status) - { - requiredFilesTextBox.Text = status; - } - - /// - /// Set the schedule manager status + /// Client Info Object /// - /// - public void SetScheduleManagerStatus(string status) + private ClientInfo() { - scheduleManagerStatus.Text = status; + // Put the XMDS url on the title window + Title = "Player Information and Status - " + ApplicationSettings.Default.ServerUri; } /// @@ -187,25 +70,7 @@ public void SetScheduleManagerStatus(string status) /// public void SetCurrentlyPlaying(string layoutName) { - Text = "Client Information and Status - " + ApplicationSettings.Default.ServerUri + " - Currently Showing: " + layoutName; - } - - /// - /// Sets the XMR Status - /// - /// - public void SetXmrStatus(string status) - { - xmrStatus.Text = status; - } - - /// - /// Set control count label - /// - /// - public void SetControlCount(string count) - { - controlCountLabel.Text = count; + Title = "Client Information and Status - " + ApplicationSettings.Default.ServerUri + " - Currently Showing: " + layoutName; } /// @@ -214,7 +79,7 @@ public void SetControlCount(string count) /// public void AddToLogGrid(string message, LogType logType) { - if (InvokeRequired) + /*if (InvokeRequired) { BeginInvoke(new AddLogMessage(AddToLogGrid), new object[] { message, logType }); return; @@ -241,7 +106,7 @@ public void AddToLogGrid(string message, LogType logType) logDataGridView.Rows[newRow].Cells[1].Value = logMessage.LogDate.ToString(); logDataGridView.Rows[newRow].Cells[2].Value = logType.ToString(); logDataGridView.Rows[newRow].Cells[3].Value = logMessage._method; - logDataGridView.Rows[newRow].Cells[4].Value = logMessage._message; + logDataGridView.Rows[newRow].Cells[4].Value = logMessage._message;*/ } /// @@ -249,84 +114,18 @@ public void AddToLogGrid(string message, LogType logType) /// public void UpdateRequiredFiles(string requiredFilesString) { - if (InvokeRequired) - { - BeginInvoke(new StatusDelegate(SetRequiredFilesTextBox), requiredFilesString); - } - else - { - SetRequiredFilesTextBox(requiredFilesString); - } - } - - /// - /// Form Closing Event (prevents the form from closing) - /// - /// - /// - void ClientInfo_FormClosing(object sender, FormClosingEventArgs e) - { - // Prevent the form from closing - e.Cancel = true; - Hide(); - } - - /// - /// Saves the log to disk - /// - /// - /// - private void saveLogToDisk_Click(object sender, EventArgs e) - { - saveFileDialog.InitialDirectory = ApplicationSettings.Default.LibraryPath; - saveFileDialog.ShowDialog(); - } - - private void saveFileDialog_FileOk(object sender, CancelEventArgs e) - { - using (StreamWriter wrt = new StreamWriter(saveFileDialog.FileName)) - { - foreach (DataGridViewRow row in logDataGridView.Rows) - { - if (row.Cells[0].Value != null) - { - wrt.Write(row.Cells[0].Value.ToString()); - for (int i = 1; i < row.Cells.Count; i++) - { - wrt.Write("|" + row.Cells[i].Value.ToString()); - } - wrt.WriteLine(); - } - } - } - - MessageBox.Show("Log saved as " + saveFileDialog.FileName, "Log Saved"); + RequiredFilesStatus = requiredFilesString; } /// /// Update Status Marker File /// public void UpdateStatusMarkerFile() - { - if (InvokeRequired) - { - BeginInvoke(new UpdateStatusFile(updateStatusFile)); - } - else - { - updateStatusFile(); - } - } - - /// - /// Update status file - /// - private void updateStatusFile() { try { File.WriteAllText(Path.Combine(ApplicationSettings.Default.LibraryPath, "status.json"), - "{\"lastActivity\":\"" + DateTime.Now.ToString() + "\",\"state\":\"" + Thread.State.ToString() + "\",\"xmdsLastActivity\":\"" + ApplicationSettings.Default.XmdsLastConnection.ToString() + "\",\"xmdsCollectInterval\":\"" + ApplicationSettings.Default.CollectInterval.ToString() + "\"}"); + "{\"lastActivity\":\"" + DateTime.Now.ToString() + "\",\"state\":\"" + Thread.CurrentThread.ThreadState.ToString() + "\",\"xmdsLastActivity\":\"" + ApplicationSettings.Default.XmdsLastConnection.ToString() + "\",\"xmdsCollectInterval\":\"" + ApplicationSettings.Default.CollectInterval.ToString() + "\"}"); } catch (Exception e) { @@ -348,15 +147,15 @@ public void notifyStatusToXmds() writer.WritePropertyName("lastActivity"); writer.WriteValue(DateTime.Now.ToString()); writer.WritePropertyName("applicationState"); - writer.WriteValue(Thread.State.ToString()); + writer.WriteValue(Thread.CurrentThread.ThreadState.ToString()); writer.WritePropertyName("xmdsLastActivity"); writer.WriteValue(ApplicationSettings.Default.XmdsLastConnection.ToString()); writer.WritePropertyName("scheduleStatus"); - writer.WriteValue(scheduleStatusLabel.Text); + writer.WriteValue(ScheduleManagerStatus); writer.WritePropertyName("requiredFilesStatus"); - writer.WriteValue(requiredFilesStatus.Text); + writer.WriteValue(RequiredFilesStatus); writer.WritePropertyName("xmrStatus"); - writer.WriteValue(xmrStatus.Text); + writer.WriteValue(XmrSubscriberStatus); writer.WriteEndObject(); } diff --git a/Log/ClientInfo.resx b/Log/ClientInfo.resx deleted file mode 100644 index fcdeb54c..00000000 --- a/Log/ClientInfo.resx +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - True - - - True - - - True - - - True - - - True - - - 17, 17 - - \ No newline at end of file diff --git a/Log/XiboTraceListener.cs b/Log/XiboTraceListener.cs index 4001df58..67cfeb9e 100644 --- a/Log/XiboTraceListener.cs +++ b/Log/XiboTraceListener.cs @@ -1,6 +1,6 @@ /* * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2016 Daniel Garner, Spring Signage Ltd + * Copyright (C) 2006-2016 Daniel Garner, Xibo Signage Ltd * * This file is part of Xibo. * diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 04bfcb6f..6eddc97d 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Xibo Signage Ltd + * Copyright (C) 2020 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -26,7 +26,6 @@ using System.Linq; using System.Reflection; using System.Text; -using System.Windows.Forms; using System.Xml; using System.Xml.Serialization; using XiboClient.Logic; @@ -41,9 +40,9 @@ public class ApplicationSettings private List _globalProperties; // Application Specific Settings we want to protect - private readonly string _clientVersion = "2 R201-edge"; + private readonly string _clientVersion = "2 R202 WPF"; private readonly string _version = "5"; - private readonly int _clientCodeVersion = 201; + private readonly int _clientCodeVersion = 202; public string ClientVersion { get { return _clientVersion; } } public string Version { get { return _version; } } @@ -73,10 +72,13 @@ public static ApplicationSettings Default if (_instance != null) return _instance; + // What is our executable path? + string executablePath = Process.GetCurrentProcess().MainModule.FileName; + // Check to see if we need to migrate XmlDocument document; string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string fileName = Path.GetFileNameWithoutExtension(Application.ExecutablePath); + string fileName = Path.GetFileNameWithoutExtension(executablePath); // Create a new application settings instance. _instance = new ApplicationSettings(); @@ -106,7 +108,7 @@ public static ApplicationSettings Default } // Populate it with the default.config.xml - _instance.AppendConfigFile(Path.GetDirectoryName(Application.ExecutablePath) + Path.DirectorySeparatorChar + _default + ".config.xml"); + _instance.AppendConfigFile(Path.GetDirectoryName(executablePath) + Path.DirectorySeparatorChar + _default + ".config.xml"); // Load the global settings. _instance.AppendConfigFile(path + Path.DirectorySeparatorChar + fileName + ".xml"); @@ -154,8 +156,9 @@ public void Save() if (_instance == null) return; + string executablePath = Process.GetCurrentProcess().MainModule.FileName; string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string fileName = Path.GetFileNameWithoutExtension(Application.ExecutablePath); + string fileName = Path.GetFileNameWithoutExtension(executablePath); // Write the global settings file using (XmlWriter writer = XmlWriter.Create(path + Path.DirectorySeparatorChar + fileName + ".xml")) @@ -343,7 +346,7 @@ public string LibraryPath if (_libraryPath == "DEFAULT") { // Get the users document space for a library - _libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + @"\" + Application.ProductName + " Library"; + _libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + @"\" + GetProductNameFromAssembly() + " Library"; } // Check the path exists @@ -550,5 +553,18 @@ public void IncrementXmdsErrorCount() // Settings HASH public string Hash { get; set; } + + + /// + /// Gets the product name from the Assembly + /// + /// Product Name + public static string GetProductNameFromAssembly() + { + return Assembly.GetEntryAssembly() + .GetCustomAttributes(typeof(AssemblyProductAttribute)) + .OfType() + .FirstOrDefault().Product; + } } } diff --git a/Logic/BlackList.cs b/Logic/BlackList.cs index 483e8c1f..7100a136 100644 --- a/Logic/BlackList.cs +++ b/Logic/BlackList.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006,2007,2008 Daniel Garner and James Packer +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -25,10 +26,17 @@ using System.Windows.Forms; using System.Diagnostics; -namespace XiboClient +namespace XiboClient.Logic { - class BlackList : IDisposable + class BlackList { + private static readonly Lazy + lazy = + new Lazy + (() => new BlackList()); + + public static BlackList Instance { get { return lazy.Value; } } + private xmds.xmds xmds1; private HardwareKey hardwareKey; @@ -178,33 +186,6 @@ public Boolean BlackListed(string fileId) return false; } - - #region IDisposableMethods - - private Boolean disposed; - - protected virtual void Dispose(bool disposing) - { - if (!disposed) - { - if (disposing) - { - // Dispose managed resources. - } - - // There are no unmanaged resources to release, but - // if we add them, they need to be released here. - } - disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion } public enum BlackListType { Single, All } diff --git a/Logic/CacheManager.cs b/Logic/CacheManager.cs index 15b97d56..1d272c02 100644 --- a/Logic/CacheManager.cs +++ b/Logic/CacheManager.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2009 Daniel Garner +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -29,14 +30,56 @@ namespace XiboClient { - public class CacheManager + public sealed class CacheManager { - public static object _locker = new object(); + private static readonly Lazy + lazy = + new Lazy + (() => new CacheManager()); + + public static CacheManager Instance { get { return lazy.Value; } } + + private static readonly object _locker = new object(); public Collection _files; - public CacheManager() + private CacheManager() { _files = new Collection(); + + SetCacheManager(); + } + + /// + /// Sets the CacheManager + /// + private void SetCacheManager() + { + try + { + // Deserialise a saved cache manager, and use its files to set our instance. + using (FileStream fileStream = File.Open(ApplicationSettings.Default.LibraryPath + @"\" + ApplicationSettings.Default.CacheManagerFile, FileMode.Open)) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(CacheManager)); + + CacheManager manager = (CacheManager)xmlSerializer.Deserialize(fileStream); + + // Set its files on ourselves + Instance._files = manager._files; + } + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("CacheManager", "Unable to reuse the Cache Manager because: " + ex.Message)); + } + + try + { + Instance.Regenerate(); + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("CacheManager", "Regenerate failed because: " + ex.Message)); + } } /// diff --git a/Logic/KeyInterceptor.cs b/Logic/KeyInterceptor.cs index 514bcb85..d3c7a95a 100644 --- a/Logic/KeyInterceptor.cs +++ b/Logic/KeyInterceptor.cs @@ -1,9 +1,28 @@ -using System; +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; using System.Diagnostics; -using System.Windows.Forms; using System.Runtime.InteropServices; -namespace XiboClient +namespace XiboClient.Logic { /// /// Adapted from: http://blogs.msdn.com/b/toub/archive/2006/05/03/589423.aspx diff --git a/Logic/MediaOption.cs b/Logic/MediaOption.cs index 2b4eea4f..f6133e6a 100644 --- a/Logic/MediaOption.cs +++ b/Logic/MediaOption.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2011 Daniel Garner +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -31,7 +32,7 @@ namespace XiboClient { - class MediaDictionary + public sealed class MediaDictionary { private List _options; diff --git a/Logic/MouseInterceptor.cs b/Logic/MouseInterceptor.cs index 843c6c29..f2081a26 100644 --- a/Logic/MouseInterceptor.cs +++ b/Logic/MouseInterceptor.cs @@ -1,8 +1,8 @@ using System; using System.Diagnostics; -using System.Windows.Forms; using System.Runtime.InteropServices; using System.Drawing; +using System.Windows; namespace XiboClient.Logic { @@ -20,7 +20,7 @@ class MouseInterceptor // The KeyPressed Event public event MouseInterceptorEventHandler MouseEvent; - private static Point _mouseLocation; + private static System.Drawing.Point _mouseLocation; public static IntPtr SetHook() { @@ -53,7 +53,7 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) MouseInterceptor.s_instance.MouseEvent(); } - _mouseLocation = new Point(hookStruct.pt.x, hookStruct.pt.y); + _mouseLocation = new System.Drawing.Point(hookStruct.pt.x, hookStruct.pt.y); } } diff --git a/Logic/RegionOptions.cs b/Logic/RegionOptions.cs index 6fa4ec70..929668f6 100644 --- a/Logic/RegionOptions.cs +++ b/Logic/RegionOptions.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Xibo Signage Ltd + * Copyright (C) 2020 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -23,6 +23,7 @@ using System.Drawing; using System.Text; using System.Xml; +using XiboClient.Rendering; namespace XiboClient { @@ -30,7 +31,7 @@ namespace XiboClient /// The options specific to a region /// NOTE: Don't change this to a class /// - struct RegionOptions + public struct RegionOptions { public double scaleFactor; public int width; diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 889a8b26..c9ce5847 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -23,7 +23,6 @@ using System.Security.Cryptography; using System.IO; using System.Text; -using System.Windows.Forms; using System.Xml; using System.Xml.Serialization; using System.Diagnostics; @@ -43,7 +42,7 @@ namespace XiboClient /// /// Reads the schedule /// - class Schedule + public class Schedule { public delegate void ScheduleChangeDelegate(string layoutPath, int scheduleId, int layoutId); public event ScheduleChangeDelegate ScheduleChangeEvent; @@ -93,9 +92,6 @@ public int CurrentLayoutId // Key private HardwareKey _hardwareKey; - // Cache Manager - private CacheManager _cacheManager; - // Schedule Manager private ScheduleManager _scheduleManager; Thread _scheduleManagerThread; @@ -124,16 +120,11 @@ public int CurrentLayoutId private EmbeddedServer _server; Thread _serverThread; - /// - /// Client Info Form - /// - private ClientInfo _clientInfoForm; - /// /// Create a schedule /// /// - public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref ClientInfo clientInfoForm) + public Schedule(string scheduleLocation) { Trace.WriteLine(string.Format("XMDS Location: {0}", ApplicationSettings.Default.XiboClient_xmds_xmds)); @@ -145,12 +136,6 @@ public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref Clie // Create a new collection for the layouts in the schedule _layoutSchedule = new Collection(); - - // Set cachemanager - _cacheManager = cacheManager; - - // Set client info form - _clientInfoForm = clientInfoForm; // Create a Register Agent _registerAgent = new RegisterAgent(); @@ -159,11 +144,10 @@ public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref Clie _registerAgentThread.Name = "RegisterAgentThread"; // Create a schedule manager - _scheduleManager = new ScheduleManager(_cacheManager, scheduleLocation); + _scheduleManager = new ScheduleManager(scheduleLocation); _scheduleManager.OnNewScheduleAvailable += new ScheduleManager.OnNewScheduleAvailableDelegate(_scheduleManager_OnNewScheduleAvailable); _scheduleManager.OnRefreshSchedule += new ScheduleManager.OnRefreshScheduleDelegate(_scheduleManager_OnRefreshSchedule); _scheduleManager.OnScheduleManagerCheckComplete += _scheduleManager_OnScheduleManagerCheckComplete; - _scheduleManager.ClientInfoForm = _clientInfoForm; // Create a schedule manager thread _scheduleManagerThread = new Thread(new ThreadStart(_scheduleManager.Run)); @@ -171,12 +155,11 @@ public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref Clie // Create a RequiredFilesAgent _scheduleAndRfAgent = new ScheduleAndFilesAgent(); - _scheduleAndRfAgent.CurrentCacheManager = cacheManager; + _scheduleAndRfAgent.CurrentCacheManager = CacheManager.Instance; _scheduleAndRfAgent.CurrentScheduleManager = _scheduleManager; _scheduleAndRfAgent.ScheduleLocation = scheduleLocation; _scheduleAndRfAgent.HardwareKey = _hardwareKey.Key; _scheduleAndRfAgent.OnFullyProvisioned += _requiredFilesAgent_OnFullyProvisioned; - _scheduleAndRfAgent.ClientInfoForm = _clientInfoForm; _scheduleAndRfAgent.OnComplete += new ScheduleAndFilesAgent.OnCompleteDelegate(LayoutFileModified); // Create a thread for the RequiredFiles Agent to run in - but dont start it up yet. @@ -185,7 +168,7 @@ public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref Clie // Library Agent _libraryAgent = new LibraryAgent(); - _libraryAgent.CurrentCacheManager = _cacheManager; + _libraryAgent.CurrentCacheManager = CacheManager.Instance; // Create a thread for the Library Agent to run in - but dont start it up yet. _libraryAgentThread = new Thread(new ThreadStart(_libraryAgent.Run)); @@ -199,7 +182,6 @@ public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref Clie // XMR Subscriber _xmrSubscriber = new XmrSubscriber(); _xmrSubscriber.HardwareKey = _hardwareKey; - _xmrSubscriber.ClientInfoForm = _clientInfoForm; _xmrSubscriber.OnAction += _xmrSubscriber_OnAction; // Thread start @@ -208,7 +190,6 @@ public Schedule(string scheduleLocation, ref CacheManager cacheManager, ref Clie // Embedded Server _server = new EmbeddedServer(); - _server.ClientInfoForm = _clientInfoForm; _server.OnServerClosed += _server_OnServerClosed; _serverThread = new Thread(new ThreadStart(_server.Run)); _serverThread.Name = "EmbeddedServer"; @@ -284,7 +265,7 @@ void _scheduleManager_OnScheduleManagerCheckComplete() if (agentThreadsAlive()) { // Update status marker on the main thread. - _clientInfoForm.UpdateStatusMarkerFile(); + ClientInfo.Instance.UpdateStatusMarkerFile(); } else { @@ -294,7 +275,7 @@ void _scheduleManager_OnScheduleManagerCheckComplete() // Log for overdue XMR if (xmrShouldBeRunning && _xmrSubscriber.LastHeartBeat < DateTime.Now.AddHours(-1)) { - _clientInfoForm.XmrSubscriberStatus = "Long term Inactive (" + ApplicationSettings.Default.XmrNetworkAddress + "), last activity: " + _xmrSubscriber.LastHeartBeat.ToString(); + ClientInfo.Instance.XmrSubscriberStatus = "Long term Inactive (" + ApplicationSettings.Default.XmrNetworkAddress + "), last activity: " + _xmrSubscriber.LastHeartBeat.ToString(); Trace.WriteLine(new LogMessage("Schedule - OnScheduleManagerCheckComplete", "XMR heart beat last received over an hour ago.")); // Issue an XMR restart if we've gone this long without connecting @@ -303,7 +284,7 @@ void _scheduleManager_OnScheduleManagerCheckComplete() } else if (xmrShouldBeRunning && _xmrSubscriber.LastHeartBeat < DateTime.Now.AddMinutes(-5)) { - _clientInfoForm.XmrSubscriberStatus = "Inactive (" + ApplicationSettings.Default.XmrNetworkAddress + "), last activity: " + _xmrSubscriber.LastHeartBeat.ToString(); + ClientInfo.Instance.XmrSubscriberStatus = "Inactive (" + ApplicationSettings.Default.XmrNetworkAddress + "), last activity: " + _xmrSubscriber.LastHeartBeat.ToString(); Trace.WriteLine(new LogMessage("Schedule - OnScheduleManagerCheckComplete", "XMR heart beat last received over 5 minutes ago."), LogType.Audit.ToString()); } } @@ -519,14 +500,14 @@ private void LayoutFileModified(string layoutPath) if (_layoutSchedule[_currentLayout].layoutFile == ApplicationSettings.Default.LibraryPath + @"\" + layoutPath) { // What happens if the action of downloading actually invalidates this layout? - bool valid = _cacheManager.IsValidPath(layoutPath); + bool valid = CacheManager.Instance.IsValidPath(layoutPath); if (valid) { // Check dependents foreach (string dependent in _layoutSchedule[_currentLayout].Dependents) { - if (!string.IsNullOrEmpty(dependent) && !_cacheManager.IsValidPath(dependent)) + if (!string.IsNullOrEmpty(dependent) && !CacheManager.Instance.IsValidPath(dependent)) { valid = false; break; diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index cf55ddcc..353dff45 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -73,7 +73,6 @@ class ScheduleManager private Collection _currentOverlaySchedule; private bool _refreshSchedule; - private CacheManager _cacheManager; private DateTime _lastScreenShotDate; /// @@ -97,9 +96,8 @@ public ClientInfo ClientInfoForm /// Creates a new schedule Manager /// /// - public ScheduleManager(CacheManager cacheManager, string scheduleLocation) + public ScheduleManager(string scheduleLocation) { - _cacheManager = cacheManager; _location = scheduleLocation; // Create an empty layout schedule @@ -445,7 +443,7 @@ private Collection LoadNewSchedule() // Is the layout valid in the cachemanager? try { - if (!_cacheManager.IsValidPath(layout.id + ".xlf")) + if (!CacheManager.Instance.IsValidPath(layout.id + ".xlf")) { invalidLayouts.Add(layout.id); Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Layout invalid: " + layout.id), LogType.Info.ToString()); @@ -464,7 +462,7 @@ private Collection LoadNewSchedule() bool validDependents = true; foreach (string dependent in layout.Dependents) { - if (!string.IsNullOrEmpty(dependent) && !_cacheManager.IsValidPath(dependent)) + if (!string.IsNullOrEmpty(dependent) && !CacheManager.Instance.IsValidPath(dependent)) { invalidLayouts.Add(layout.id); Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Layout has invalid dependent: " + dependent), LogType.Info.ToString()); @@ -568,7 +566,7 @@ private Collection LoadNewOverlaySchedule() // Is the layout valid in the cachemanager? try { - if (!_cacheManager.IsValidPath(layout.id + ".xlf")) + if (!CacheManager.Instance.IsValidPath(layout.id + ".xlf")) { invalidLayouts.Add(layout.id); Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewOverlaySchedule", "Layout invalid: " + layout.id), LogType.Info.ToString()); @@ -586,7 +584,7 @@ private Collection LoadNewOverlaySchedule() // Check dependents foreach (string dependent in layout.Dependents) { - if (!_cacheManager.IsValidPath(dependent)) + if (!CacheManager.Instance.IsValidPath(dependent)) { invalidLayouts.Add(layout.id); Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewOverlaySchedule", "Layout has invalid dependent: " + dependent), LogType.Info.ToString()); diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs deleted file mode 100644 index 1b262771..00000000 --- a/MainForm.Designer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Windows.Forms; -using XiboClient.Properties; -namespace XiboClient -{ - partial class MainForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.SuspendLayout(); - // - // MainForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.BackColor = System.Drawing.Color.Black; - this.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center; - this.DoubleBuffered = global::XiboClient.ApplicationSettings.Default.DoubleBuffering; - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; - this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath); - this.Name = "MainForm"; - this.Load += new System.EventHandler(this.MainForm_Load); - this.ResumeLayout(false); - } - - #endregion - - - - - - } -} - diff --git a/MainForm.cs b/MainForm.cs deleted file mode 100644 index a8e4aaf7..00000000 --- a/MainForm.cs +++ /dev/null @@ -1,1374 +0,0 @@ -/* - * Xibo - Digital Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2018 Spring Signage Ltd - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections; -using System.Collections.ObjectModel; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Text; -using System.Windows.Forms; -using System.Xml; -using System.Net; -using System.IO; -using System.Drawing.Imaging; -using System.Xml.Serialization; -using System.Diagnostics; -using XiboClient.Log; -using System.Threading; -using XiboClient.Properties; -using System.Runtime.InteropServices; -using System.Globalization; -using XiboClient.Logic; -using XiboClient.Control; -using XiboClient.Error; - -namespace XiboClient -{ - public partial class MainForm : Form - { - /// - /// Schedule Class - /// - private Schedule _schedule; - - /// - /// Regions - /// - private Collection _regions; - - /// - /// Overlay Regions - /// - private Collection _overlays; - - private bool _changingLayout = false; - private int _scheduleId; - private int _layoutId; - private bool _screenSaver = false; - private bool _showingSplash = false; - - double _layoutWidth; - double _layoutHeight; - double _scaleFactor; - - private StatLog _statLog; - private Stat _stat; - private CacheManager _cacheManager; - - private ClientInfo _clientInfoForm; - - private delegate void ChangeToNextLayoutDelegate(string layoutPath); - private delegate void ManageOverlaysDelegate(Collection overlays); - - /// - /// Border style - usually none, but useful for debugging. - /// - private BorderStyle _borderStyle = BorderStyle.None; - - [FlagsAttribute] - enum EXECUTION_STATE : uint - { - ES_AWAYMODE_REQUIRED = 0x00000040, - ES_CONTINUOUS = 0x80000000, - ES_DISPLAY_REQUIRED = 0x00000002, - ES_SYSTEM_REQUIRED = 0x00000001 - } - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); - - // Changes the parent window of the specified child window - [DllImport("user32.dll")] - private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); - - // Changes an attribute of the specified window - [DllImport("user32.dll")] - private static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - // Retrieves information about the specified window - [DllImport("user32.dll", SetLastError = true)] - private static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - // Retrieves the coordinates of a window's client area - [DllImport("user32.dll")] - private static extern bool GetClientRect(IntPtr hWnd, out Rectangle lpRect); - - public MainForm(IntPtr previewHandle) - { - InitializeComponent(); - - // Set the preview window of the screen saver selection - // dialog in Windows as the parent of this form. - SetParent(this.Handle, previewHandle); - - // Set this form to a child form, so that when the screen saver selection - // dialog in Windows is closed, this form will also close. - SetWindowLong(this.Handle, -16, new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000)); - - // Set the size of the screen saver to the size of the screen saver - // preview window in the screen saver selection dialog in Windows. - Rectangle ParentRect; - GetClientRect(previewHandle, out ParentRect); - - ApplicationSettings.Default.SizeX = ParentRect.Size.Width; - ApplicationSettings.Default.SizeY = ParentRect.Size.Height; - ApplicationSettings.Default.OffsetX = 0; - ApplicationSettings.Default.OffsetY = 0; - - InitializeScreenSaver(true); - InitializeXibo(); - } - - public MainForm(bool screenSaver) - { - InitializeComponent(); - - if (screenSaver) - InitializeScreenSaver(false); - - InitializeXibo(); - } - - public MainForm() - { - InitializeComponent(); - InitializeXibo(); - } - - private void InitializeXibo() - { - this.Text = Application.ProductName; - - Thread.CurrentThread.Name = "UI Thread"; - - // Check the directories exist - if (!Directory.Exists(ApplicationSettings.Default.LibraryPath + @"\backgrounds\")) - { - // Will handle the create of everything here - Directory.CreateDirectory(ApplicationSettings.Default.LibraryPath + @"\backgrounds"); - } - - // Default the XmdsConnection - ApplicationSettings.Default.XmdsLastConnection = DateTime.MinValue; - - // Set the Main Window Size - SetMainWindowSize(); - - // Bind to the resize event - Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; - - // Show in taskbar - ShowInTaskbar = ApplicationSettings.Default.ShowInTaskbar; - - // Setup the proxy information - OptionForm.SetGlobalProxy(); - - _statLog = new StatLog(); - - this.FormClosing += new FormClosingEventHandler(MainForm_FormClosing); - this.Shown += new EventHandler(MainForm_Shown); - - // Create the info form - _clientInfoForm = new ClientInfo(); - _clientInfoForm.Hide(); - - // Define the hotkey - Keys key; - try - { - key = (Keys)Enum.Parse(typeof(Keys), ApplicationSettings.Default.ClientInformationKeyCode.ToUpper()); - } - catch - { - // Default back to I - key = Keys.I; - } - - KeyStore.Instance.AddKeyDefinition("ClientInfo", key, ((ApplicationSettings.Default.ClientInfomationCtrlKey) ? Keys.Control : Keys.None)); - - // Register a handler for the key event - KeyStore.Instance.KeyPress += Instance_KeyPress; - - // Trace listener for Client Info - ClientInfoTraceListener clientInfoTraceListener = new ClientInfoTraceListener(_clientInfoForm); - clientInfoTraceListener.Name = "ClientInfo TraceListener"; - Trace.Listeners.Add(clientInfoTraceListener); - - // Log to disk? - if (!string.IsNullOrEmpty(ApplicationSettings.Default.LogToDiskLocation)) - { - TextWriterTraceListener listener = new TextWriterTraceListener(ApplicationSettings.Default.LogToDiskLocation); - Trace.Listeners.Add(listener); - } - -#if !DEBUG - // Initialise the watchdog - if (!_screenSaver) - { - try - { - // Update/write the status.json file - File.WriteAllText(Path.Combine(ApplicationSettings.Default.LibraryPath, "status.json"), "{\"lastActivity\":\"" + DateTime.Now.ToString() + "\"}"); - - // Start watchdog - WatchDogManager.Start(); - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("MainForm - InitializeXibo", "Cannot start watchdog. E = " + e.Message), LogType.Error.ToString()); - } - } -#endif - // An empty set of overlay regions - _overlays = new Collection(); - - // Switch to TLS 2.1 - System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - - Trace.WriteLine(new LogMessage("MainForm", "Client Initialised"), LogType.Info.ToString()); - } - - private void InitializeScreenSaver(bool preview) - { - _screenSaver = true; - - // Configure some listeners for the mouse (to quit) - if (!preview) - { - KeyStore.Instance.ScreenSaver = true; - - MouseInterceptor.Instance.MouseEvent += Instance_MouseEvent; - } - } - - void Instance_MouseEvent() - { - Close(); - } - - /// - /// Handle the Key Event - /// - /// - void Instance_KeyPress(string name) - { - Debug.WriteLine("KeyPress " + name); - if (name == "ClientInfo") - { - // Toggle - if (_clientInfoForm.Visible) - { - _clientInfoForm.Hide(); -#if !DEBUG - if (!_screenSaver) - TopMost = true; -#endif - } - else - { -#if !DEBUG - if (!_screenSaver) - TopMost = false; -#endif - _clientInfoForm.Show(); - _clientInfoForm.BringToFront(); - } - } - else if (name == "ScreenSaver") - { - Debug.WriteLine("Closing due to ScreenSaver key press"); - if (!_screenSaver) - return; - - Close(); - } - } - - /// - /// Called after the form has been shown - /// - /// - /// - void MainForm_Shown(object sender, EventArgs e) - { - // Create a cachemanager - SetCacheManager(); - - try - { - // Create the Schedule - _schedule = new Schedule(ApplicationSettings.Default.LibraryPath + @"\" + ApplicationSettings.Default.ScheduleFile, ref _cacheManager, ref _clientInfoForm); - - // Bind to the schedule change event - notifys of changes to the schedule - _schedule.ScheduleChangeEvent += ScheduleChangeEvent; - - // Bind to the overlay change event - _schedule.OverlayChangeEvent += ScheduleOverlayChangeEvent; - - // Initialize the other schedule components - _schedule.InitializeComponents(); - - // Set this form to topmost -#if !DEBUG - if (!_screenSaver) - TopMost = true; -#endif - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message, LogType.Error.ToString()); - MessageBox.Show("Fatal Error initialising the application. " + ex.Message, "Fatal Error"); - Close(); - Dispose(); - } - } - - /// - /// Called before the form has loaded for the first time - /// - /// - /// - private void MainForm_Load(object sender, EventArgs e) - { - // Is the mouse enabled? - if (!ApplicationSettings.Default.EnableMouse) - // Hide the cursor - Cursor.Hide(); - - // Move the cursor to the starting place - if (!_screenSaver) - SetCursorStartPosition(); - - // Show the splash screen - ShowSplashScreen(); - - // Change the default Proxy class - OptionForm.SetGlobalProxy(); - - // UserApp data - Debug.WriteLine(new LogMessage("MainForm_Load", "User AppData Path: " + ApplicationSettings.Default.LibraryPath), LogType.Info.ToString()); - } - - /// - /// Called as the Main Form starts to close - /// - /// - /// - private void MainForm_FormClosing(object sender, FormClosingEventArgs e) - { - // We want to tidy up some stuff as this form closes. - Trace.Listeners.Remove("ClientInfo TraceListener"); - - try - { - // Close the client info screen - if (_clientInfoForm != null) - _clientInfoForm.Hide(); - - // Stop the schedule object - if (_schedule != null) - _schedule.Stop(); - - // Flush the stats - if (_statLog != null) - _statLog.Flush(); - - // Write the CacheManager to disk - if (_cacheManager != null) - _cacheManager.WriteCacheManager(); - } - catch (NullReferenceException) - { - // Stopped before we really started, nothing to do - } - - // Flush the logs - Trace.Flush(); - } - - /// - /// Sets the CacheManager - /// - private void SetCacheManager() - { - try - { - using (FileStream fileStream = File.Open(ApplicationSettings.Default.LibraryPath + @"\" + ApplicationSettings.Default.CacheManagerFile, FileMode.Open)) - { - XmlSerializer xmlSerializer = new XmlSerializer(typeof(CacheManager)); - - _cacheManager = (CacheManager)xmlSerializer.Deserialize(fileStream); - } - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("MainForm - SetCacheManager", "Unable to reuse the Cache Manager because: " + ex.Message)); - - // Create a new cache manager - _cacheManager = new CacheManager(); - } - - try - { - _cacheManager.Regenerate(); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("MainForm - SetCacheManager", "Regenerate failed because: " + ex.Message)); - } - } - - /// - /// Handles the ScheduleChange event - /// - /// - void ScheduleChangeEvent(string layoutPath, int scheduleId, int layoutId) - { - Trace.WriteLine(new LogMessage("MainForm - ScheduleChangeEvent", string.Format("Schedule Changing to {0}", layoutPath)), LogType.Audit.ToString()); - - // We are changing the layout - _changingLayout = true; - - _scheduleId = scheduleId; - _layoutId = layoutId; - - if (_stat != null) - { - // Log the end of the currently running layout. - _stat.toDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - - // Record this stat event in the statLog object - _statLog.RecordStat(_stat); - } - - if (InvokeRequired) - { - BeginInvoke(new ChangeToNextLayoutDelegate(ChangeToNextLayout), layoutPath); - return; - } - - ChangeToNextLayout(layoutPath); - } - - /// - /// Change to the next layout - /// - private void ChangeToNextLayout(string layoutPath) - { - if (ApplicationSettings.Default.PreventSleep) - { - try - { - SetThreadExecutionState(EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); - } - catch - { - Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Unable to set Thread Execution state"), LogType.Info.ToString()); - } - } - - try - { - // Destroy the Current Layout - try - { - DestroyLayout(); - } - catch (Exception e) - { - // Force collect all controls - foreach (System.Windows.Forms.Control control in Controls) - { - control.Dispose(); - Controls.Remove(control); - } - - Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Destroy Layout Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); - throw e; - } - - // Prepare the next layout - try - { - PrepareLayout(layoutPath); - - _clientInfoForm.CurrentLayoutId = layoutPath; - _schedule.CurrentLayoutId = _layoutId; - } - catch (DefaultLayoutException) - { - throw; - } - catch (Exception e) - { - DestroyLayout(); - Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Prepare Layout Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); - throw; - } - - _clientInfoForm.ControlCount = Controls.Count; - - // Do we need to notify? - try - { - if (ApplicationSettings.Default.SendCurrentLayoutAsStatusUpdate) - { - using (xmds.xmds statusXmds = new xmds.xmds()) - { - statusXmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=notifyStatus"; - statusXmds.NotifyStatusAsync(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, "{\"currentLayoutId\":" + _layoutId + "}"); - } - } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Notify Status Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); - throw; - } - } - catch (Exception ex) - { - if (!(ex is DefaultLayoutException)) - Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Layout Change to " + layoutPath + " failed. Exception raised was: " + ex.Message), LogType.Error.ToString()); - - // Do we have more than one Layout in our schedule? - if (_schedule.ActiveLayouts > 1) - { - _schedule.NextLayout(); - } - else - { - if (!_showingSplash) - ShowSplashScreen(); - - // In 10 seconds fire the next layout - System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer(); - timer.Interval = 10000; - timer.Tick += new EventHandler(splashScreenTimer_Tick); - - // Start the timer - timer.Start(); - } - } - - // We have finished changing the layout - _changingLayout = false; - } - - /// - /// Expire the Splash Screen - /// - /// - /// - void splashScreenTimer_Tick(object sender, EventArgs e) - { - Debug.WriteLine(new LogMessage("timer_Tick", "Loading next layout after splashscreen")); - - System.Windows.Forms.Timer timer = (System.Windows.Forms.Timer)sender; - timer.Stop(); - timer.Dispose(); - - _schedule.NextLayout(); - } - - /// - /// Prepares the Layout.. rendering all the necessary controls - /// - private void PrepareLayout(string layoutPath) - { - // Get this layouts XML - XmlDocument layoutXml = new XmlDocument(); - DateTime layoutModifiedTime; - - // Default or not - if (layoutPath == ApplicationSettings.Default.LibraryPath + @"\Default.xml" || String.IsNullOrEmpty(layoutPath)) - { - throw new DefaultLayoutException(); - } - else - { - // try to open the layout file - try - { - using (FileStream fs = File.Open(layoutPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - using (XmlReader reader = XmlReader.Create(fs)) - { - layoutXml.Load(reader); - - reader.Close(); - } - fs.Close(); - } - } - catch (IOException ioEx) - { - _cacheManager.Remove(layoutPath); - Trace.WriteLine(new LogMessage("MainForm - PrepareLayout", "IOException: " + ioEx.ToString()), LogType.Error.ToString()); - throw; - } - - layoutModifiedTime = File.GetLastWriteTime(layoutPath); - } - - // Attributes of the main layout node - XmlNode layoutNode = layoutXml.SelectSingleNode("/layout"); - - XmlAttributeCollection layoutAttributes = layoutNode.Attributes; - - // Set the background and size of the form - _layoutWidth = int.Parse(layoutAttributes["width"].Value, CultureInfo.InvariantCulture); - _layoutHeight = int.Parse(layoutAttributes["height"].Value, CultureInfo.InvariantCulture); - - // Are stats enabled for this Layout? - bool isStatEnabled = (layoutAttributes["enableStat"] == null) ? true : (int.Parse(layoutAttributes["enableStat"].Value) == 1); - - // Scaling factor, will be applied to all regions - _scaleFactor = Math.Min(ClientSize.Width / _layoutWidth, ClientSize.Height / _layoutHeight); - - // Want to be able to center this shiv - therefore work out which one of these is going to have left overs - int backgroundWidth = (int)(_layoutWidth * _scaleFactor); - int backgroundHeight = (int)(_layoutHeight * _scaleFactor); - - double leftOverX; - double leftOverY; - - try - { - leftOverX = Math.Abs(ClientSize.Width - backgroundWidth); - leftOverY = Math.Abs(ClientSize.Height - backgroundHeight); - - if (leftOverX != 0) leftOverX = leftOverX / 2; - if (leftOverY != 0) leftOverY = leftOverY / 2; - } - catch - { - leftOverX = 0; - leftOverY = 0; - } - - // New region and region options objects - _regions = new Collection(); - RegionOptions options = new RegionOptions(); - options.LayoutModifiedDate = layoutModifiedTime; - options.LayoutSize = ClientSize; - - // Deal with the color - try - { - if (layoutAttributes["bgcolor"] != null && layoutAttributes["bgcolor"].Value != "") - { - this.BackColor = ColorTranslator.FromHtml(layoutAttributes["bgcolor"].Value); - options.backgroundColor = layoutAttributes["bgcolor"].Value; - } - } - catch - { - this.BackColor = Color.Black; // Default black - options.backgroundColor = "#000000"; - } - - // Get the background - try - { - if (layoutAttributes["background"] != null && !string.IsNullOrEmpty(layoutAttributes["background"].Value)) - { - string bgFilePath = ApplicationSettings.Default.LibraryPath + @"\backgrounds\" + backgroundWidth + "x" + backgroundHeight + "_" + layoutAttributes["background"].Value; - - // Create a correctly sized background image in the temp folder - if (!File.Exists(bgFilePath)) - GenerateBackgroundImage(layoutAttributes["background"].Value, backgroundWidth, backgroundHeight, bgFilePath); - - BackgroundImage = new Bitmap(bgFilePath); - options.backgroundImage = @"/backgrounds/" + backgroundWidth + "x" + backgroundHeight + "_" + layoutAttributes["background"].Value; ; - } - else - { - // Assume there is no background image - BackgroundImage = null; - options.backgroundImage = ""; - } - - // We have loaded a layout background and therefore are no longer showing the splash screen - _showingSplash = false; - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("MainForm - PrepareLayout", "Unable to set background: " + ex.Message), LogType.Error.ToString()); - - // Assume there is no background image - this.BackgroundImage = null; - options.backgroundImage = ""; - } - - // Get the regions - XmlNodeList listRegions = layoutXml.SelectNodes("/layout/region"); - XmlNodeList listMedia = layoutXml.SelectNodes("/layout/region/media"); - - // Check to see if there are any regions on this layout. - if (listRegions.Count == 0 || listMedia.Count == 0) - { - Trace.WriteLine(new LogMessage("PrepareLayout", - string.Format("A layout with {0} regions and {1} media has been detected.", listRegions.Count.ToString(), listMedia.Count.ToString())), - LogType.Info.ToString()); - - if (_schedule.ActiveLayouts == 1) - { - Trace.WriteLine(new LogMessage("PrepareLayout", "Only 1 layout scheduled and it has nothing to show."), LogType.Info.ToString()); - - throw new Exception("Only 1 layout schduled and it has nothing to show"); - } - else - { - Trace.WriteLine(new LogMessage("PrepareLayout", - string.Format(string.Format("An empty layout detected, will show for {0} seconds.", ApplicationSettings.Default.EmptyLayoutDuration.ToString()))), LogType.Info.ToString()); - - // Put a small dummy region in place, with a small dummy media node - which expires in 10 seconds. - XmlDocument dummyXml = new XmlDocument(); - dummyXml.LoadXml(string.Format("", - ApplicationSettings.Default.EmptyLayoutDuration.ToString())); - - // Replace the list of regions (they mean nothing as they are empty) - listRegions = dummyXml.SelectNodes("/region"); - } - } - - // Create a start record for this layout - _stat = new Stat(); - _stat.type = StatType.Layout; - _stat.scheduleID = _scheduleId; - _stat.layoutID = _layoutId; - _stat.fromDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - _stat.isEnabled = isStatEnabled; - - foreach (XmlNode region in listRegions) - { - // Is there any media - if (region.ChildNodes.Count == 0) - { - Debug.WriteLine("A region with no media detected"); - continue; - } - - // Region loop setting - options.RegionLoop = false; - - XmlNode regionOptionsNode = region.SelectSingleNode("options"); - - if (regionOptionsNode != null) - { - foreach (XmlNode option in regionOptionsNode.ChildNodes) - { - if (option.Name == "loop" && option.InnerText == "1") - options.RegionLoop = true; - } - } - - //each region - XmlAttributeCollection nodeAttibutes = region.Attributes; - - options.scheduleId = _scheduleId; - options.layoutId = _layoutId; - options.regionId = nodeAttibutes["id"].Value.ToString(); - options.width = (int)(Convert.ToDouble(nodeAttibutes["width"].Value, CultureInfo.InvariantCulture) * _scaleFactor); - options.height = (int)(Convert.ToDouble(nodeAttibutes["height"].Value, CultureInfo.InvariantCulture) * _scaleFactor); - options.left = (int)(Convert.ToDouble(nodeAttibutes["left"].Value, CultureInfo.InvariantCulture) * _scaleFactor); - options.top = (int)(Convert.ToDouble(nodeAttibutes["top"].Value, CultureInfo.InvariantCulture) * _scaleFactor); - - options.scaleFactor = _scaleFactor; - - // Store the original width and original height for scaling - options.originalWidth = (int)Convert.ToDouble(nodeAttibutes["width"].Value, CultureInfo.InvariantCulture); - options.originalHeight = (int)Convert.ToDouble(nodeAttibutes["height"].Value, CultureInfo.InvariantCulture); - - // Set the backgrounds (used for Web content offsets) - options.backgroundLeft = options.left * -1; - options.backgroundTop = options.top * -1; - - // Account for scaling - options.left = options.left + (int)leftOverX; - options.top = options.top + (int)leftOverY; - - // All the media nodes for this region / layout combination - options.mediaNodes = region.SelectNodes("media"); - - Region temp = new Region(ref _statLog, ref _cacheManager); - temp.DurationElapsedEvent += new Region.DurationElapsedDelegate(temp_DurationElapsedEvent); - temp.BorderStyle = _borderStyle; - - Debug.WriteLine("Created new region", "MainForm - Prepare Layout"); - - // Dont be fooled, this innocent little statement kicks everything off - temp.regionOptions = options; - - _regions.Add(temp); - Controls.Add(temp); - - Debug.WriteLine("Adding region", "MainForm - Prepare Layout"); - } - - // Null stuff - listRegions = null; - listMedia = null; - - bringOverlaysForward(); - } - - /// - /// Generates a background image and saves it in the library for use later - /// - /// - /// - /// - /// - private static void GenerateBackgroundImage(string sourceFile, int backgroundWidth, int backgroundHeight, string bgFilePath) - { - Trace.WriteLine(new LogMessage("MainForm - GenerateBackgroundImage", "Trying to generate a background image. It will be saved: " + bgFilePath), LogType.Audit.ToString()); - - using (Image img = Image.FromFile(ApplicationSettings.Default.LibraryPath + @"\" + sourceFile)) - { - using (Bitmap bmp = new Bitmap(img, backgroundWidth, backgroundHeight)) - { - EncoderParameters encoderParameters = new EncoderParameters(1); - EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L); - encoderParameters.Param[0] = qualityParam; - - ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg"); - - bmp.Save(bgFilePath, jpegCodec, encoderParameters); - } - } - } - - /// - /// Shows the splash screen (set the background to the embedded resource) - /// - private void ShowSplashScreen() - { - _showingSplash = true; - - if (!string.IsNullOrEmpty(ApplicationSettings.Default.SplashOverride)) - { - try - { - using (Image bgSplash = Image.FromFile(ApplicationSettings.Default.SplashOverride)) - { - Bitmap bmpSplash = new Bitmap(bgSplash, ClientSize); - BackgroundImage = bmpSplash; - } - } - catch - { - Trace.WriteLine(new LogMessage("ShowSplashScreen", "Unable to load user splash screen"), LogType.Error.ToString()); - ShowDefaultSplashScreen(); - } - } - else - { - ShowDefaultSplashScreen(); - } - } - - /// - /// Show the Default Splash Screen - /// - private void ShowDefaultSplashScreen() - { - // We are running with the Default.xml - meaning the schedule doesnt exist - System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); - Stream resourceStream = assembly.GetManifestResourceStream("XiboClient.Resources.splash.jpg"); - - Debug.WriteLine("Showing Splash Screen"); - - // Load into a stream and then into an Image - try - { - using (Image bgSplash = Image.FromStream(resourceStream)) - { - Bitmap bmpSplash = new Bitmap(bgSplash, ClientSize); - BackgroundImage = bmpSplash; - } - } - catch (Exception ex) - { - // Log - Debug.WriteLine("Failed Showing Splash Screen: " + ex.Message); - } - } - - /// - /// Returns the image codec with the given mime type - /// - private static ImageCodecInfo GetEncoderInfo(string mimeType) - { - // Get image codecs for all image formats - ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders(); - - // Find the correct image codec - for (int i = 0; i < codecs.Length; i++) - if (codecs[i].MimeType == mimeType) - return codecs[i]; - return null; - } - - /// - /// The duration of a Region has been reached - /// - void temp_DurationElapsedEvent() - { - Trace.WriteLine(new LogMessage("MainForm - DurationElapsedEvent", "Region Elapsed"), LogType.Audit.ToString()); - - // Are we already changing the layout? - if (_changingLayout) - { - Trace.WriteLine(new LogMessage("MainForm - DurationElapsedEvent", "Already Changing Layout"), LogType.Audit.ToString()); - return; - } - - bool isExpired = true; - - // Check the other regions to see if they are also expired. - foreach (Region temp in _regions) - { - if (!temp.hasExpired()) - { - isExpired = false; - break; - } - } - - // If we are sure we have expired after checking all regions, then set the layout expired flag on them all - if (isExpired) - { - // Inform each region that the layout containing it has expired - foreach (Region temp in _regions) - { - temp.setLayoutExpired(); - } - - Trace.WriteLine(new LogMessage("MainForm - DurationElapsedEvent", "All Regions have expired. Raising a Next layout event."), LogType.Audit.ToString()); - - // We are changing the layout - _changingLayout = true; - - // Yield and restart - _schedule.NextLayout(); - } - } - - /// - /// Disposes Layout - removes the controls - /// - private void DestroyLayout() - { - Debug.WriteLine("Destroying Layout", "MainForm - DestoryLayout"); - - if (_regions == null) - return; - - lock (_regions) - { - foreach (Region region in _regions) - { - try - { - // Remove the region from the list of controls - Controls.Remove(region); - - // Clear the region - region.Clear(); - - Trace.WriteLine(new LogMessage("MainForm - DestoryLayout", "Calling Dispose on Region " + region.regionOptions.regionId), LogType.Audit.ToString()); - region.Dispose(); - } - catch (Exception e) - { - // If we can't dispose we should log to understand why - Trace.WriteLine(new LogMessage("MainForm - DestoryLayout", e.Message), LogType.Info.ToString()); - } - } - - _regions.Clear(); - } - - _regions = null; - } - - /// - /// Set the Cursor start position - /// - private void SetCursorStartPosition() - { - Point position; - - switch (ApplicationSettings.Default.CursorStartPosition) - { - case "Top Left": - position = new Point(0, 0); - break; - - case "Top Right": - position = new Point(ClientSize.Width, 0); - break; - - case "Bottom Left": - position = new Point(0, ClientSize.Height); - break; - - case "Bottom Right": - position = new Point(ClientSize.Width, ClientSize.Height); - break; - - default: - // The default position or "unchanged" as it will be sent, is to not do anything - // leave the cursor where it is - return; - } - - Cursor.Position = position; - } - - /// - /// Force a flush of the stats log - /// - public void FlushStats() - { - try - { - _statLog.Flush(); - } - catch - { - System.Diagnostics.Trace.WriteLine(new LogMessage("MainForm - FlushStats", "Unable to Flush Stats"), LogType.Error.ToString()); - } - } - - /// - /// Overlay change event. - /// - /// - void ScheduleOverlayChangeEvent(Collection overlays) - { - if (InvokeRequired) - { - BeginInvoke(new ManageOverlaysDelegate(ManageOverlays), overlays); - return; - } - - ManageOverlays(overlays); - } - - /// - /// Manage Overlays - /// - /// - public void ManageOverlays(Collection overlays) - { - try - { - // Parse all overlays and compare what we have now to the overlays we have already created (see OverlayRegions) - Debug.WriteLine("Arrived at Manage Overlays with " + overlays.Count + " overlay schedules to show. We're already showing " + _overlays.Count + " overlay Regions", "Overlays"); - - // Take the ones we currently have up and remove them if they aren't in the new list or if they've been set to refresh - // We use a for loop so that we are able to remove the region from the collection - for (int i = 0; i < _overlays.Count; i++) - { - Debug.WriteLine("Assessing Overlay Region " + i, "Overlays"); - - Region region = _overlays[i]; - bool found = false; - bool refresh = false; - - foreach (ScheduleItem item in overlays) - { - if (item.scheduleid == region.scheduleId) - { - found = true; - refresh = item.Refresh; - break; - } - } - - if (!found || refresh) - { - if (refresh) - { - Trace.WriteLine(new LogMessage("MainForm - ManageOverlays", "Refreshing item that has changed."), LogType.Info.ToString()); - } - Debug.WriteLine("Removing overlay " + i + " which is no-longer required. Overlay: " + region.scheduleId, "Overlays"); - - // Remove the Region from the overlays collection - _overlays.Remove(region); - - // As we've removed the thing we're iterating over, reduce i - i--; - - // Clear down and dispose of the region. - region.Clear(); - region.Dispose(); - Controls.Remove(region); - } - else - { - Debug.WriteLine("Overlay Region found and not needing refresh " + i, "Overlays"); - } - } - - // Take the ones that are in the new list and add them - foreach (ScheduleItem item in overlays) - { - // Check its not already added. - bool found = false; - foreach (Region region in _overlays) - { - if (region.scheduleId == item.scheduleid) - { - found = true; - break; - } - } - - if (found) - { - Debug.WriteLine("Region already found for overlay - we're assuming here that if we've found one, they are all there.", "Overlays"); - continue; - } - - // Reset refresh - item.Refresh = false; - - // Parse the layout for regions, and create them. - string layoutPath = item.layoutFile; - - // Get this layouts XML - XmlDocument layoutXml = new XmlDocument(); - - try - { - // try to open the layout file - using (FileStream fs = File.Open(layoutPath, FileMode.Open, FileAccess.Read, FileShare.Write)) - { - using (XmlReader reader = XmlReader.Create(fs)) - { - layoutXml.Load(reader); - - reader.Close(); - } - fs.Close(); - } - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("MainForm - _schedule_OverlayChangeEvent", string.Format("Could not find the layout file {0}: {1}", layoutPath, ex.Message)), LogType.Info.ToString()); - continue; - } - - // Attributes of the main layout node - XmlNode layoutNode = layoutXml.SelectSingleNode("/layout"); - - XmlAttributeCollection layoutAttributes = layoutNode.Attributes; - - // Set the background and size of the form - double layoutWidth = int.Parse(layoutAttributes["width"].Value, CultureInfo.InvariantCulture); - double layoutHeight = int.Parse(layoutAttributes["height"].Value, CultureInfo.InvariantCulture); - - // Scaling factor, will be applied to all regions - double scaleFactor = Math.Min(ClientSize.Width / layoutWidth, ClientSize.Height / layoutHeight); - - // Want to be able to center this shiv - therefore work out which one of these is going to have left overs - int backgroundWidth = (int)(layoutWidth * scaleFactor); - int backgroundHeight = (int)(layoutHeight * scaleFactor); - - double leftOverX; - double leftOverY; - - try - { - leftOverX = Math.Abs(ClientSize.Width - backgroundWidth); - leftOverY = Math.Abs(ClientSize.Height - backgroundHeight); - - if (leftOverX != 0) leftOverX = leftOverX / 2; - if (leftOverY != 0) leftOverY = leftOverY / 2; - } - catch - { - leftOverX = 0; - leftOverY = 0; - } - - // New region and region options objects - RegionOptions options = new RegionOptions(); - - // Deal with the color - // this is imperfect, but we haven't any way to make these controls transparent. - try - { - if (layoutAttributes["bgcolor"] != null && layoutAttributes["bgcolor"].Value != "") - { - options.backgroundColor = layoutAttributes["bgcolor"].Value; - } - } - catch - { - options.backgroundColor = "#000000"; - } - - // Get the regions - XmlNodeList listRegions = layoutXml.SelectNodes("/layout/region"); - - foreach (XmlNode region in listRegions) - { - // Is there any media - if (region.ChildNodes.Count == 0) - { - Debug.WriteLine("A region with no media detected"); - continue; - } - - //each region - XmlAttributeCollection nodeAttibutes = region.Attributes; - - options.scheduleId = item.scheduleid; - options.layoutId = item.id; - options.regionId = nodeAttibutes["id"].Value.ToString(); - options.width = (int)(Convert.ToDouble(nodeAttibutes["width"].Value, CultureInfo.InvariantCulture) * scaleFactor); - options.height = (int)(Convert.ToDouble(nodeAttibutes["height"].Value, CultureInfo.InvariantCulture) * scaleFactor); - options.left = (int)(Convert.ToDouble(nodeAttibutes["left"].Value, CultureInfo.InvariantCulture) * scaleFactor); - options.top = (int)(Convert.ToDouble(nodeAttibutes["top"].Value, CultureInfo.InvariantCulture) * scaleFactor); - options.scaleFactor = scaleFactor; - - // Store the original width and original height for scaling - options.originalWidth = (int)Convert.ToDouble(nodeAttibutes["width"].Value, CultureInfo.InvariantCulture); - options.originalHeight = (int)Convert.ToDouble(nodeAttibutes["height"].Value, CultureInfo.InvariantCulture); - - // Set the backgrounds (used for Web content offsets) - options.backgroundLeft = options.left * -1; - options.backgroundTop = options.top * -1; - - // Account for scaling - options.left = options.left + (int)leftOverX; - options.top = options.top + (int)leftOverY; - - // All the media nodes for this region / layout combination - options.mediaNodes = region.SelectNodes("media"); - - Region temp = new Region(ref _statLog, ref _cacheManager); - temp.scheduleId = item.scheduleid; - temp.BorderStyle = _borderStyle; - - // Dont be fooled, this innocent little statement kicks everything off - temp.regionOptions = options; - - _overlays.Add(temp); - Controls.Add(temp); - temp.BringToFront(); - } - - // Null stuff - listRegions = null; - } - - _clientInfoForm.ControlCount = Controls.Count; - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("MainForm - _schedule_OverlayChangeEvent", "Unknown issue managing overlays. Ex = " + e.Message), LogType.Info.ToString()); - } - - bringOverlaysForward(); - } - - private void bringOverlaysForward() - { - // Bring overlays to the front - foreach (Region region in _overlays) - { - region.BringToFront(); - } - } - - /// - /// Set the Main Window Size, either to the primary monitor, or the configured size - /// - private void SetMainWindowSize() - { - Debug.WriteLine("SetMainWindowSize: IN"); - - // Override the default size if necessary - if (ApplicationSettings.Default.SizeX != 0 || ApplicationSettings.Default.SizeY != 0) - { - Debug.WriteLine("SetMainWindowSize: Use Settings Size"); - - // Determine the client size - int sizeX = (int)ApplicationSettings.Default.SizeX; - if (sizeX <= 0) - { - sizeX = SystemInformation.PrimaryMonitorSize.Width; - } - - int sizeY = (int)ApplicationSettings.Default.SizeY; - if (sizeY <= 0) - { - sizeY = SystemInformation.PrimaryMonitorSize.Height; - } - - ClientSize = new Size(sizeX, sizeY); - StartPosition = FormStartPosition.Manual; - Location = new Point((int)ApplicationSettings.Default.OffsetX, (int)ApplicationSettings.Default.OffsetY); - } - else - { - Debug.WriteLine("SetMainWindowSize: Use Monitor Size"); - - // Use the primary monitor size - ClientSize = SystemInformation.PrimaryMonitorSize; - } - - Debug.WriteLine("SetMainWindowSize: Setting Size to " + ClientSize.Width + "x" + ClientSize.Height); - - // Use the client size we've calculated to set the actual size of the form - WindowState = FormWindowState.Normal; - Size = ClientSize; - - Debug.WriteLine("SetMainWindowSize: OUT"); - } - - /// - /// Display Settings Changed - /// - /// - /// - private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) - { - Trace.WriteLine(new LogMessage("SystemEvents_DisplaySettingsChanged", "Display Settings have changed, resizing the Player window and moving on to the next Layout. W=" + SystemInformation.PrimaryMonitorSize.Width.ToString() + ", H=" + SystemInformation.PrimaryMonitorSize.Height.ToString()), LogType.Info.ToString()); - - // Reassert the size of our client (should resize if necessary) - SetMainWindowSize(); - - // Expire the current layout and move on - _changingLayout = true; - - // Yield and restart - _schedule.NextLayout(); - } - } -} \ No newline at end of file diff --git a/MainForm.resx b/MainForm.resx deleted file mode 100644 index 7f095ede..00000000 --- a/MainForm.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - True - - \ No newline at end of file diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 00000000..7183847b --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs new file mode 100644 index 00000000..4ac20490 --- /dev/null +++ b/MainWindow.xaml.cs @@ -0,0 +1,921 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Collections; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Data; +using System.Xml; +using System.Net; +using System.IO; +using System.Xml.Serialization; +using System.Diagnostics; +using XiboClient.Log; +using System.Threading; +using XiboClient.Properties; +using System.Globalization; +using XiboClient.Logic; +using XiboClient.Error; +using System.Drawing.Imaging; +using System.Windows.Threading; +using XiboClient.Rendering; +using XiboClient.Stats; + +namespace XiboClient +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + /// + /// Schedule Class + /// + private Schedule _schedule; + + /// + /// Overlay Regions + /// + private Collection _overlays; + + /// + /// The Currently Running Layout + /// + private Layout currentLayout; + + /// + /// Some other misc state tracking things that need looking at + /// + private bool _changingLayout = false; + private int _scheduleId; + private int _layoutId; + private bool _screenSaver = false; + private bool _showingSplash = false; + + /// + /// Delegates to Invoke various actions after yielding + /// + /// + private delegate void ChangeToNextLayoutDelegate(string layoutPath); + private delegate void ManageOverlaysDelegate(Collection overlays); + + #region DLL Imports + + [FlagsAttribute] + enum EXECUTION_STATE : uint + { + ES_AWAYMODE_REQUIRED = 0x00000040, + ES_CONTINUOUS = 0x80000000, + ES_DISPLAY_REQUIRED = 0x00000002, + ES_SYSTEM_REQUIRED = 0x00000001 + } + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); + + // Changes the parent window of the specified child window + [DllImport("user32.dll")] + private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); + + // Changes an attribute of the specified window + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + // Retrieves information about the specified window + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + // Retrieves the coordinates of a window's client area + [DllImport("user32.dll")] + private static extern bool GetClientRect(IntPtr hWnd, out Rectangle lpRect); + + [DllImport("User32.dll")] + private static extern bool SetCursorPos(int X, int Y); + + #endregion + + /*public MainWindow(IntPtr previewHandle) + { + InitializeComponent(); + + // Set the preview window of the screen saver selection + // dialog in Windows as the parent of this form. + SetParent(this.Handle, previewHandle); + + // Set this form to a child form, so that when the screen saver selection + // dialog in Windows is closed, this form will also close. + SetWindowLong(this.Handle, -16, new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000)); + + // Set the size of the screen saver to the size of the screen saver + // preview window in the screen saver selection dialog in Windows. + Rectangle ParentRect; + GetClientRect(previewHandle, out ParentRect); + + ApplicationSettings.Default.SizeX = ParentRect.Size.Width; + ApplicationSettings.Default.SizeY = ParentRect.Size.Height; + ApplicationSettings.Default.OffsetX = 0; + ApplicationSettings.Default.OffsetY = 0; + + InitializeScreenSaver(true); + InitializeXibo(); + }*/ + + public MainWindow(bool screenSaver) + { + InitializeComponent(); + + if (screenSaver) + { + InitializeScreenSaver(false); + } + + InitializeXibo(); + } + + public MainWindow() + { + InitializeComponent(); + InitializeXibo(); + } + + private void InitializeXibo() + { + // Set the title + Title = ApplicationSettings.GetProductNameFromAssembly(); + + Thread.CurrentThread.Name = "UI Thread"; + + // Check the directories exist + if (!Directory.Exists(ApplicationSettings.Default.LibraryPath + @"\backgrounds\")) + { + // Will handle the create of everything here + Directory.CreateDirectory(ApplicationSettings.Default.LibraryPath + @"\backgrounds"); + } + + // Default the XmdsConnection + ApplicationSettings.Default.XmdsLastConnection = DateTime.MinValue; + + // Set the Main Window Size + SetMainWindowSize(); + + // Bind to the resize event + Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + + // Show in taskbar + ShowInTaskbar = ApplicationSettings.Default.ShowInTaskbar; + + // Setup the proxy information + OptionsForm.SetGlobalProxy(); + + // Events + Loaded += MainWindow_Loaded; + Closing += MainForm_FormClosing; + ContentRendered += MainForm_Shown; + + // Define the hotkey + /*Keys key; + try + { + key = (Keys)Enum.Parse(typeof(Keys), ApplicationSettings.Default.ClientInformationKeyCode.ToUpper()); + } + catch + { + // Default back to I + key = Keys.I; + } + + KeyStore.Instance.AddKeyDefinition("ClientInfo", key, ((ApplicationSettings.Default.ClientInfomationCtrlKey) ? Keys.Control : Keys.None)); + + // Register a handler for the key event + KeyStore.Instance.KeyPress += Instance_KeyPress;*/ + + // Trace listener for Client Info + ClientInfoTraceListener clientInfoTraceListener = new ClientInfoTraceListener(ClientInfo.Instance); + clientInfoTraceListener.Name = "ClientInfo TraceListener"; + Trace.Listeners.Add(clientInfoTraceListener); + + // Log to disk? + if (!string.IsNullOrEmpty(ApplicationSettings.Default.LogToDiskLocation)) + { + TextWriterTraceListener listener = new TextWriterTraceListener(ApplicationSettings.Default.LogToDiskLocation); + Trace.Listeners.Add(listener); + } + +#if !DEBUG + // Initialise the watchdog + if (!_screenSaver) + { + try + { + // Update/write the status.json file + File.WriteAllText(Path.Combine(ApplicationSettings.Default.LibraryPath, "status.json"), "{\"lastActivity\":\"" + DateTime.Now.ToString() + "\"}"); + + // Start watchdog + WatchDogManager.Start(); + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("MainForm - InitializeXibo", "Cannot start watchdog. E = " + e.Message), LogType.Error.ToString()); + } + } +#endif + + // An empty set of overlays + _overlays = new Collection(); + + // Switch to TLS 2.1 + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + + Trace.WriteLine(new LogMessage("MainForm", "Player Initialised"), LogType.Info.ToString()); + } + + private void InitializeScreenSaver(bool preview) + { + _screenSaver = true; + + // Configure some listeners for the mouse (to quit) + if (!preview) + { + KeyStore.Instance.ScreenSaver = true; + + MouseInterceptor.Instance.MouseEvent += Instance_MouseEvent; + } + } + + void Instance_MouseEvent() + { + Close(); + } + + /// + /// Handle the Key Event + /// + /// + /*void Instance_KeyPress(string name) + { + Debug.WriteLine("KeyPress " + name); + if (name == "ClientInfo") + { + // Toggle + if (_clientInfoForm.Visible) + { + _clientInfoForm.Hide(); +#if !DEBUG + if (!_screenSaver) + TopMost = true; +#endif + } + else + { +#if !DEBUG + if (!_screenSaver) + TopMost = false; +#endif + _clientInfoForm.Show(); + _clientInfoForm.BringToFront(); + } + } + else if (name == "ScreenSaver") + { + Debug.WriteLine("Closing due to ScreenSaver key press"); + if (!_screenSaver) + return; + + Close(); + } + }*/ + + /// + /// main window loding event + /// + /// + /// + private void MainWindow_Loaded(object sender, RoutedEventArgs e) + { + // Is the mouse enabled? + if (!ApplicationSettings.Default.EnableMouse) + { + // Hide the cursor + } + + // Move the cursor to the starting place + if (!_screenSaver) + { + SetCursorStartPosition(); + } + + // Show the splash screen + ShowSplashScreen(); + + // Change the default Proxy class + OptionsForm.SetGlobalProxy(); + + // UserApp data + Debug.WriteLine(new LogMessage("MainForm_Load", "User AppData Path: " + ApplicationSettings.Default.LibraryPath), LogType.Info.ToString()); + } + + /// + /// Called after the form has been shown + /// + /// + /// + void MainForm_Shown(object sender, EventArgs e) + { + try + { + // Create the Schedule + _schedule = new Schedule(ApplicationSettings.Default.LibraryPath + @"\" + ApplicationSettings.Default.ScheduleFile); + + // Bind to the schedule change event - notifys of changes to the schedule + _schedule.ScheduleChangeEvent += ScheduleChangeEvent; + + // Bind to the overlay change event + _schedule.OverlayChangeEvent += ScheduleOverlayChangeEvent; + + // Initialize the other schedule components + _schedule.InitializeComponents(); + + // Set this form to topmost +#if !DEBUG + if (!_screenSaver) + TopMost = true; +#endif + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message, LogType.Error.ToString()); + MessageBox.Show("Fatal Error initialising the application. " + ex.Message, "Fatal Error"); + Close(); + } + } + + /// + /// Called before the form has loaded for the first time + /// + /// + /// + private void MainForm_Load(object sender, EventArgs e) + { + // Is the mouse enabled? + if (!ApplicationSettings.Default.EnableMouse) + { + // Hide the cursor + Mouse.OverrideCursor = Cursors.None; + } + + // Move the cursor to the starting place + if (!_screenSaver) + SetCursorStartPosition(); + + // Show the splash screen + ShowSplashScreen(); + + // Change the default Proxy class + OptionsForm.SetGlobalProxy(); + + // UserApp data + Debug.WriteLine(new LogMessage("MainForm_Load", "User AppData Path: " + ApplicationSettings.Default.LibraryPath), LogType.Info.ToString()); + } + + /// + /// Called as the Main Form starts to close + /// + /// + /// + private void MainForm_FormClosing(object sender, CancelEventArgs e) + { + // We want to tidy up some stuff as this form closes. + Trace.Listeners.Remove("ClientInfo TraceListener"); + + try + { + // Close the client info screen + //if (_clientInfoForm != null) + // _clientInfoForm.Hide(); + + // Stop the schedule object + if (_schedule != null) + _schedule.Stop(); + + // Flush the stats + FlushStats(); + + // Write the CacheManager to disk + CacheManager.Instance.WriteCacheManager(); + } + catch (NullReferenceException) + { + // Stopped before we really started, nothing to do + } + + // Flush the logs + Trace.Flush(); + } + + /// + /// Handles the ScheduleChange event + /// + /// + void ScheduleChangeEvent(string layoutPath, int scheduleId, int layoutId) + { + Trace.WriteLine(new LogMessage("MainForm - ScheduleChangeEvent", string.Format("Schedule Changing to {0}", layoutPath)), LogType.Audit.ToString()); + + // We are changing the layout + _changingLayout = true; + + _scheduleId = scheduleId; + _layoutId = layoutId; + + if (!Dispatcher.CheckAccess()) + { + Dispatcher.BeginInvoke(new ChangeToNextLayoutDelegate(ChangeToNextLayout), layoutPath); + return; + } + + ChangeToNextLayout(layoutPath); + } + + /// + /// Change to the next layout + /// + private void ChangeToNextLayout(string layoutPath) + { + if (ApplicationSettings.Default.PreventSleep) + { + try + { + SetThreadExecutionState(EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + catch + { + Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Unable to set Thread Execution state"), LogType.Info.ToString()); + } + } + + try + { + // Destroy the Current Layout + try + { + currentLayout.Stop(); + + DestroyLayout(); + } + catch (Exception e) + { + // Force collect all controls + this.Scene.Children.Clear(); + + Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Destroy Layout Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); + throw e; + } + + // Prepare the next layout + try + { + currentLayout = PrepareLayout(layoutPath, false); + + // We have loaded a layout background and therefore are no longer showing the splash screen + _showingSplash = false; + + // Add this Layout to our controls + this.Scene.Children.Add(currentLayout); + + ClientInfo.Instance.CurrentLayoutId = layoutPath; + _schedule.CurrentLayoutId = _layoutId; + } + catch (DefaultLayoutException) + { + throw; + } + catch (Exception e) + { + DestroyLayout(); + Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Prepare Layout Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); + throw; + } + + ClientInfo.Instance.ControlCount = this.Scene.Children.Count; + + // Do we need to notify? + try + { + if (ApplicationSettings.Default.SendCurrentLayoutAsStatusUpdate) + { + using (xmds.xmds statusXmds = new xmds.xmds()) + { + statusXmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=notifyStatus"; + statusXmds.NotifyStatusAsync(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, "{\"currentLayoutId\":" + _layoutId + "}"); + } + } + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Notify Status Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); + throw; + } + } + catch (Exception ex) + { + if (!(ex is DefaultLayoutException)) + Trace.WriteLine(new LogMessage("MainForm - ChangeToNextLayout", "Layout Change to " + layoutPath + " failed. Exception raised was: " + ex.Message), LogType.Error.ToString()); + + // Do we have more than one Layout in our schedule? + if (_schedule.ActiveLayouts > 1) + { + _schedule.NextLayout(); + } + else + { + if (!_showingSplash) + { + ShowSplashScreen(); + } + + // In 10 seconds fire the next layout + DispatcherTimer timer = new DispatcherTimer() + { + Interval = new TimeSpan(0, 0, 10) + }; + timer.Tick += new EventHandler(splashScreenTimer_Tick); + timer.Start(); + } + } + + // We have finished changing the layout + _changingLayout = false; + } + + /// + /// Expire the Splash Screen + /// + /// + /// + void splashScreenTimer_Tick(object sender, EventArgs e) + { + Debug.WriteLine(new LogMessage("timer_Tick", "Loading next layout after splashscreen")); + + DispatcherTimer timer = (DispatcherTimer)sender; + timer.Stop(); + + _schedule.NextLayout(); + } + + /// + /// Prepares the Layout.. rendering all the necessary controls + /// + /// + /// + /// + private Layout PrepareLayout(string layoutPath, bool isOverlay) + { + // Default or not + if (layoutPath == ApplicationSettings.Default.LibraryPath + @"\Default.xml" || String.IsNullOrEmpty(layoutPath)) + { + throw new DefaultLayoutException(); + } + else + { + try + { + // Construct a new Current Layout + Layout layout = new Layout(); + layout.Width = Width; + layout.Height = Height; + layout.Schedule = _schedule; + layout.loadFromFile(layoutPath, _layoutId, _scheduleId, isOverlay); + return layout; + } + catch (IOException) + { + CacheManager.Instance.Remove(layoutPath); + + throw new DefaultLayoutException(); + } + } + } + + /// + /// Shows the splash screen (set the background to the embedded resource) + /// + private void ShowSplashScreen() + { + _showingSplash = true; + + if (!string.IsNullOrEmpty(ApplicationSettings.Default.SplashOverride)) + { + try + { + System.Windows.Controls.Image img = new System.Windows.Controls.Image() + { + Name = "Splash" + }; + img.Source = new BitmapImage(new Uri(ApplicationSettings.Default.SplashOverride)); + this.Scene.Children.Add(img); + } + catch + { + Trace.WriteLine(new LogMessage("ShowSplashScreen", "Unable to load user splash screen"), LogType.Error.ToString()); + ShowDefaultSplashScreen(); + } + } + else + { + ShowDefaultSplashScreen(); + } + } + + /// + /// Show the Default Splash Screen + /// + private void ShowDefaultSplashScreen() + { + Uri path = new Uri("pack://application:,,,/Resources/splash.jpg"); + System.Windows.Controls.Image img = new System.Windows.Controls.Image() + { + Name = "Splash" + }; + img.Source = new BitmapImage(path); + this.Scene.Children.Add(img); + } + + /// + /// Disposes Layout - removes the controls + /// + private void DestroyLayout() + { + Debug.WriteLine("Destroying Layout", "MainForm - DestoryLayout"); + + this.currentLayout.Remove(); + } + + /// + /// Set the Cursor start position + /// + private void SetCursorStartPosition() + { + Point position; + + switch (ApplicationSettings.Default.CursorStartPosition) + { + case "Top Left": + position = new Point(0, 0); + break; + + case "Top Right": + position = new Point(Width, 0); + break; + + case "Bottom Left": + position = new Point(0, Height); + break; + + case "Bottom Right": + position = new Point(Width, Height); + break; + + default: + // The default position or "unchanged" as it will be sent, is to not do anything + // leave the cursor where it is + return; + } + + SetCursorPos((int)position.X, (int)position.Y); + } + + /// + /// Force a flush of the stats log + /// + public void FlushStats() + { + try + { + StatLog.Instance.Flush(); + } + catch + { + Trace.WriteLine(new LogMessage("MainForm - FlushStats", "Unable to Flush Stats"), LogType.Error.ToString()); + } + } + + /// + /// Overlay change event. + /// + /// + void ScheduleOverlayChangeEvent(Collection overlays) + { + if (!Dispatcher.CheckAccess()) + { + Dispatcher.BeginInvoke(new ManageOverlaysDelegate(ManageOverlays), overlays); + return; + } + + ManageOverlays(overlays); + } + + /// + /// Manage Overlays + /// + /// + public void ManageOverlays(Collection overlays) + { + try + { + // Parse all overlays and compare what we have now to the overlays we have already created (see OverlayRegions) + Debug.WriteLine("Arrived at Manage Overlays with " + overlays.Count + " overlay schedules to show. We're already showing " + _overlays.Count + " overlay Regions", "Overlays"); + + // Take the ones we currently have up and remove them if they aren't in the new list or if they've been set to refresh + // We use a for loop so that we are able to remove the region from the collection + for (int i = 0; i < _overlays.Count; i++) + { + Debug.WriteLine("Assessing Overlay Region " + i, "Overlays"); + + Layout layout = _overlays[i]; + bool found = false; + bool refresh = false; + + foreach (ScheduleItem item in overlays) + { + if (item.scheduleid == layout.ScheduleId) + { + found = true; + refresh = item.Refresh; + break; + } + } + + if (!found || refresh) + { + if (refresh) + { + Trace.WriteLine(new LogMessage("MainForm - ManageOverlays", "Refreshing item that has changed."), LogType.Info.ToString()); + } + Debug.WriteLine("Removing overlay " + i + " which is no-longer required. Overlay: " + layout.ScheduleId, "Overlays"); + + // Remove the Layout from the overlays collection + _overlays.Remove(layout); + + // As we've removed the thing we're iterating over, reduce i + i--; + + // Clear down and dispose of the region. + layout.Stop(); + layout.Remove(); + + this.OverlayScene.Children.Remove(layout); + } + else + { + Debug.WriteLine("Overlay Layout found and not needing refresh " + i, "Overlays"); + } + } + + // Take the ones that are in the new list and add them + foreach (ScheduleItem item in overlays) + { + // Check its not already added. + bool found = false; + foreach (Layout layout in _overlays) + { + if (layout.ScheduleId == item.scheduleid) + { + found = true; + break; + } + } + + if (found) + { + Debug.WriteLine("Layout already found for overlay - we're assuming here that if we've found one, they are all there.", "Overlays"); + continue; + } + + // Reset refresh + item.Refresh = false; + + // Parse the layout for regions, and create them. + try + { + Layout layout = PrepareLayout(item.layoutFile, true); + + // Add to our collection of Overlays + _overlays.Add(layout); + + // Add to the Scene + OverlayScene.Children.Add(layout); + } + catch (DefaultLayoutException) + { + // Unable to prepare this layout - log and move on + Trace.WriteLine(new LogMessage("MainForm - ManageOverlays", "Unable to Prepare Layout: " + item.layoutFile), LogType.Audit.ToString()); + } + } + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("MainForm - _schedule_OverlayChangeEvent", "Unknown issue managing overlays. Ex = " + e.Message), LogType.Info.ToString()); + } + } + + /// + /// Set the Main Window Size, either to the primary monitor, or the configured size + /// + private void SetMainWindowSize() + { + Debug.WriteLine("SetMainWindowSize: IN"); + + // Override the default size if necessary + if (ApplicationSettings.Default.SizeX != 0 || ApplicationSettings.Default.SizeY != 0) + { + Debug.WriteLine("SetMainWindowSize: Use Settings Size"); + + // Determine the client size + double sizeX = (double)ApplicationSettings.Default.SizeX; + if (sizeX <= 0) + { + sizeX = SystemParameters.PrimaryScreenWidth; + } + + double sizeY = (int)ApplicationSettings.Default.SizeY; + if (sizeY <= 0) + { + sizeY = SystemParameters.PrimaryScreenHeight; + } + + Width = sizeX; + Height = sizeY; + Top = (double)ApplicationSettings.Default.OffsetX; + Left = (double)ApplicationSettings.Default.OffsetY; + } + else + { + Debug.WriteLine("SetMainWindowSize: Use Monitor Size"); + + // Use the primary monitor size + Width = SystemParameters.PrimaryScreenWidth; + Height = SystemParameters.PrimaryScreenHeight; + } + + Debug.WriteLine("SetMainWindowSize: Setting Size to " + Width + "x" + Height); + + // Use the client size we've calculated to set the actual size of the form + WindowState = WindowState.Normal; + + Debug.WriteLine("SetMainWindowSize: OUT"); + } + + /// + /// Display Settings Changed + /// + /// + /// + private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) + { + Trace.WriteLine(new LogMessage("SystemEvents_DisplaySettingsChanged", + "Display Settings have changed, resizing the Player window and moving on to the next Layout. W=" + + SystemParameters.PrimaryScreenWidth.ToString() + ", H=" + + SystemParameters.PrimaryScreenHeight.ToString()), LogType.Info.ToString()); + + // Reassert the size of our client (should resize if necessary) + SetMainWindowSize(); + + // Expire the current layout and move on + _changingLayout = true; + + // Yield and restart + _schedule.NextLayout(); + } + } +} diff --git a/Media/Audio.cs b/Media/Audio.cs deleted file mode 100644 index d002dc5d..00000000 --- a/Media/Audio.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; - -namespace XiboClient -{ - class Audio : Media - { - private string _filePath; - private VideoPlayer _videoPlayer; - private int _duration; - private bool _expired = false; - private bool _detectEnd = false; - - /// - /// Constructor - /// - /// - public Audio(RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - _filePath = Uri.UnescapeDataString(options.uri).Replace('+',' '); - _duration = options.duration; - - _videoPlayer = new VideoPlayer(); - _videoPlayer.Width = 0; - _videoPlayer.Height = 0; - _videoPlayer.Location = new System.Drawing.Point(0, 0); - _videoPlayer.SetVisible(false); - - // Should we loop? - _videoPlayer.SetLooping((options.Dictionary.Get("loop", "0") == "1" && _duration != 0)); - - // Should we mute? - _videoPlayer.SetVolume(options.Dictionary.Get("volume", 100)); - - // Capture any video errors - _videoPlayer.VideoError += new VideoPlayer.VideoErrored(_videoPlayer_VideoError); - _videoPlayer.VideoEnd += new VideoPlayer.VideoFinished(_videoPlayer_VideoEnd); - - Controls.Add(_videoPlayer); - } - - public override void RenderMedia() - { - // Check to see if the video exists or not (if it doesnt say we are already expired) - if (!File.Exists(_filePath)) - { - Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Local Video file " + _filePath + " not found.")); - throw new FileNotFoundException(); - } - - // Do we need to determine the end time ourselves? - if (_duration == 0) - { - // Set the duration to 1 second - Duration = 1; - _detectEnd = true; - } - - // Render media as normal (starts the timer, shows the form, etc) - base.RenderMedia(); - - try - { - // Start Player - _videoPlayer.StartPlayer(_filePath); - - // Show the player - _videoPlayer.Show(); - - Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Video Started"), LogType.Audit.ToString()); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Audio - RenderMedia", ex.Message), LogType.Error.ToString()); - - // Unable to start video - expire this media immediately - throw; - } - } - - void _videoPlayer_VideoError() - { - // Immediately hide the player - _videoPlayer.Hide(); - - _expired = true; - } - - /// - /// Video End event - /// - void _videoPlayer_VideoEnd() - { - // Has the video finished playing - if (_videoPlayer.FinishedPlaying) - { - Trace.WriteLine(new LogMessage("Audio - _videoPlayer_VideoEnd", "End of video detected"), LogType.Audit.ToString()); - - // Immediately hide the player - _videoPlayer.Hide(); - - // Set to expired - _expired = true; - } - } - - /// - /// Override the timer tick - /// - /// - /// - protected override void timer_Tick(object sender, EventArgs e) - { - if (!_detectEnd || _expired) - base.timer_Tick(sender, e); - } - - protected override void Dispose(bool disposing) - { - try - { - // Remove the event handlers - _videoPlayer.VideoError -= _videoPlayer_VideoError; - _videoPlayer.VideoEnd -= _videoPlayer_VideoEnd; - - // Stop and Clear - _videoPlayer.StopAndClear(); - - // Remove the control - Controls.Remove(_videoPlayer); - - // Dispose of the Control - _videoPlayer.Dispose(); - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("Audio - Dispose", "Problem disposing of the Video Player. Ex = " + e.Message), LogType.Audit.ToString()); - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/EdgeWebMedia.cs b/Media/EdgeWebMedia.cs deleted file mode 100644 index 3d9f9843..00000000 --- a/Media/EdgeWebMedia.cs +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright (C) 2019 Xibo Signage Ltd - * - * Xibo - Digital Signage - http://www.xibo.org.uk - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using Microsoft.Toolkit.Forms.UI.Controls; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace XiboClient -{ - class EdgeWebMedia : WebMedia - { - private bool _disposed; - - private WebView mWebView; - - public EdgeWebMedia(RegionOptions options) - : base(options) - { - } - - /// - /// Render Media - /// - public override void RenderMedia() - { - // Create the web view we will use - mWebView = new WebView(); - - ((ISupportInitialize)mWebView).BeginInit(); - - mWebView.Dock = System.Windows.Forms.DockStyle.Fill; - mWebView.Size = Size; - mWebView.Visible = false; - mWebView.IsPrivateNetworkClientServerCapabilityEnabled = true; - mWebView.NavigationCompleted += MWebView_NavigationCompleted; - - Controls.Add(mWebView); - - ((ISupportInitialize)mWebView).EndInit(); - - // _webBrowser.ScrollBarsEnabled = false; - // _webBrowser.ScriptErrorsSuppressed = true; - - HtmlUpdatedEvent += IeWebMedia_HtmlUpdatedEvent; - - if (IsNativeOpen()) - { - // Navigate directly - mWebView.Navigate(_filePath); - } - else if (HtmlReady()) - { - // Write to temporary file - ReadControlMeta(); - - // Navigate to temp file - mWebView.Navigate(_localWebPath); - } - else - { - Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); - } - - // Render media shows the controls and starts timers, etc - base.RenderMedia(); - } - - private void MWebView_NavigationCompleted(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlNavigationCompletedEventArgs e) - { - - Debug.WriteLine("Navigate Completed to " + e.Uri + " " + e.WebErrorStatus.ToString(), "EdgeWebView"); - - if (e.IsSuccess) - { - DocumentCompleted(); - - if (!IsDisposed) - { - // Show the browser - mWebView.Visible = true; - } - } - else - { - Trace.WriteLine(new LogMessage("EdgeWebMedia", "Cannot navigate to " + e.Uri + ". e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); - - // This should exipre the media - Duration = 5; - base.RenderMedia(); - } - } - - private void IeWebMedia_HtmlUpdatedEvent(string url) - { - if (mWebView != null) - { - mWebView.Navigate(url); - } - } - - /// - /// Dispose of this text item - /// - /// - protected override void Dispose(bool disposing) - { - Debug.WriteLine("Disposing of " + _filePath, "IeWebMedia - Dispose"); - - if (disposing) - { - // Remove the webbrowser control - try - { - // Remove the web browser control - Controls.Remove(mWebView); - - // Workaround to remove COM object - PerformLayout(); - - // Detatch event and remove - if (mWebView != null && !_disposed) - { - mWebView.NavigationCompleted -= MWebView_NavigationCompleted; - mWebView.Dispose(); - - _disposed = true; - } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("IeWebMedia - Dispose", "Cannot dispose of web browser. E = " + e.Message), LogType.Info.ToString()); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/Flash.cs b/Media/Flash.cs deleted file mode 100644 index 5329b423..00000000 --- a/Media/Flash.cs +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2011 Daniel Garner and James Packer - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Text; -using System.Windows.Forms; -using System.Diagnostics; - -namespace XiboClient -{ - class Flash - : Media - { - private TemporaryHtml _tempHtml; - private WebBrowser _webBrowser; - private string _backgroundImage; - private string _backgroundColor; - private string _backgroundTop; - private string _backgroundLeft; - private bool _disposed = false; - - public Flash (RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - _tempHtml = new TemporaryHtml(); - - _backgroundImage = options.backgroundImage; - _backgroundColor = options.backgroundColor; - _backgroundTop = options.backgroundTop + "px"; - _backgroundLeft = options.backgroundLeft + "px"; - - // Create the HEAD of the document - GenerateHeadHtml(); - - // Set the body - string html = @" - - - - - - - - - "; - - _tempHtml.BodyContent = string.Format(html, options.uri, options.uri, options.width.ToString(), options.height.ToString()); - - // Fire up a webBrowser control to display the completed file. - _webBrowser = new WebBrowser(); - _webBrowser.Size = this.Size; - _webBrowser.ScrollBarsEnabled = false; - _webBrowser.ScriptErrorsSuppressed = true; - _webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(_webBrowser_DocumentCompleted); - - // Navigate to temp file - _webBrowser.Navigate(_tempHtml.Path); - Controls.Add(_webBrowser); - - // Show the control - Show(); - } - - /// - /// Generates the Head Html for this Document - /// - private void GenerateHeadHtml() - { - // Handle the background - String bodyStyle; - - if (_backgroundImage == null || _backgroundImage == "") - { - bodyStyle = "background-color:" + _backgroundColor + " ;"; - } - else - { - string bgImage = ApplicationSettings.Default.LibraryPath + @"/" + _backgroundImage; - bodyStyle = "background-image: url('" + _backgroundImage + "'); background-attachment:fixed; background-color:" + _backgroundColor + " background-repeat: no-repeat; background-position: " + _backgroundLeft + " " + _backgroundTop + ";"; - } - - // Store the document text in the temporary HTML space - _tempHtml.HeadContent = ""; ; - } - - /// - /// Web browser completed event - /// - /// - /// - void _webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) - { - base.StartTimer(); - - if (_disposed) - return; - - _webBrowser.Visible = true; - } - - /// - /// Dispose - /// - /// - protected override void Dispose(bool disposing) - { - _disposed = true; - if (disposing) - { - // Remove the webbrowser control - try - { - _webBrowser.Dispose(); - } - catch - { - System.Diagnostics.Trace.WriteLine(new LogMessage("WebBrowser still in use.", String.Format("Dispose"))); - } - - // Remove the temporary file we created - try - { - _tempHtml.Dispose(); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Dispose", String.Format("Unable to dispose TemporaryHtml with exception {0}", ex.Message))); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/IeWebMedia.cs b/Media/IeWebMedia.cs deleted file mode 100644 index c1bb705f..00000000 --- a/Media/IeWebMedia.cs +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (C) 2019 Xibo Signage Ltd - * - * Xibo - Digital Signage - http://www.xibo.org.uk - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace XiboClient -{ - class IeWebMedia : WebMedia - { - private bool _disposed; - - private WebBrowser _webBrowser; - - public IeWebMedia(RegionOptions options) - : base(options) - { - } - - /// - /// Render Media - /// - public override void RenderMedia() - { - // Create the web view we will use - _webBrowser = new WebBrowser(); - _webBrowser.DocumentCompleted += _webBrowser_DocumentCompleted; - _webBrowser.Size = Size; - _webBrowser.ScrollBarsEnabled = false; - _webBrowser.ScriptErrorsSuppressed = true; - _webBrowser.Visible = false; - - HtmlUpdatedEvent += IeWebMedia_HtmlUpdatedEvent; - - if (IsNativeOpen()) - { - // Navigate directly - _webBrowser.Navigate(_filePath); - } - else if (HtmlReady()) - { - // Write to temporary file - ReadControlMeta(); - - // Navigate to temp file - _webBrowser.Navigate(_localWebPath); - } - else - { - Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); - } - - Controls.Add(_webBrowser); - - // Render media shows the controls and starts timers, etc - base.RenderMedia(); - } - - /// - /// Web Browser finished loading document - /// - /// - /// - private void _webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) - { - DocumentCompleted(); - - if (!IsDisposed) - { - // Show the browser - _webBrowser.Visible = true; - } - } - - private void IeWebMedia_HtmlUpdatedEvent(string url) - { - if (_webBrowser != null) - { - _webBrowser.Navigate(url); - } - } - - /// - /// Dispose of this text item - /// - /// - protected override void Dispose(bool disposing) - { - Debug.WriteLine("Disposing of " + _filePath, "IeWebMedia - Dispose"); - - if (disposing) - { - // Remove the webbrowser control - try - { - // Remove the web browser control - Controls.Remove(_webBrowser); - - // Workaround to remove COM object - PerformLayout(); - - // Detatch event and remove - if (_webBrowser != null && !_disposed) - { - _webBrowser.DocumentCompleted -= _webBrowser_DocumentCompleted; - _webBrowser.Navigate("about:blank"); - _webBrowser.Dispose(); - - _disposed = true; - } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("IeWebMedia - Dispose", "Cannot dispose of web browser. E = " + e.Message), LogType.Info.ToString()); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/Image.cs b/Media/Image.cs deleted file mode 100644 index 11b8d303..00000000 --- a/Media/Image.cs +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2014 Daniel Garner and James Packer - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Text; -using System.Windows.Forms; - -namespace XiboClient -{ - class ImagePosition : Media - { - private string _filePath; - PictureBox _pictureBox; - RegionOptions _options; - - public ImagePosition(RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - _options = options; - _filePath = options.uri; - - if (!System.IO.File.Exists(_filePath)) - { - // Exit - System.Diagnostics.Trace.WriteLine(new LogMessage("Image - Dispose", "Cannot Create image object. Invalid Filepath."), LogType.Error.ToString()); - return; - } - - try - { - _pictureBox = new PictureBox(); - _pictureBox.Size = new Size(_width, _height); - _pictureBox.Location = new Point(0, 0); - _pictureBox.BorderStyle = BorderStyle.None; - _pictureBox.BackColor = Color.Transparent; - - // Do we need to align the image in any way? - if (options.Dictionary.Get("scaleType", "stretch") == "center" && (options.Dictionary.Get("align", "center") != "center" || options.Dictionary.Get("valign", "middle") != "middle")) - { - // Yes we do, so we must override the paint method - _pictureBox.Paint += _pictureBox_Paint; - } - else - { - // No we don't so use a normal picture box. - _pictureBox.SizeMode = (options.Dictionary.Get("scaleType", "center") == "stretch") ? PictureBoxSizeMode.StretchImage : PictureBoxSizeMode.Zoom; - _pictureBox.Image = new Bitmap(_filePath); - } - - Controls.Add(this._pictureBox); - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine(new LogMessage("ImagePosition", String.Format("Cannot create Image Object with exception: {0}", ex.Message)), LogType.Error.ToString()); - } - } - - void _pictureBox_Paint(object sender, PaintEventArgs e) - { - string align = _options.Dictionary.Get("align", "center"); - string valign = _options.Dictionary.Get("valign", "middle"); - - Image image = Image.FromFile(_filePath); - - // Get our image - Graphics graphics = e.Graphics; - graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - - // Calculate the width and height required - double imageProportion = (double)image.Width / (double)image.Height; - double regionProportion = _pictureBox.Width / _pictureBox.Height; - - int x = 0; - int y = 0; - int width = _pictureBox.Width; - int height = _pictureBox.Height; - - if (imageProportion > regionProportion) - { - // Use the full width possible and adjust the height accordingly - height = (int)(_pictureBox.Width / imageProportion); - - if (valign == "middle") - { - // top margin needs to drop down half - x = x + ((_pictureBox.Height - height) / 2); - } - else if (valign == "bottom") { - x = x + (_pictureBox.Height - height); - } - } - else - { - // Use the full height possible and adjust the width accordingly - width = (int)(imageProportion * _pictureBox.Height); - - if (align == "center") - { - y = y + ((_pictureBox.Width - width) / 2); - } - else if (align == "right") - { - y = y + (_pictureBox.Width - width); - } - } - - graphics.DrawImage(image, - y, - x, - width, - height); - } - - public override void RenderMedia() - { - base.RenderMedia(); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - try - { - Controls.Remove(_pictureBox); - - if (_pictureBox.Image != null) - _pictureBox.Image.Dispose(); - - _pictureBox.Dispose(); - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine(new LogMessage("Image - Dispose", String.Format("Cannot dispose Image Object with exception: {0}", ex.Message)), LogType.Error.ToString()); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/Media.cs b/Media/Media.cs deleted file mode 100644 index 1c5ff668..00000000 --- a/Media/Media.cs +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2012 Daniel Garner - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Text; -using System.Windows.Forms; -using XiboClient.Properties; -using System.Management; -using System.Diagnostics; -using System.Drawing; - -namespace XiboClient -{ - class Media : Form - { - // Events - public delegate void DurationElapsedDelegate(int filesPlayed); - public event DurationElapsedDelegate DurationElapsedEvent; - protected int _filesPlayed = 1; - - /// - /// Gets or Sets the duration of this media. Will be 0 if "" - /// - public int Duration - { - get - { - return _duration; - } - set - { - _duration = value; - } - } - private int _duration; - - // Variables for size and position - public int _width; - public int _height; - public int _top; - public int _left; - - /// - /// Has this media exipred? - /// - public bool Expired - { - get - { - return _hasExpired; - } - set - { - _hasExpired = value; - } - } - private bool _hasExpired = false; - - // Private Timer - protected Timer _timer; - private bool _timerStarted = false; - - /// - /// Refresh Rate - /// - /// - public static int PrimaryScreenRefreshRate - { - get - { - if (_refreshRate == 0) - { - - try - { - ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from win32_videocontroller"); - - foreach (ManagementObject videocontroller in searcher.Get()) - { - uint rate = (uint)videocontroller["CurrentRefreshRate"]; - - if (rate != 0) - { - Debug.WriteLine("Screen RefreshRate: " + rate); - - _refreshRate = (int)rate; - - if (_refreshRate < 0 || _refreshRate > 500) - { - Debug.WriteLine("Invalid!"); - _refreshRate = 60; - } - - break; - } - } - } - catch (Exception ex) - { - _refreshRate = 60; - Debug.WriteLine("Unable to get screen refresh rate: " + ex); - } - } - return _refreshRate; - } - } - private static int _refreshRate; - - /// - /// Media Object - /// - /// - /// - /// - /// - public Media(int width, int height, int top, int left) - { - Hide(); - - _width = width; - _height = height; - _top = top; - _left = left; - - // Form properties - TopLevel = false; - FormBorderStyle = FormBorderStyle.None; - - Width = width; - Height = height; - Location = new System.Drawing.Point(0, 0); - - // Transparency - SetStyle(ControlStyles.SupportsTransparentBackColor, true); - BackColor = System.Drawing.Color.Transparent; - TransparencyKey = System.Drawing.Color.White; - - // Only enable double buffering if set in options - if (ApplicationSettings.Default.DoubleBuffering) - { - SetStyle(ControlStyles.OptimizedDoubleBuffer, true); - SetStyle(ControlStyles.AllPaintingInWmPaint, true); - } - } - - /// - /// Start the Timer for this Media - /// - protected void StartTimer() - { - //start the timer - if (!_timerStarted && _duration != 0) - { - _timer = new Timer(); - _timer.Interval = (1000 * _duration); - _timer.Start(); - - _timer.Tick += new EventHandler(timer_Tick); - - _timerStarted = true; - } - } - - /// - /// Reset the timer and start again - /// - protected void RestartTimer() - { - if (_timerStarted) - { - _timer.Stop(); - _timer.Start(); - } - else - { - StartTimer(); - } - } - - /// - /// Render Media call - /// - public virtual void RenderMedia() - { - // Start the timer for this media - StartTimer(); - - // Show the form - Show(); - } - - /// - /// Timer Tick - /// - /// - /// - protected virtual void timer_Tick(object sender, EventArgs e) - { - // Once it has expired we might as well stop the timer? - _timer.Stop(); - - // Signal that this Media Item's duration has elapsed - SignalElapsedEvent(); - } - - /// - /// Signals that an event is elapsed - /// Will raise a DurationElapsedEvent - /// - public void SignalElapsedEvent() - { - _hasExpired = true; - - Trace.WriteLine(new LogMessage("Media - SignalElapsedEvent", "Media Complete"), LogType.Audit.ToString()); - - if (DurationElapsedEvent != null) - DurationElapsedEvent(_filesPlayed); - } - - /// - /// Dispose of this media - /// - /// - protected override void Dispose(bool disposing) - { - try - { - // Dispose of the Timer - if (_timer != null) - _timer.Dispose(); - } - catch (Exception ex) - { - // Some things dont have a timer - Debug.WriteLine(ex.Message); - } - - base.Dispose(disposing); - } - - /// - /// Is a region size change required - /// - /// - public virtual bool RegionSizeChangeRequired() - { - return false; - } - - /// - /// Get Region Size - /// - /// - public virtual Size GetRegionSize() - { - return new Size(_width, _height); - } - - /// - /// Get Region Location - /// - /// - public virtual Point GetRegionLocation() - { - return new Point(_top, _left); - } - } -} diff --git a/Media/PowerPoint.cs b/Media/PowerPoint.cs deleted file mode 100644 index 654eea95..00000000 --- a/Media/PowerPoint.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2014 Spring Signage Ltd - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Text; -using System.Windows.Forms; -using System.Diagnostics; -using System.Globalization; -using System.IO; - -namespace XiboClient -{ - class PowerPoint : Media - { - int scheduleId; - int layoutId; - string mediaId; - string type; - - string _filePath; - WebBrowser webBrowser; - int duration; - - public PowerPoint(RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - duration = options.duration; - scheduleId = options.scheduleId; - layoutId = options.layoutId; - mediaId = options.mediaid; - type = options.type; - _filePath = Uri.UnescapeDataString(options.uri).Replace('+', ' '); - - webBrowser = new WebBrowser(); - webBrowser.Size = this.Size; - webBrowser.ScrollBarsEnabled = false; - webBrowser.ScriptErrorsSuppressed = true; - webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(webBrowser_DocumentCompleted); - webBrowser.Visible = false; - - if (!ApplicationSettings.Default.PowerpointEnabled) - { - webBrowser.DocumentText = "

Powerpoint not enabled on this display

"; - - Trace.WriteLine(String.Format("[*]ScheduleID:{1},LayoutID:{2},MediaID:{3},Message:{0}", "Powerpoint is not enabled on this display", scheduleId, layoutId, mediaId)); - } - else - { - try - { - webBrowser.Navigate(_filePath); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("WebContent", "Unable to show webpage. Exception: " + ex.Message, scheduleId, layoutId), LogType.Error.ToString()); - throw new InvalidOperationException("Cannot navigate to PowerPoint file"); - } - } - - Controls.Add(webBrowser); - Show(); - } - - void webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) - { - // Get ready to show the control - webBrowser.Visible = true; - } - - protected override void Dispose(bool disposing) - { - try - { - Controls.Remove(webBrowser); - webBrowser.Dispose(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(String.Format("Unable to dispose {0} because {1}", _filePath, ex.Message)); - } - - base.Dispose(disposing); - } - } -} \ No newline at end of file diff --git a/Media/ShellCommand.cs b/Media/ShellCommand.cs deleted file mode 100644 index 158b8402..00000000 --- a/Media/ShellCommand.cs +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2012-16 Daniel Garner - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.Text; -using System.Diagnostics; -using XiboClient.Properties; -using XiboClient.Logic; - -namespace XiboClient -{ - class ShellCommand : Media - { - string _command = ""; - string _code = ""; - bool _launchThroughCmd = true; - bool _terminateCommand = false; - bool _useTaskKill = false; - int _processId; - - public ShellCommand(RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - _command = Uri.UnescapeDataString(options.Dictionary.Get("windowsCommand")).Replace('+', ' '); - _code = options.Dictionary.Get("commandCode"); - - // Default to launching through CMS for backwards compatiblity - _launchThroughCmd = (options.Dictionary.Get("launchThroughCmd", "1") == "1"); - - // Termination - _terminateCommand = (options.Dictionary.Get("terminateCommand") == "1"); - _useTaskKill = (options.Dictionary.Get("useTaskkill") == "1"); - } - - public override void RenderMedia() - { - if (!string.IsNullOrEmpty(_code)) - { - // Stored command - bool success; - - try - { - Command command = Command.GetByCode(_code); - success = command.Run(); - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("ScheduleManager - Run", "Cannot run Command: " + e.Message), LogType.Error.ToString()); - success = false; - } - - // Notify the state of the command (success or failure) - using (xmds.xmds statusXmds = new xmds.xmds()) - { - statusXmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=notifyStatus"; - statusXmds.NotifyStatusAsync(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, "{\"lastCommandSuccess\":" + success + "}"); - } - } - else - { - // shell command - - // Is this module enabled? - if (ApplicationSettings.Default.EnableShellCommands) - { - // Check to see if we have an allow list - if (!string.IsNullOrEmpty(ApplicationSettings.Default.ShellCommandAllowList)) - { - // Array of allowed commands - string[] allowedCommands = ApplicationSettings.Default.ShellCommandAllowList.Split(','); - - // Check we are allowed to execute the command - bool found = false; - - foreach (string allowedCommand in allowedCommands) - { - if (_command.StartsWith(allowedCommand)) - { - found = true; - ExecuteShellCommand(); - break; - } - } - - if (!found) - Trace.WriteLine(new LogMessage("ShellCommand - RenderMedia", "Shell Commands not in allow list: " + ApplicationSettings.Default.ShellCommandAllowList), LogType.Error.ToString()); - } - else - { - // All commands are allowed - ExecuteShellCommand(); - } - } - else - { - Trace.WriteLine(new LogMessage("ShellCommand - RenderMedia", "Shell Commands are disabled"), LogType.Error.ToString()); - } - } - - // All shell commands have a duration of 1 - base.RenderMedia(); - } - - /// - /// Execute the shell command - /// - private void ExecuteShellCommand() - { - Trace.WriteLine(new LogMessage("ShellCommand - ExecuteShellCommand", _command), LogType.Info.ToString()); - - // Execute the commend - if (!string.IsNullOrEmpty(_command)) - { - using (Process process = new Process()) - { - ProcessStartInfo startInfo = new ProcessStartInfo(); - - if (_launchThroughCmd) - { - startInfo.WindowStyle = ProcessWindowStyle.Hidden; - startInfo.FileName = "cmd.exe"; - startInfo.Arguments = "/C " + _command; - } - else - { - // Split the command into a command string and arguments. - string[] splitCommand = _command.Split(new[] { ' ' }, 2); - startInfo.FileName = splitCommand[0]; - - if (splitCommand.Length > 1) - startInfo.Arguments = splitCommand[1]; - } - - process.StartInfo = startInfo; - process.Start(); - - // Grab the ID - _processId = process.Id; - } - } - } - - /// - /// Terminates the shell command - /// - private void TerminateCommand() - { - Trace.WriteLine(new LogMessage("ShellCommand - TerminateCommand", _command), LogType.Info.ToString()); - - if (_processId == 0) - { - Trace.WriteLine(new LogMessage("ShellCommand - TerminateCommand", "ProcessID empty for command: " + _command), LogType.Error.ToString()); - return; - } - - if (_useTaskKill) - { - using (Process process = new Process()) - { - ProcessStartInfo startInfo = new ProcessStartInfo(); - - startInfo.WindowStyle = ProcessWindowStyle.Hidden; - startInfo.FileName = "taskkill.exe"; - startInfo.Arguments = "/pid " + _processId.ToString(); - - process.StartInfo = startInfo; - process.Start(); - } - } - else - { - using (Process process = Process.GetProcessById(_processId)) - { - process.Kill(); - } - } - } - - /// - /// Dispose of this text item - /// - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - // Remove the webbrowser control - try - { - // Terminate the command (only if we've been asked to!) - if (_terminateCommand) - { - TerminateCommand(); - } - } - catch - { - Debug.WriteLine(new LogMessage("Unable to terminate command", "Dispose")); - } - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/TemporaryHtml.cs b/Media/TemporaryHtml.cs deleted file mode 100644 index 25db2000..00000000 --- a/Media/TemporaryHtml.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Reflection; -using System.Diagnostics; - -namespace XiboClient -{ - /// - /// A temporary html object. - /// Once FileContent is set it will contain the complete HTML page - /// - class TemporaryHtml : IDisposable - { - private String _fileContent; - private String _headContent; - private String _resourceTemplate; - private String _filePath; - - public TemporaryHtml() - { - // Load the resource file - - - } - - /// - /// The File content - can only be set once. - /// - public String BodyContent - { - set - { - // Set the contents of the file - _fileContent = value; - - // Create the temporary file - Store(); - } - } - - /// - /// The Head content - must be added before the body - /// - public String HeadContent - { - set - { - // Set the head content - _headContent = value; - } - } - - public String Path - { - get - { - return _filePath; - } - } - - /// - /// Stores the file - /// - private void Store() - { - // Create a temporary file - _filePath = System.IO.Path.GetTempFileName(); - - Debug.WriteLine(_filePath); - - // Open the resource file - Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); - - // Use the assembly to get the HtmlTemplate resource - using (Stream resourceStream = assembly.GetManifestResourceStream("XiboClient.Resources.HtmlTemplate.htm")) - { - using (TextReader tw = new StreamReader(resourceStream)) - { - _resourceTemplate = tw.ReadToEnd(); - } - } - - // Insert the file content into the resource file - _resourceTemplate = _resourceTemplate.Replace("", _headContent); - _resourceTemplate = _resourceTemplate.Replace("", _fileContent); - - // Write it to the file - using (FileStream fileStream = File.Open(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - using (StreamWriter sw = new StreamWriter(fileStream, Encoding.UTF8)) - { - sw.Write(_resourceTemplate); - sw.Close(); - } - } - } - - - #region IDisposable Members - - public void Dispose() - { - // Remove the temporary file - File.Delete(_filePath); - } - - #endregion - } -} diff --git a/Media/Video.cs b/Media/Video.cs deleted file mode 100644 index 55fc4b69..00000000 --- a/Media/Video.cs +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Copyright (C) 2019 Xibo Signage Ltd - * - * Xibo - Digital Signage - http://www.xibo.org.uk - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text; -using System.Windows.Forms; -using System.Diagnostics; -using System.IO; - -/// 09/06/12 Dan Changed to raise an event when the video is finished -/// 03/11/12 Dan Fix for non zero duration timers. - -namespace XiboClient -{ - class Video : Media - { - private string _filePath; - private VideoPlayer _videoPlayer; - private int _duration; - private bool _expired = false; - private bool _detectEnd = false; - private RegionOptions _options; - - /// - /// Constructor - /// - /// - public Video(RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - _options = options; - _filePath = Uri.UnescapeDataString(options.uri).Replace('+',' '); - _duration = options.duration; - - _videoPlayer = new VideoPlayer(); - - // Should this video be full screen? - if (options.Dictionary.Get("showFullScreen", "0") == "1") - { - Width = options.LayoutSize.Width; - Height = options.LayoutSize.Height; - _videoPlayer.Width = options.LayoutSize.Width; - _videoPlayer.Height = options.LayoutSize.Height; - } - else - { - _videoPlayer.Width = options.width; - _videoPlayer.Height = options.height; - } - - // Assert the location after setting the control size - _videoPlayer.Location = new System.Drawing.Point(0, 0); - - // Should we loop? - _videoPlayer.SetLooping((options.Dictionary.Get("loop", "0") == "1" && _duration != 0)); - - // Should we mute? - _videoPlayer.SetMute((options.Dictionary.Get("mute", "0") == "1")); - - // Capture any video errors - _videoPlayer.VideoError += new VideoPlayer.VideoErrored(_videoPlayer_VideoError); - _videoPlayer.VideoEnd += new VideoPlayer.VideoFinished(_videoPlayer_VideoEnd); - - Controls.Add(_videoPlayer); - } - - public override void RenderMedia() - { - // Check to see if the video exists or not (if it doesnt say we are already expired) - if (!File.Exists(_filePath)) - { - Trace.WriteLine(new LogMessage("Video - RenderMedia", "Local Video file " + _filePath + " not found.")); - throw new FileNotFoundException(); - } - - // Do we need to determine the end time ourselves? - if (_duration == 0) - { - // Set the duration to 1 second - Duration = 1; - _detectEnd = true; - } - - // Render media as normal (starts the timer, shows the form, etc) - base.RenderMedia(); - - try - { - // Start Player - _videoPlayer.StartPlayer(_filePath); - - // Show the player - _videoPlayer.Show(); - - Trace.WriteLine(new LogMessage("Video - RenderMedia", "Video Started"), LogType.Audit.ToString()); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Video - RenderMedia", ex.Message), LogType.Error.ToString()); - - // Unable to start video - expire this media immediately - throw; - } - } - - void _videoPlayer_VideoError() - { - // Immediately hide the player - _videoPlayer.Hide(); - - _expired = true; - } - - /// - /// Video End event - /// - void _videoPlayer_VideoEnd() - { - // Has the video finished playing - if (_videoPlayer.FinishedPlaying) - { - Trace.WriteLine(new LogMessage("Video - _videoPlayer_VideoEnd", "End of video detected"), LogType.Audit.ToString()); - - // Immediately hide the player - _videoPlayer.Hide(); - - // Set to expired - _expired = true; - } - } - - /// - /// Override the timer tick - /// - /// - /// - protected override void timer_Tick(object sender, EventArgs e) - { - if (!_detectEnd || _expired) - base.timer_Tick(sender, e); - } - - protected override void Dispose(bool disposing) - { - try - { - // Remove the event handlers - _videoPlayer.VideoError -= _videoPlayer_VideoError; - _videoPlayer.VideoEnd -= _videoPlayer_VideoEnd; - - // Stop and Clear - _videoPlayer.StopAndClear(); - - // Remove the control - Controls.Remove(_videoPlayer); - - // Dispose of the Control - _videoPlayer.Dispose(); - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("Video - Dispose", "Problem disposing of the Video Player. Ex = " + e.Message), LogType.Audit.ToString()); - } - - base.Dispose(disposing); - } - - /// - /// Is a region size change required - /// - /// - public override bool RegionSizeChangeRequired() - { - return (_options.Dictionary.Get("showFullScreen", "0") == "1"); - } - - /// - /// Get Region Size - /// - /// - public override System.Drawing.Size GetRegionSize() - { - if (RegionSizeChangeRequired()) - { - return new System.Drawing.Size(_videoPlayer.Width, _videoPlayer.Height); - } - else - { - return base.GetRegionSize(); - } - } - - /// - /// Get Region Location - /// - /// - public override System.Drawing.Point GetRegionLocation() - { - if (RegionSizeChangeRequired()) - { - return new System.Drawing.Point(0, 0); - } - else - { - return base.GetRegionLocation(); - } - } - } -} diff --git a/Media/VideoDS.cs b/Media/VideoDS.cs deleted file mode 100644 index 48cb1afb..00000000 --- a/Media/VideoDS.cs +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2012 Daniel Garner - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text; -using System.Windows.Forms; -using System.Diagnostics; -using System.IO; - -/// 05/09/12 Dan Created as a MOCK UP for the DirectShow Player - -namespace XiboClient -{ - class VideoDS : Media - { - private VideoPlayer _videoPlayer; - //private DSVideoPlayer _videoPlayer; - private bool _expired = false; - - /// - /// Constructor - /// - /// - public VideoDS(RegionOptions options) - : base(options.width, options.height, options.top, options.left) - { - _filesPlayed = 0; - _videoPlayer = new VideoPlayer(); - _videoPlayer.Width = options.width; - _videoPlayer.Height = options.height; - _videoPlayer.Location = new System.Drawing.Point(0, 0); - //_videoPlayer.SetPlaylist(options.mediaNodes, options.CurrentIndex); - - Controls.Add(_videoPlayer); - } - - public override void RenderMedia() - { - // Configure an event for the end of a file - // _videoPlayer.VideoEnd += new VideoPlayer.VideoEnd(_videoPlayer_FileEnd); - - // Use an event for the end of the playlist - //_videoPlayer.PlaylistEnd += new VideoPlayer.PlaylistEnd(_videoPlayer_VideoEnd); - - // Render media as normal (starts the timer, shows the form, etc) - base.RenderMedia(); - - try - { - // Start Player - //_videoPlayer.StartPlayer(); - - // Show the player - _videoPlayer.Show(); - - Trace.WriteLine(new LogMessage("Video - RenderMedia", "Video Started"), LogType.Audit.ToString()); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Video - RenderMedia", ex.Message), LogType.Error.ToString()); - - // Unable to start video - expire this media immediately - throw; - } - } - - //private void _videoPlayer_FileEnd() - //{ - // _filesPlayed++; - //} - - /// - /// Video End event - /// - private void _videoPlayer_VideoEnd() - { - // Has the video finished playing - if (_videoPlayer.FinishedPlaying) - { - Trace.WriteLine(new LogMessage("Video - _videoPlayer_VideoEnd", "End of video detected"), LogType.Audit.ToString()); - - // Immediately hide the player - _videoPlayer.Hide(); - - _expired = true; - } - } - - /// - /// Override the timer tick - /// - /// - /// - protected override void timer_Tick(object sender, EventArgs e) - { - if (_expired) - base.timer_Tick(sender, e); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - - } - - try - { - _videoPlayer.Hide(); - Controls.Remove(_videoPlayer); - _videoPlayer.Dispose(); - } - catch - { - - } - - base.Dispose(disposing); - } - } -} diff --git a/Media/VideoPlayer.Designer.cs b/Media/VideoPlayer.Designer.cs deleted file mode 100644 index d6956d6e..00000000 --- a/Media/VideoPlayer.Designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace XiboClient -{ - partial class VideoPlayer - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(VideoPlayer)); - this.axWindowsMediaPlayer1 = new AxWMPLib.AxWindowsMediaPlayer(); - ((System.ComponentModel.ISupportInitialize)(this.axWindowsMediaPlayer1)).BeginInit(); - this.SuspendLayout(); - // - // axWindowsMediaPlayer1 - // - this.axWindowsMediaPlayer1.Enabled = true; - this.axWindowsMediaPlayer1.Location = new System.Drawing.Point(0, 0); - this.axWindowsMediaPlayer1.Name = "axWindowsMediaPlayer1"; - this.axWindowsMediaPlayer1.OcxState = ((System.Windows.Forms.AxHost.State)(resources.GetObject("axWindowsMediaPlayer1.OcxState"))); - this.axWindowsMediaPlayer1.Size = new System.Drawing.Size(291, 269); - this.axWindowsMediaPlayer1.TabIndex = 0; - // - // VideoPlayer - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(292, 266); - this.Controls.Add(this.axWindowsMediaPlayer1); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; - this.Name = "VideoPlayer"; - this.Text = "VideoPlayer"; - ((System.ComponentModel.ISupportInitialize)(this.axWindowsMediaPlayer1)).EndInit(); - this.ResumeLayout(false); - - } - - #endregion - - private AxWMPLib.AxWindowsMediaPlayer axWindowsMediaPlayer1; - - } -} \ No newline at end of file diff --git a/Media/VideoPlayer.cs b/Media/VideoPlayer.cs deleted file mode 100644 index 3ae2c05f..00000000 --- a/Media/VideoPlayer.cs +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2015 Daniel Garner - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Text; -using System.Windows.Forms; -using System.Diagnostics; -using System.Runtime.InteropServices; - -/// 09/06/12 Dan Changed to raise an event when the video is finished -/// 04/08/12 Dan Changed to raise an error event if one is raised from the control - -namespace XiboClient -{ - public partial class VideoPlayer : Form - { - private bool _finished; - private bool _visible = true; - - public delegate void VideoFinished(); - public event VideoFinished VideoEnd; - - public delegate void VideoErrored(); - public event VideoErrored VideoError; - - private bool _looping = false; - - public VideoPlayer() - { - InitializeComponent(); - this.TopLevel = false; - - _finished = false; - } - - public void StartPlayer(string filePath) - { - if (_visible) - { - axWindowsMediaPlayer1.Visible = true; - axWindowsMediaPlayer1.Width = this.Width; - axWindowsMediaPlayer1.Height = this.Height; - } - else - { - axWindowsMediaPlayer1.Visible = false; - } - axWindowsMediaPlayer1.Location = new System.Drawing.Point(0, 0); - - axWindowsMediaPlayer1.uiMode = "none"; - axWindowsMediaPlayer1.URL = filePath; - axWindowsMediaPlayer1.stretchToFit = true; - axWindowsMediaPlayer1.windowlessVideo = false; - - axWindowsMediaPlayer1.PlayStateChange += new AxWMPLib._WMPOCXEvents_PlayStateChangeEventHandler(axWMP_PlayStateChange); - axWindowsMediaPlayer1.ErrorEvent += new EventHandler(axWindowsMediaPlayer1_ErrorEvent); - } - - /// - /// Set Loop - /// - /// - public void SetLooping(bool looping) - { - axWindowsMediaPlayer1.settings.setMode("loop", looping); - _looping = looping; - } - - /// - /// Set Mute - /// - /// - public void SetMute(bool mute) - { - if (mute) - { - axWindowsMediaPlayer1.settings.volume = 0; - axWindowsMediaPlayer1.settings.mute = true; - } - else - axWindowsMediaPlayer1.settings.volume = 100; - } - - /// - /// Set Volume - /// - /// - public void SetVolume(int volume) - { - if (volume == 0) - { - SetMute(true); - } - else - { - axWindowsMediaPlayer1.settings.volume = volume; - axWindowsMediaPlayer1.settings.mute = false; - } - } - - /// - /// Visible - /// - /// - public void SetVisible(bool visible) - { - _visible = visible; - } - - /// - /// Stop and Clear everything - /// - public void StopAndClear() - { - try - { - if (axWindowsMediaPlayer1 != null) - { - // Unbind events - axWindowsMediaPlayer1.PlayStateChange -= axWMP_PlayStateChange; - axWindowsMediaPlayer1.ErrorEvent -= axWindowsMediaPlayer1_ErrorEvent; - - // Release resources - Marshal.FinalReleaseComObject(axWindowsMediaPlayer1.currentMedia); - axWindowsMediaPlayer1.close(); - axWindowsMediaPlayer1.URL = null; - axWindowsMediaPlayer1.Dispose(); - - // Remove the WMP control - Controls.Remove(axWindowsMediaPlayer1); - - // Workaround to remove the event handlers from the cachedLayoutEventArgs - PerformLayout(); - - // Close this form - Close(); - - axWindowsMediaPlayer1 = null; - } - - GC.WaitForPendingFinalizers(); - GC.Collect(); - } - catch (AccessViolationException) - { - - } - } - - void axWindowsMediaPlayer1_ErrorEvent(object sender, EventArgs e) - { - // Get the error for logging - string error; - try - { - error = axWindowsMediaPlayer1.Error.get_Item(0).errorDescription; - } - catch - { - error = "Unknown Error"; - } - - Trace.WriteLine(new LogMessage("VideoPlayer - ErrorEvent", axWindowsMediaPlayer1.URL + ". Ex = " + error), LogType.Error.ToString()); - - // Raise the event - if (VideoError == null) - { - Trace.WriteLine(new LogMessage("VideoPlayer - ErrorEvent", "Error event handler is null"), LogType.Audit.ToString()); - } - else - { - VideoError(); - } - } - - void axWMP_PlayStateChange(object sender, AxWMPLib._WMPOCXEvents_PlayStateChangeEvent e) - { - if (e.newState == 8 && !_looping) - { - // indicate we are stopped - _finished = true; - - // Raise the event - if (VideoEnd == null) - { - Trace.WriteLine(new LogMessage("VideoPlayer - Playstate Complete", "Video end handler is null"), LogType.Audit.ToString()); - } - else - { - VideoEnd(); - } - } - } - - /// - /// Has this player finished playing - /// - public bool FinishedPlaying - { - get - { - return _finished; - } - } - } -} \ No newline at end of file diff --git a/Media/VideoPlayer.resx b/Media/VideoPlayer.resx deleted file mode 100644 index 2769d211..00000000 --- a/Media/VideoPlayer.resx +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w - LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACFTeXN0 - ZW0uV2luZG93cy5Gb3Jtcy5BeEhvc3QrU3RhdGUBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAuQAAAAIB - AAAAAQAAAAAAAAAAAAAAAKQAAAAAAwAACAACAAAAAAAFAAAAAAAAAPA/AwAAAAAABQAAAAAAAAAAAAgA - AgAAAAAAAwABAAAACwD//wMAAAAAAAsAAAAIAAIAAAAAAAMAMgAAAAsAAAAIAAoAAABuAG8AbgBlAAAA - CwD//wsAAAALAAAACwAAAAsAAAAIAAIAAAAAAAgAAgAAAAAACAACAAAAAAAIAAIAAAAAAAsAAAATHgAA - zRsAAAs= - - - \ No newline at end of file diff --git a/OptionsForm.xaml b/OptionsForm.xaml new file mode 100644 index 00000000..7e71a5ec --- /dev/null +++ b/OptionsForm.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/OptionsForm.xaml.cs b/OptionsForm.xaml.cs new file mode 100644 index 00000000..cb7bf738 --- /dev/null +++ b/OptionsForm.xaml.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace XiboClient +{ + /// + /// Interaction logic for OptionsForm.xaml + /// + public partial class OptionsForm : Window + { + public OptionsForm() + { + InitializeComponent(); + } + + + /// + /// Sets up the global proxy + /// + public static void SetGlobalProxy() + { + Debug.WriteLine("[IN]", "SetGlobalProxy"); + + Debug.WriteLine("Trying to detect a proxy.", "SetGlobalProxy"); + + if (ApplicationSettings.Default.ProxyUser != "") + { + // disable expect100Continue + ServicePointManager.Expect100Continue = false; + + Debug.WriteLine("Creating a network credential using the Proxy User.", "SetGlobalProxy"); + + NetworkCredential nc = new NetworkCredential(ApplicationSettings.Default.ProxyUser, ApplicationSettings.Default.ProxyPassword); + + if (ApplicationSettings.Default.ProxyDomain != "") + nc.Domain = ApplicationSettings.Default.ProxyDomain; + + WebRequest.DefaultWebProxy.Credentials = nc; + } + else + { + Debug.WriteLine("No Proxy.", "SetGlobalProxy"); + WebRequest.DefaultWebProxy.Credentials = null; + } + + // What if the URL for XMDS has a SSL certificate? + ServicePointManager.ServerCertificateValidationCallback += delegate (object sender, X509Certificate certificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors) + { + Debug.WriteLine("[IN]", "ServerCertificateValidationCallback"); + bool validationResult = false; + + Debug.WriteLine(certificate.Subject); + Debug.WriteLine(certificate.Issuer); + + if (sslPolicyErrors != System.Net.Security.SslPolicyErrors.None) + { + Debug.WriteLine(sslPolicyErrors.ToString()); + } + + validationResult = true; + + Debug.WriteLine("[OUT]", "ServerCertificateValidationCallback"); + return validationResult; + }; + + Debug.WriteLine("[OUT]", "SetGlobalProxy"); + + return; + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 1b58a94f..d24bbbb6 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,9 +1,10 @@ using System.Reflection; +using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Resources; +using System.Windows; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Xibo Open Source Digital Signage")] @@ -11,25 +12,45 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Xibo Digital Signage")] [assembly: AssemblyProduct("Xibo")] -[assembly: AssemblyCopyright("Copyright Xibo Signage Ltd © 2008-2020")] +[assembly: AssemblyCopyright("Copyright © Xibo Signage Ltd 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + // Version information for an assembly consists of the following four values: // // Major Version -// Minor Version +// Minor Version // Build Number // Revision // -[assembly: AssemblyVersion("2.0.1.201")] -[assembly: AssemblyFileVersion("2.0.1.201")] -[assembly: NeutralResourcesLanguageAttribute("en-GB")] +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.2.0.202")] +[assembly: AssemblyFileVersion("2.2.0.202")] +[assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index fc080467..90c7c9a4 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace XiboClient.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -60,26 +60,6 @@ internal Resources() { } } - /// - /// Looks up a localized string similar to Xibo - Digital Signage - http://www.xibo.org.uk - ///Copyright (C) 2006-2017 Spring Signage Ltd - /// - ///Xibo is free software: you can redistribute it and/or modify - ///it under the terms of the GNU Affero General Public License as published by - ///the Free Software Foundation, either version 3 of the License, or - ///any later version. - /// - ///Xibo is distributed in the hope that it will be useful, - ///but WITHOUT ANY WARRANTY; without even the implied warranty of - ///MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - ///GNU [rest of string was truncated]";. - /// - internal static string licence { - get { - return ResourceManager.GetString("licence", resourceCulture); - } - } - /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -99,14 +79,5 @@ internal static System.Drawing.Bitmap splash { return ((System.Drawing.Bitmap)(obj)); } } - - /// - /// Looks up a localized string similar to http://xibo.org.uk/manual. - /// - internal static string SupportUrl { - get { - return ResourceManager.GetString("SupportUrl", resourceCulture); - } - } } } diff --git a/Properties/Resources.resx b/Properties/Resources.resx index 9960f7d2..6c2e23ce 100644 --- a/Properties/Resources.resx +++ b/Properties/Resources.resx @@ -118,16 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - ..\Resources\licence.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 - ..\Resources\logo.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\Resources\splash.jpg;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - http://xibo.org.uk/manual + ..\Resources\splash.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a \ No newline at end of file diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index 77ca5d59..0ed9d715 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace XiboClient.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -22,5 +22,15 @@ public static Settings Default { return defaultInstance; } } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] + [global::System.Configuration.DefaultSettingValueAttribute("{{XMDS_LOCATION}}")] + public string XiboClient_xmds_xmds { + get { + return ((string)(this["XiboClient_xmds_xmds"])); + } + } } } diff --git a/Properties/Settings.settings b/Properties/Settings.settings index 8e615f25..09f5b37c 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -1,5 +1,9 @@  - + - + + + {{XMDS_LOCATION}} + + \ No newline at end of file diff --git a/Properties/app.manifest b/Properties/app.manifest deleted file mode 100644 index 5a90f2de..00000000 --- a/Properties/app.manifest +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 2a187009..6476b334 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Introduction -Xibo - Digital Signage - http://www.xibo.org.uk -Copyright (C) 2006-2015 Daniel Garner, James Packer and Alex Harrington +This is the repository for the Xibo for Windows Digital Signage Player, compatible with the Xibo Content Management System, and intended to be used for Digital Signage. + +If you are looking for more information about Xibo please refer to [our website](https://xibo.org.uk). + + + +## Licence + +Xibo - Digital Signage - http://xibo.org.uk - Copyright (C) 2006-2020 Xibo Signage Ltd Xibo is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -15,8 +22,24 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Xibo. If not, see . -# Repository -This is the repository for the Xibo for Windows .NET Player. + + +## Branches + +We have a number of branches + +- master: our stable branch, currently on version 2 +- develop: our next release work in progress +- feature/webview: a branch which tracks `develop` and is used to build the Microsoft Edge enabled version of the Player (Windows 10 onward) +- feature/wpf: an experimental branch of the Player which uses WPF instead of WinForms (Windows 10 onward) + # Issues -Please submit issues to the central repository for the Xibo Project: https://github.com/xibosignage/xibo/issues + +The Xibo Project uses GitHub Issues to record verified bugs only. + +If you're having difficulties with Xibo, or need support, +please post on our Community Site here: https://community.xibo.org.uk + +If the issue you are having does turn out to be a verified bug, a +representative will log it here as an issue on your behalf. \ No newline at end of file diff --git a/Rendering/Audio.cs b/Rendering/Audio.cs new file mode 100644 index 00000000..442370c3 --- /dev/null +++ b/Rendering/Audio.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; + +namespace XiboClient.Rendering +{ + class Audio : Media + { + private string _filePath; + private int _duration; + private bool _detectEnd = false; + private bool isLooping = false; + + private MediaElement mediaElement; + + /// + /// Constructor + /// + /// + public Audio(RegionOptions options) + : base(options) + { + _filePath = Uri.UnescapeDataString(options.uri).Replace('+', ' '); + _duration = options.duration; + + this.mediaElement = new MediaElement(); + this.mediaElement.Width = 0; + this.mediaElement.Height = 0; + this.mediaElement.Visibility = Visibility.Hidden; + this.mediaElement.Volume = options.Dictionary.Get("volume", 100); + + // Events + this.mediaElement.MediaEnded += MediaElement_MediaEnded; + this.mediaElement.MediaFailed += MediaElement_MediaFailed; + + // Should we loop? + this.isLooping = (options.Dictionary.Get("loop", "0") == "1" && _duration != 0); + } + + private void MediaElement_MediaFailed(object sender, ExceptionRoutedEventArgs e) + { + // Log and expire + Trace.WriteLine(new LogMessage("Audio", "MediaElement_MediaFailed: Media Failed. E = " + e.ErrorException.Message), LogType.Error.ToString()); + + Expired = true; + } + + private void MediaElement_MediaEnded(object sender, System.Windows.RoutedEventArgs e) + { + // Should we loop? + if (isLooping) + { + this.mediaElement.Position = TimeSpan.Zero; + this.mediaElement.Play(); + } + else + { + Expired = true; + } + } + + public override void RenderMedia() + { + // Check to see if the video exists or not (if it doesnt say we are already expired) + if (!File.Exists(_filePath)) + { + Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Local Video file " + _filePath + " not found.")); + throw new FileNotFoundException(); + } + + // Do we need to determine the end time ourselves? + if (_duration == 0) + { + // Set the duration to 1 second + // this essentially means RenderMedia will set up a timer which ticks every second + // when we're actually expired and we detect the end, we set expired + Duration = 1; + _detectEnd = true; + } + + // Render media as normal (starts the timer, shows the form, etc) + base.RenderMedia(); + + try + { + // Start Player + this.mediaElement.Source = new Uri(_filePath); + + this.MediaScene.Children.Add(this.mediaElement); + + Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Video Started"), LogType.Audit.ToString()); + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("Audio - RenderMedia", ex.Message), LogType.Error.ToString()); + + // Unable to start video - expire this media immediately + throw; + } + } + + public override void Stop() + { + // Remove the event handlers + this.mediaElement.MediaEnded -= MediaElement_MediaEnded; + this.mediaElement.MediaFailed -= MediaElement_MediaFailed; + + base.Stop(); + } + + /// + /// Override the timer tick + /// + /// + /// + protected override void timer_Tick(object sender, EventArgs e) + { + if (!_detectEnd || Expired) + { + // We're not end detect, so we pass the timer through + base.timer_Tick(sender, e); + } + } + } +} diff --git a/Rendering/Image.cs b/Rendering/Image.cs new file mode 100644 index 00000000..3f32a4a5 --- /dev/null +++ b/Rendering/Image.cs @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Rendering +{ + class Image : Media + { + public Image(RegionOptions options) : base(options) + { + + } + } +} diff --git a/Rendering/Layout.xaml b/Rendering/Layout.xaml new file mode 100644 index 00000000..bb2ecf31 --- /dev/null +++ b/Rendering/Layout.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs new file mode 100644 index 00000000..47b3d469 --- /dev/null +++ b/Rendering/Layout.xaml.cs @@ -0,0 +1,473 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Xml; +using XiboClient.Stats; + +namespace XiboClient.Rendering +{ + /// + /// Interaction logic for Layout.xaml + /// + public partial class Layout : UserControl + { + public Schedule Schedule; + + private double _layoutWidth; + private double _layoutHeight; + private double _scaleFactor; + + private bool isStatEnabled; + private Stat _stat; + + /// + /// Is this Layout Changing? + /// + public bool IsLayoutChanging = false; + + /// + /// Regions for this Layout + /// + private Collection _regions; + + /// + /// Last updated time of this Layout + /// + private DateTime layoutModifiedTime; + + private int _layoutId; + private int _scheduleId; + private bool isOverlay; + + public int ScheduleId { get { return _scheduleId; } } + + /// + /// Event to signify that this Layout's duration has elapsed + /// + public delegate void DurationElapsedDelegate(); + public event DurationElapsedDelegate DurationElapsedEvent; + + public Layout() + { + InitializeComponent(); + + // Create a new empty collection of Regions + _regions = new Collection(); + } + + /// + /// Load this Layout from its File + /// + /// + /// + /// + /// + public void loadFromFile(string layoutPath, int layoutId, int scheduleId, bool isOverlay) + { + // Store the Schedule and LayoutIds + this._scheduleId = scheduleId; + this._layoutId = layoutId; + this.isOverlay = isOverlay; + + // Get this layouts XML + XmlDocument layoutXml = new XmlDocument(); + + // try to open the layout file + try + { + using (FileStream fs = File.Open(layoutPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + using (XmlReader reader = XmlReader.Create(fs)) + { + layoutXml.Load(reader); + + reader.Close(); + } + fs.Close(); + } + } + catch (IOException ioEx) + { + Trace.WriteLine(new LogMessage("MainForm - PrepareLayout", "IOException: " + ioEx.ToString()), LogType.Error.ToString()); + throw; + } + + layoutModifiedTime = File.GetLastWriteTime(layoutPath); + + // Attributes of the main layout node + XmlNode layoutNode = layoutXml.SelectSingleNode("/layout"); + + XmlAttributeCollection layoutAttributes = layoutNode.Attributes; + + // Set the background and size of the form + _layoutWidth = int.Parse(layoutAttributes["width"].Value, CultureInfo.InvariantCulture); + _layoutHeight = int.Parse(layoutAttributes["height"].Value, CultureInfo.InvariantCulture); + + // Are stats enabled for this Layout? + isStatEnabled = (layoutAttributes["enableStat"] == null) ? true : (int.Parse(layoutAttributes["enableStat"].Value) == 1); + + // Scaling factor, will be applied to all regions + _scaleFactor = Math.Min(Width / _layoutWidth, Height / _layoutHeight); + + // Want to be able to center this shiv - therefore work out which one of these is going to have left overs + int backgroundWidth = (int)(_layoutWidth * _scaleFactor); + int backgroundHeight = (int)(_layoutHeight * _scaleFactor); + + double leftOverX; + double leftOverY; + + try + { + leftOverX = Math.Abs(Width - backgroundWidth); + leftOverY = Math.Abs(Height - backgroundHeight); + + if (leftOverX != 0) leftOverX = leftOverX / 2; + if (leftOverY != 0) leftOverY = leftOverY / 2; + } + catch + { + leftOverX = 0; + leftOverY = 0; + } + + // New region and region options objects + RegionOptions options = new RegionOptions(); + + options.LayoutModifiedDate = layoutModifiedTime; + options.LayoutSize = new System.Drawing.Size() + { + Width = (int)this.Width, + Height = (int)this.Height + }; + + // Deal with the color + // unless we are an overlay, in which case don't put up a background at all + if (!isOverlay) + { + Brush backgroundColour = Brushes.Black; + try + { + if (layoutAttributes["bgcolor"] != null && layoutAttributes["bgcolor"].Value != "") + { + var bc = new BrushConverter(); + backgroundColour = (Brush)bc.ConvertFrom(layoutAttributes["bgcolor"].Value); + options.backgroundColor = layoutAttributes["bgcolor"].Value; + } + } + catch + { + // Default black + options.backgroundColor = "#000000"; + } + + // Get the background + try + { + if (layoutAttributes["background"] != null && !string.IsNullOrEmpty(layoutAttributes["background"].Value)) + { + string bgFilePath = ApplicationSettings.Default.LibraryPath + @"\backgrounds\" + backgroundWidth + "x" + backgroundHeight + "_" + layoutAttributes["background"].Value; + + // Create a correctly sized background image in the temp folder + if (!File.Exists(bgFilePath)) + { + GenerateBackgroundImage(layoutAttributes["background"].Value, backgroundWidth, backgroundHeight, bgFilePath); + } + + Background = new ImageBrush(new BitmapImage(new Uri(bgFilePath))); + options.backgroundImage = @"/backgrounds/" + backgroundWidth + "x" + backgroundHeight + "_" + layoutAttributes["background"].Value; + } + else + { + // Assume there is no background image + options.backgroundImage = ""; + Background = backgroundColour; + } + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("MainForm - PrepareLayout", "Unable to set background: " + ex.Message), LogType.Error.ToString()); + + // Assume there is no background image + Background = backgroundColour; + options.backgroundImage = ""; + } + } + + // Get the regions + XmlNodeList listRegions = layoutXml.SelectNodes("/layout/region"); + XmlNodeList listMedia = layoutXml.SelectNodes("/layout/region/media"); + + // Check to see if there are any regions on this layout. + if (listRegions.Count == 0 || listMedia.Count == 0) + { + Trace.WriteLine(new LogMessage("PrepareLayout", + string.Format("A layout with {0} regions and {1} media has been detected.", listRegions.Count.ToString(), listMedia.Count.ToString())), + LogType.Info.ToString()); + + if (Schedule.ActiveLayouts == 1) + { + Trace.WriteLine(new LogMessage("PrepareLayout", "Only 1 layout scheduled and it has nothing to show."), LogType.Info.ToString()); + + throw new Exception("Only 1 layout schduled and it has nothing to show"); + } + else + { + Trace.WriteLine(new LogMessage("PrepareLayout", + string.Format(string.Format("An empty layout detected, will show for {0} seconds.", ApplicationSettings.Default.EmptyLayoutDuration.ToString()))), LogType.Info.ToString()); + + // Put a small dummy region in place, with a small dummy media node - which expires in 10 seconds. + XmlDocument dummyXml = new XmlDocument(); + dummyXml.LoadXml(string.Format("", + ApplicationSettings.Default.EmptyLayoutDuration.ToString())); + + // Replace the list of regions (they mean nothing as they are empty) + listRegions = dummyXml.SelectNodes("/region"); + } + } + + // Create a start record for this layout + _stat = new Stat(); + _stat.type = StatType.Layout; + _stat.scheduleID = _scheduleId; + _stat.layoutID = _layoutId; + _stat.fromDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + _stat.isEnabled = isStatEnabled; + + foreach (XmlNode region in listRegions) + { + // Is there any media + if (region.ChildNodes.Count == 0) + { + Debug.WriteLine("A region with no media detected"); + continue; + } + + // Region loop setting + options.RegionLoop = false; + + XmlNode regionOptionsNode = region.SelectSingleNode("options"); + + if (regionOptionsNode != null) + { + foreach (XmlNode option in regionOptionsNode.ChildNodes) + { + if (option.Name == "loop" && option.InnerText == "1") + options.RegionLoop = true; + } + } + + //each region + XmlAttributeCollection nodeAttibutes = region.Attributes; + + options.scheduleId = _scheduleId; + options.layoutId = _layoutId; + options.regionId = nodeAttibutes["id"].Value.ToString(); + options.width = (int)(Convert.ToDouble(nodeAttibutes["width"].Value, CultureInfo.InvariantCulture) * _scaleFactor); + options.height = (int)(Convert.ToDouble(nodeAttibutes["height"].Value, CultureInfo.InvariantCulture) * _scaleFactor); + options.left = (int)(Convert.ToDouble(nodeAttibutes["left"].Value, CultureInfo.InvariantCulture) * _scaleFactor); + options.top = (int)(Convert.ToDouble(nodeAttibutes["top"].Value, CultureInfo.InvariantCulture) * _scaleFactor); + + options.scaleFactor = _scaleFactor; + + // Store the original width and original height for scaling + options.originalWidth = (int)Convert.ToDouble(nodeAttibutes["width"].Value, CultureInfo.InvariantCulture); + options.originalHeight = (int)Convert.ToDouble(nodeAttibutes["height"].Value, CultureInfo.InvariantCulture); + + // Set the backgrounds (used for Web content offsets) + options.backgroundLeft = options.left * -1; + options.backgroundTop = options.top * -1; + + // Account for scaling + options.left = options.left + (int)leftOverX; + options.top = options.top + (int)leftOverY; + + // All the media nodes for this region / layout combination + options.mediaNodes = region.SelectNodes("media"); + + Region temp = new Region(); + temp.DurationElapsedEvent += new Region.DurationElapsedDelegate(Region_DurationElapsedEvent); + + Debug.WriteLine("Created new region", "MainForm - Prepare Layout"); + + // Dont be fooled, this innocent little statement kicks everything off + temp.loadFromOptions(options); + + // Add to our list of Regions + _regions.Add(temp); + + // Add this Region to our Scene + LayoutScene.Children.Add(temp); + + Debug.WriteLine("Adding region", "MainForm - Prepare Layout"); + } + + // Null stuff + listRegions = null; + listMedia = null; + } + + public void Stop() + { + if (_stat != null) + { + // Log the end of the currently running layout. + _stat.toDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + // Record this stat event in the statLog object + StatLog.Instance.RecordStat(_stat); + } + } + + public void Remove() + { + if (_regions == null) + return; + + lock (_regions) + { + foreach (Region region in _regions) + { + try + { + // Remove the region from the list of controls + this.LayoutScene.Children.Remove(region); + + // Clear the region + region.Clear(); + } + catch (Exception e) + { + // If we can't dispose we should log to understand why + Trace.WriteLine(new LogMessage("Layout", "Remove: " + e.Message), LogType.Info.ToString()); + } + } + + _regions.Clear(); + } + + _regions = null; + } + + /// + /// The duration of a Region has been reached + /// + private void Region_DurationElapsedEvent() + { + Trace.WriteLine(new LogMessage("MainForm - DurationElapsedEvent", "Region Elapsed"), LogType.Audit.ToString()); + + // Are we already changing the layout? + if (IsLayoutChanging) + { + Trace.WriteLine(new LogMessage("MainForm - DurationElapsedEvent", "Already Changing Layout"), LogType.Audit.ToString()); + return; + } + + bool isExpired = true; + + // Check the other regions to see if they are also expired. + foreach (Region temp in _regions) + { + if (!temp.IsExpired) + { + isExpired = false; + break; + } + } + + // If we are sure we have expired after checking all regions, then set the layout expired flag on them all + if (isExpired) + { + // Inform each region that the layout containing it has expired + foreach (Region temp in _regions) + { + temp.IsLayoutExpired = true; + } + + Trace.WriteLine(new LogMessage("MainForm - DurationElapsedEvent", "All Regions have expired. Raising a Next layout event."), LogType.Audit.ToString()); + + // We are changing the layout + IsLayoutChanging = true; + + // Yield and restart + Schedule.NextLayout(); + } + } + + /// + /// Generates a background image and saves it in the library for use later + /// + /// + /// + /// + /// + private static void GenerateBackgroundImage(string sourceFile, int backgroundWidth, int backgroundHeight, string bgFilePath) + { + Trace.WriteLine(new LogMessage("MainForm - GenerateBackgroundImage", "Trying to generate a background image. It will be saved: " + bgFilePath), LogType.Audit.ToString()); + + using (System.Drawing.Image img = System.Drawing.Image.FromFile(ApplicationSettings.Default.LibraryPath + @"\" + sourceFile)) + { + using (System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(img, backgroundWidth, backgroundHeight)) + { + EncoderParameters encoderParameters = new EncoderParameters(1); + EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L); + encoderParameters.Param[0] = qualityParam; + + ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg"); + + bmp.Save(bgFilePath, jpegCodec, encoderParameters); + } + } + } + + /// + /// Returns the image codec with the given mime type + /// + private static ImageCodecInfo GetEncoderInfo(string mimeType) + { + // Get image codecs for all image formats + ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders(); + + // Find the correct image codec + for (int i = 0; i < codecs.Length; i++) + if (codecs[i].MimeType == mimeType) + return codecs[i]; + return null; + } + } +} diff --git a/Rendering/Media.xaml b/Rendering/Media.xaml new file mode 100644 index 00000000..2e1a365b --- /dev/null +++ b/Rendering/Media.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs new file mode 100644 index 00000000..be0db414 --- /dev/null +++ b/Rendering/Media.xaml.cs @@ -0,0 +1,218 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Windows.Threading; + +namespace XiboClient.Rendering +{ + /// + /// Interaction logic for Media.xaml + /// + public partial class Media : UserControl + { + + /// + /// Event for Duration Elapsed + /// + /// + public delegate void DurationElapsedDelegate(int filesPlayed); + public event DurationElapsedDelegate DurationElapsedEvent; + protected int _filesPlayed = 1; + + /// + /// Gets or Sets the duration of this media. Will be 0 if "" + /// + public int Duration { get; set; } + + /// + /// Has this media exipred? + /// + public bool Expired { get; set; } = false; + + // Private Timer + protected DispatcherTimer _timer; + private bool _timerStarted = false; + + /// + /// The Region Options + /// + private RegionOptions options; + + public Media(RegionOptions options) + { + InitializeComponent(); + + // Store the options. + this.options = options; + } + + /// + /// Start the Timer for this Media + /// + protected void StartTimer() + { + //start the timer + if (!_timerStarted && Duration != 0) + { + _timer = new DispatcherTimer(); + _timer.Interval = TimeSpan.FromSeconds(Duration); + _timer.Start(); + + _timer.Tick += new EventHandler(timer_Tick); + + _timerStarted = true; + } + } + + /// + /// Reset the timer and start again + /// + protected void RestartTimer() + { + if (_timerStarted) + { + _timer.Stop(); + _timer.Start(); + } + else + { + StartTimer(); + } + } + + /// + /// Render Media call + /// + public virtual void RenderMedia() + { + // Start the timer for this media + StartTimer(); + } + + /// + /// Timer Tick + /// + /// + /// + protected virtual void timer_Tick(object sender, EventArgs e) + { + // Once it has expired we might as well stop the timer? + _timer.Stop(); + + // Signal that this Media Item's duration has elapsed + SignalElapsedEvent(); + } + + /// + /// Signals that an event is elapsed + /// Will raise a DurationElapsedEvent + /// + public void SignalElapsedEvent() + { + Expired = true; + + Trace.WriteLine(new LogMessage("Media - SignalElapsedEvent", "Media Complete"), LogType.Audit.ToString()); + + // We're complete + DurationElapsedEvent?.Invoke(_filesPlayed); + } + + /// + /// Stop this Media + /// + public virtual void Stop() + { + // Initiate any tidy up that is needed in here. + // Dispose of the Timer + if (_timer != null) + { + if (_timer.IsEnabled) + { + _timer.Stop(); + } + _timer = null; + } + } + + /// + /// Is a region size change required + /// + /// + public virtual bool RegionSizeChangeRequired() + { + return false; + } + + /// + /// Get Region Size + /// + /// + public virtual Size GetRegionSize() + { + return new Size(Width, Height); + } + + /// + /// Get Region Location + /// + /// + public virtual Point GetRegionLocation() + { + return new Point(this.options.top, this.options.left); + } + + public void transitionIn() + { + /*if (this.options.tr != null) + { + Transitions.MoveAnimation(medaiElemnt, OpacityProperty, transIn, transInDirection, transInDuration, "in", _top, _left); + } */ + } + + public void transitionOut() + { + /*if (transOut != null) + { + var timerTransition = new DispatcherTimer { Interval = TimeSpan.FromSeconds(transOutStartTime) }; + timerTransition.Start(); + timerTransition.Tick += (sender1, args) => + { + timerTransition.Stop(); + Transitions.MoveAnimation(medaiElemnt, OpacityProperty, transOut, transOutDirection, transOutDuration, "out", _top, _left); + }; + }*/ + } + } +} diff --git a/Rendering/Region.xaml b/Rendering/Region.xaml new file mode 100644 index 00000000..99bc180b --- /dev/null +++ b/Rendering/Region.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/Control/Region.cs b/Rendering/Region.xaml.cs similarity index 57% rename from Control/Region.cs rename to Rendering/Region.xaml.cs index 4bbaabab..54eccfbc 100644 --- a/Control/Region.cs +++ b/Rendering/Region.xaml.cs @@ -1,183 +1,124 @@ -/** - * Copyright (C) 2019 Xibo Signage Ltd - * - * Xibo - Digital Signage - http://www.xibo.org.uk - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ -using System; +using System; using System.Collections.Generic; -using System.Text; -using System.Windows.Forms; -using System.Xml; using System.Diagnostics; -using XiboClient.Properties; using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Xml; +using XiboClient.Logic; +using XiboClient.Stats; -namespace XiboClient +namespace XiboClient.Rendering { /// - /// Layout Region, container for Media + /// Interaction logic for Region.xaml /// - class Region : Panel + public partial class Region : UserControl { /// - /// Track regions created as overlays + /// Has the Layout Expired? /// - public int scheduleId = 0; - - private BlackList _blackList; - public delegate void DurationElapsedDelegate(); - public event DurationElapsedDelegate DurationElapsedEvent; - - private Media _media; - private RegionOptions _options; - private bool _hasExpired = false; - private bool _layoutExpired = false; - private bool _sizeResetRequired = false; - private int _currentSequence = -1; + public bool IsLayoutExpired = false; /// - /// Audio Sequence + /// Has this Region Expired? /// - private int _audioSequence = -1; - - // Stat objects - private StatLog _statLog; - private Stat _stat; - - // Cache Manager - private CacheManager _cacheManager; + public bool IsExpired = false; /// - /// Creates the Region + /// The Region Options /// - /// - /// - public Region(ref StatLog statLog, ref CacheManager cacheManager) - { - // Store the statLog - _statLog = statLog; - - // Store the cache manager - _cacheManager = cacheManager; - - //default options - _options = new RegionOptions(); - _options.width = 1024; - _options.height = 768; - _options.left = 0; - _options.top = 0; - _options.uri = null; - - Location = new System.Drawing.Point(_options.left, _options.top); - Size = new System.Drawing.Size(_options.width, _options.height); - BackColor = System.Drawing.Color.Transparent; - - if (ApplicationSettings.Default.DoubleBuffering) - { - SetStyle(ControlStyles.OptimizedDoubleBuffer, true); - SetStyle(ControlStyles.AllPaintingInWmPaint, true); - } - - // Create a new BlackList for us to use - _blackList = new BlackList(); - } + private RegionOptions options; /// - /// Options for the region + /// Current Media /// - public RegionOptions regionOptions - { - get - { - return this._options; - } - set - { - _options = value; + private Media currentMedia; - EvalOptions(); - } - } + /// + /// A stat record for this Region + /// + private Stat stat; /// - /// Inform the region that the layout has expired + /// Track the current sequence /// - public void setLayoutExpired() - { - _layoutExpired = true; - } + private int currentSequence = -1; + private bool _sizeResetRequired; + private int _audioSequence; /// - /// Has this region expired + /// Event to indicate that this Region's duration has elapsed /// - /// - public bool hasExpired() + public delegate void DurationElapsedDelegate(); + public event DurationElapsedDelegate DurationElapsedEvent; + + public Region() { - return _hasExpired; + InitializeComponent(); } - private void SetDimensions(int left, int top, int width, int height) + public void loadFromOptions(RegionOptions options) { - // Evaluate the width, etc - Location = new System.Drawing.Point(left, top); - Size = new System.Drawing.Size(width, height); + // Start of by setting our dimensions + SetDimensions(options.left, options.top, options.width, options.height); + + // Store the options + this.options = options; } - private void SetDimensions(System.Drawing.Point location, System.Drawing.Size size) + /// + /// Start + /// + public void Start() { - Debug.WriteLine("Setting Dimensions to " + size.ToString() + ", " + location.ToString()); - // Evaluate the width, etc - Size = size; - Location = location; + // Start this region + this.currentSequence = -1; + StartNext(); } - /// - /// Evaulates the change in options - /// - private void EvalOptions() + /// + /// Start the Next Media + /// + private void StartNext() { // First time - bool initialMedia = (_currentSequence == -1); + bool initialMedia = (this.currentSequence == -1); if (initialMedia) { // Evaluate the width, etc - SetDimensions(_options.left, _options.top, _options.width, _options.height); + SetDimensions(this.options.left, this.options.top, this.options.width, this.options.height); } // Try to populate a new media object for this region - Media newMedia = new Media(0, 0, 0, 0); + Media newMedia; // Loop around trying to start the next media bool startSuccessful = false; int countTries = 0; - + while (!startSuccessful) { // If we go round this the same number of times as media objects, then we are unsuccessful and should exception - if (countTries >= _options.mediaNodes.Count) + if (countTries >= this.options.mediaNodes.Count) throw new ArgumentOutOfRangeException("Unable to set and start a media node"); // Lets try again countTries++; // Store the current sequence - int temp = _currentSequence; + int temp = this.currentSequence; // Before we can try to set the next media node, we need to stop any currently running Audio StopAudio(); @@ -192,42 +133,46 @@ private void EvalOptions() // If the sequence hasnt been changed, OR the layout has been expired // there has been no change to the sequence, therefore the media we have already created is still valid // or this media has actually been destroyed and we are working out way out the call stack - if (_layoutExpired) + if (IsLayoutExpired) { return; } - else if (_currentSequence == temp) + else if (this.currentSequence == temp) { // Media has not changed, we are likely the only valid media item in the region // the layout has not yet expired, so depending on whether we loop or not, we either // reload the same media item again // or do nothing (return) // This could be made more succinct, but is clearer written as an elseif. - if (!_options.RegionLoop) + if (!this.options.RegionLoop) return; } // Store the Current Index - _options.CurrentIndex = _currentSequence; + this.options.CurrentIndex = this.currentSequence; // See if we can start the new media object try { - newMedia = CreateNextMediaNode(_options); + newMedia = CreateNextMediaNode(); } catch (Exception ex) { - Trace.WriteLine(new LogMessage("Region - Eval Options", "Unable to create new " + _options.type + " object: " + ex.Message), LogType.Error.ToString()); + Trace.WriteLine(new LogMessage("Region - Eval Options", "Unable to create new " + this.options.type + " object: " + ex.Message), LogType.Error.ToString()); // Try the next node startSuccessful = false; continue; } + // New Media record has been created + // ------------ // First thing we do is stop the current stat record if (!initialMedia) + { CloseCurrentStatRecord(); - + } + // Start the new media try { @@ -239,7 +184,7 @@ private void EvalOptions() } else if (_sizeResetRequired) { - SetDimensions(_options.left, _options.top, _options.width, _options.height); + SetDimensions(this.options.left, this.options.top, this.options.width, this.options.height); _sizeResetRequired = false; } @@ -247,7 +192,7 @@ private void EvalOptions() } catch (Exception ex) { - Trace.WriteLine(new LogMessage("Region - Eval Options", "Unable to start new " + _options.type + " object: " + ex.Message), LogType.Error.ToString()); + Trace.WriteLine(new LogMessage("Region - Eval Options", "Unable to start new " + this.options.type + " object: " + ex.Message), LogType.Error.ToString()); startSuccessful = false; continue; } @@ -257,12 +202,12 @@ private void EvalOptions() // Remove the old media if (!initialMedia) { - StopMedia(_media); - _media = null; + StopMedia(currentMedia); + currentMedia = null; } // Change the reference - _media = newMedia; + currentMedia = newMedia; // Open a stat record OpenStatRecordForMedia(); @@ -276,7 +221,7 @@ private void EvalOptions() private bool SetNextMediaNodeInOptions() { // What if there are no media nodes? - if (_options.mediaNodes.Count == 0) + if (this.options.mediaNodes.Count == 0) { Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", "No media nodes to display"), LogType.Audit.ToString()); @@ -284,84 +229,85 @@ private bool SetNextMediaNodeInOptions() } // Zero out the options that are persisted - _options.text = ""; - _options.documentTemplate = ""; - _options.copyrightNotice = ""; - _options.scrollSpeed = 30; - _options.updateInterval = 6; - _options.uri = ""; - _options.direction = "none"; - _options.javaScript = ""; - _options.FromDt = DateTime.MinValue; - _options.ToDt = DateTime.MaxValue; - _options.Dictionary = new MediaDictionary(); + this.options.text = ""; + this.options.documentTemplate = ""; + this.options.copyrightNotice = ""; + this.options.scrollSpeed = 30; + this.options.updateInterval = 6; + this.options.uri = ""; + this.options.direction = "none"; + this.options.javaScript = ""; + this.options.FromDt = DateTime.MinValue; + this.options.ToDt = DateTime.MaxValue; + this.options.Dictionary = new MediaDictionary(); // Tidy up old audio if necessary - foreach (Media audio in _options.Audio) + foreach (Media audio in this.options.Audio) { try { // Unbind any events and dispose audio.DurationElapsedEvent -= audio_DurationElapsedEvent; - audio.Dispose(); + audio.Stop(); } catch { Trace.WriteLine(new LogMessage("Region - SetNextMediaNodeInOptions", "Unable to dispose of audio item"), LogType.Audit.ToString()); } } - + // Empty the options node - _options.Audio.Clear(); + this.options.Audio.Clear(); // Get a media node bool validNode = false; int numAttempts = 0; - + // Loop through all the nodes in order - while (numAttempts < _options.mediaNodes.Count) + while (numAttempts < this.options.mediaNodes.Count) { // Move the sequence on - _currentSequence++; + this.currentSequence++; - if (_currentSequence >= _options.mediaNodes.Count) + if (this.currentSequence >= this.options.mediaNodes.Count) { // Start from the beginning - _currentSequence = 0; + this.currentSequence = 0; // We have expired (want to raise an expired event to the parent) - _hasExpired = true; + IsExpired = true; - Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", "Media Expired:" + _options.ToString() + " . Reached the end of the sequence. Starting from the beginning."), LogType.Audit.ToString()); + Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", "Media Expired:" + this.options.ToString() + " . Reached the end of the sequence. Starting from the beginning."), LogType.Audit.ToString()); // Region Expired - if (DurationElapsedEvent != null) - DurationElapsedEvent(); + DurationElapsedEvent?.Invoke(); // We want to continue on to show the next media (unless the duration elapsed event triggers a region change) - if (_layoutExpired) + if (IsLayoutExpired) + { return true; + } } // Get the media node for this sequence - XmlNode mediaNode = _options.mediaNodes[_currentSequence]; + XmlNode mediaNode = this.options.mediaNodes[this.currentSequence]; XmlAttributeCollection nodeAttributes = mediaNode.Attributes; // Set the media id - if (nodeAttributes["id"].Value != null) - _options.mediaid = nodeAttributes["id"].Value; + if (nodeAttributes["id"].Value != null) + this.options.mediaid = nodeAttributes["id"].Value; // Set the file id if (nodeAttributes["fileId"] != null) { - _options.FileId = int.Parse(nodeAttributes["fileId"].Value); + this.options.FileId = int.Parse(nodeAttributes["fileId"].Value); } // Check isnt blacklisted - if (_blackList.BlackListed(_options.mediaid)) + if (BlackList.Instance.BlackListed(this.options.mediaid)) { - Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", string.Format("MediaID [{0}] has been blacklisted.", _options.mediaid)), LogType.Error.ToString()); - + Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", string.Format("MediaID [{0}] has been blacklisted.", this.options.mediaid)), LogType.Error.ToString()); + // Increment the number of attempts and try again numAttempts++; @@ -370,13 +316,14 @@ private bool SetNextMediaNodeInOptions() } // Stats enabled? - _options.isStatEnabled = (nodeAttributes["enableStat"] == null) ? true : (int.Parse(nodeAttributes["enableStat"].Value) == 1); + this.options.isStatEnabled = (nodeAttributes["enableStat"] == null) ? true : (int.Parse(nodeAttributes["enableStat"].Value) == 1); // Parse the options for this media node ParseOptionsForMediaNode(mediaNode, nodeAttributes); // Is this widget inside the from/to date? - if (!(_options.FromDt <= DateTime.Now && _options.ToDt > DateTime.Now)) { + if (!(this.options.FromDt <= DateTime.Now && this.options.ToDt > DateTime.Now)) + { Trace.WriteLine(new LogMessage("Region", "SetNextMediaNode: Widget outside from/to date."), LogType.Audit.ToString()); // Increment the number of attempts and try again @@ -390,10 +337,10 @@ private bool SetNextMediaNodeInOptions() validNode = true; // Is this a file based media node? - if (_options.type == "video" || _options.type == "flash" || _options.type == "image" || _options.type == "powerpoint" || _options.type == "audio" || _options.type == "htmlpackage") + if (this.options.type == "video" || this.options.type == "flash" || this.options.type == "image" || this.options.type == "powerpoint" || this.options.type == "audio" || this.options.type == "htmlpackage") { // Use the cache manager to determine if the file is valid - validNode = _cacheManager.IsValidPath(_options.uri); + validNode = CacheManager.Instance.IsValidPath(this.options.uri); } // If we have a valid node, break out of the loop @@ -408,7 +355,7 @@ private bool SetNextMediaNodeInOptions() if (!validNode) return false; - Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", "New media detected " + _options.type), LogType.Audit.ToString()); + Trace.WriteLine(new LogMessage("Region - SetNextMediaNode", "New media detected " + this.options.type), LogType.Audit.ToString()); return true; } @@ -425,21 +372,21 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection // parse all the possible media type nodes here. // Type and Duration will always be on the media node - _options.type = nodeAttributes["type"].Value; + this.options.type = nodeAttributes["type"].Value; // Render as if (nodeAttributes["render"] != null) - _options.render = nodeAttributes["render"].Value; + this.options.render = nodeAttributes["render"].Value; //TODO: Check the type of node we have, and make sure it is supported. if (nodeAttributes["duration"].Value != "") { - _options.duration = int.Parse(nodeAttributes["duration"].Value); + this.options.duration = int.Parse(nodeAttributes["duration"].Value); } else { - _options.duration = 60; + this.options.duration = 60; Trace.WriteLine("Duration is Empty, using a default of 60.", "Region - SetNextMediaNode"); } @@ -448,24 +395,24 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection { if (nodeAttributes["fromDt"] != null) { - _options.FromDt = DateTime.Parse(nodeAttributes["fromDt"].Value, CultureInfo.InvariantCulture); + this.options.FromDt = DateTime.Parse(nodeAttributes["fromDt"].Value, CultureInfo.InvariantCulture); } if (nodeAttributes["toDt"] != null) { - _options.ToDt = DateTime.Parse(nodeAttributes["toDt"].Value, CultureInfo.InvariantCulture); + this.options.ToDt = DateTime.Parse(nodeAttributes["toDt"].Value, CultureInfo.InvariantCulture); } } - catch (Exception e) + catch { Trace.WriteLine(new LogMessage("Region", "ParseOptionsForMediaNode: Unable to parse widget from/to dates."), LogType.Error.ToString()); } // We cannot have a 0 duration here... not sure why we would... but - if (_options.duration == 0 && _options.type != "video" && _options.type != "localvideo") + if (this.options.duration == 0 && this.options.type != "video" && this.options.type != "localvideo") { int emptyLayoutDuration = int.Parse(ApplicationSettings.Default.EmptyLayoutDuration.ToString()); - _options.duration = (emptyLayoutDuration == 0) ? 10 : emptyLayoutDuration; + this.options.duration = (emptyLayoutDuration == 0) ? 10 : emptyLayoutDuration; } // There will be some stuff on option nodes @@ -479,21 +426,21 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection { if (option.Name == "direction") { - _options.direction = option.InnerText; + this.options.direction = option.InnerText; } else if (option.Name == "uri") { - _options.uri = option.InnerText; + this.options.uri = option.InnerText; } else if (option.Name == "copyright") { - _options.copyrightNotice = option.InnerText; + this.options.copyrightNotice = option.InnerText; } else if (option.Name == "scrollSpeed") { try { - _options.scrollSpeed = int.Parse(option.InnerText); + this.options.scrollSpeed = int.Parse(option.InnerText); } catch { @@ -506,19 +453,19 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection try { - _options.updateInterval = int.Parse(option.InnerText); + this.options.updateInterval = int.Parse(option.InnerText); } catch { // Update interval not defined, so assume a high value - _options.updateInterval = 3600; + this.options.updateInterval = 3600; Trace.WriteLine("Non integer updateInterval in XLF", "Region - SetNextMediaNode"); } } // Add this to the options object - _options.Dictionary.Add(option.Name, option.InnerText); + this.options.Dictionary.Add(option.Name, option.InnerText); } // And some stuff on Raw nodes @@ -530,19 +477,19 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection { if (raw.Name == "text") { - _options.text = raw.InnerText; + this.options.text = raw.InnerText; } else if (raw.Name == "template") { - _options.documentTemplate = raw.InnerText; + this.options.documentTemplate = raw.InnerText; } else if (raw.Name == "embedHtml") { - _options.text = raw.InnerText; + this.options.text = raw.InnerText; } else if (raw.Name == "embedScript") { - _options.javaScript = raw.InnerText; + this.options.javaScript = raw.InnerText; } } } @@ -566,7 +513,7 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection if (options.Dictionary.Get("loop", 0) == 1) { // Set the media duration to be equal to the duration of the parent media - options.duration = (_options.duration == 0) ? int.MaxValue : _options.duration; + options.duration = (this.options.duration == 0) ? int.MaxValue : this.options.duration; } } @@ -574,11 +521,11 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection options.Dictionary.Add("volume", audioNode.Attributes["volume"].Value); Media audioMedia = new Audio(options); - + // Bind to the media complete event audioMedia.DurationElapsedEvent += audio_DurationElapsedEvent; - _options.Audio.Add(audioMedia); + this.options.Audio.Add(audioMedia); } } @@ -587,15 +534,15 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection if (!updateIntervalProvided) { // Special handling for text/webpages because we know they should never have a default update interval applied - if (_options.type == "webpage" || _options.type == "text") + if (this.options.type == "webpage" || this.options.type == "text") { // Very high (will expire eventually, but shouldn't cause a routine request for a new resource - _options.updateInterval = int.MaxValue; + this.options.updateInterval = int.MaxValue; } else { // Default to 5 minutes for those items that do not provide an update interval - _options.updateInterval = 5; + this.options.updateInterval = 5; } } } @@ -603,93 +550,99 @@ private void ParseOptionsForMediaNode(XmlNode mediaNode, XmlAttributeCollection /// /// Create the next media node based on the provided options /// - /// /// - private Media CreateNextMediaNode(RegionOptions options) + private Media CreateNextMediaNode() { Media media; + // Grab a local copy of options + RegionOptions options = this.options; + Trace.WriteLine(new LogMessage("Region - CreateNextMediaNode", string.Format("Creating new media: {0}, {1}", options.type, options.mediaid)), LogType.Audit.ToString()); - - if (options.render == "html") - { - media = WebMedia.GetConfiguredWebMedia(options); - } - else + + // We've set our next media node in options already + // this includes checking that file based media is valid. + switch (options.type) { - // We've set our next media node in options already - // this includes checking that file based media is valid. - switch (options.type) - { - case "image": - options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - media = new ImagePosition(options); - break; - - case "powerpoint": - options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - media = new PowerPoint(options); - break; - - case "video": - options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - - // Which video engine are we using? - if (ApplicationSettings.Default.VideoRenderingEngine == "DirectShow") - media = new VideoDS(options); - else - media = new Video(options); - - break; - - case "localvideo": - // Which video engine are we using? - if (ApplicationSettings.Default.VideoRenderingEngine == "DirectShow") - media = new VideoDS(options); - else - media = new Video(options); - - break; - - case "audio": - options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - media = new Audio(options); - break; - - case "datasetview": - case "embedded": - case "ticker": - case "text": - case "webpage": - media = WebMedia.GetConfiguredWebMedia(options); - break; + case "image": + options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; + media = new Image(options); + break; - case "flash": - options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - media = new Flash(options); - break; + /*case "powerpoint": + options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; + media = new PowerPoint(options); + break;*/ - case "shellcommand": - media = new ShellCommand(options); - break; + /*case "video": + options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - case "htmlpackage": - media = WebMedia.GetConfiguredWebMedia(options); - ((WebMedia)media).ConfigureForHtmlPackage(); - break; + // Which video engine are we using? + if (ApplicationSettings.Default.VideoRenderingEngine == "DirectShow") + media = new VideoDS(options); + else + media = new Video(options); + + break; + + case "localvideo": + // Which video engine are we using? + if (ApplicationSettings.Default.VideoRenderingEngine == "DirectShow") + media = new VideoDS(options); + else + media = new Video(options); + + break; + + case "audio": + options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; + media = new Audio(options); + break;*/ + + case "datasetview": + case "embedded": + case "ticker": + case "text": + case "webpage": + media = WebMedia.GetConfiguredWebMedia(options); + + break; - default: + /*case "flash": + options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; + media = new Flash(options); + break; + + case "shellcommand": + media = new ShellCommand(options); + break;*/ + + case "htmlpackage": + media = WebMedia.GetConfiguredWebMedia(options); + ((WebMedia)media).ConfigureForHtmlPackage(); + break; + + default: + if (options.render == "html") + { + media = WebMedia.GetConfiguredWebMedia(options); + } + else + { throw new InvalidOperationException("Not a valid media node type: " + options.type); - } + } + break; } // Sets up the timer for this media, if it hasn't already been set if (media.Duration == 0) + { media.Duration = options.duration; + } // Add event handler for when this completes media.DurationElapsedEvent += new Media.DurationElapsedDelegate(media_DurationElapsedEvent); - + return media; } @@ -701,8 +654,11 @@ private void StartMedia(Media media) { Trace.WriteLine(new LogMessage("Region - StartMedia", "Starting media"), LogType.Audit.ToString()); + // Render the media, this adds the child controls to the Media UserControls grid media.RenderMedia(); - Controls.Add(media); + + // Add to this scene + this.RegionScene.Children.Add(media); // Reset the audio sequence and start _audioSequence = 1; @@ -715,13 +671,15 @@ private void StartMedia(Media media) private void startAudio() { // Start any associated audio - if (_options.Audio.Count >= _audioSequence) + if (this.options.Audio.Count >= _audioSequence) { - Media audio = _options.Audio[_audioSequence - 1]; + Media audio = this.options.Audio[_audioSequence - 1]; // call render media and add to controls audio.RenderMedia(); - Controls.Add(audio); + + // Add to this scene + this.RegionScene.Children.Add(audio); } } @@ -729,18 +687,20 @@ private void startAudio() /// Audio Finished Playing ///
/// - void audio_DurationElapsedEvent(int filesPlayed) + private void audio_DurationElapsedEvent(int filesPlayed) { try { - StopMedia(_options.Audio[_audioSequence - 1]); + StopMedia(this.options.Audio[_audioSequence - 1]); } catch (Exception ex) { Trace.WriteLine(new LogMessage("Region - audio_DurationElapsedEvent", "Audio - Unable to dispose. Ex = " + ex.Message), LogType.Audit.ToString()); } - _audioSequence = _audioSequence + filesPlayed; + _audioSequence += filesPlayed; + + // Start startAudio(); } @@ -752,22 +712,19 @@ private void StopMedia(Media media) { Trace.WriteLine(new LogMessage("Region - Stop Media", "Stopping media"), LogType.Audit.ToString()); - // Hide the media - media.Hide(); - - // Remove the controls - Controls.Remove(media); - // Dispose of the current media try { - // Dispose of the media - media.Dispose(); + // Tidy Up + media.Stop(); } catch (Exception ex) { Trace.WriteLine(new LogMessage("Region - Stop Media", "Unable to dispose. Ex = " + ex.Message), LogType.Audit.ToString()); } + + // Remove the controls + RegionScene.Children.Remove(media); } /// @@ -776,11 +733,11 @@ private void StopMedia(Media media) private void StopAudio() { // Stop the currently playing audio (if there is any) - if (_options.Audio.Count > 0) + if (this.options.Audio.Count > 0) { try { - StopMedia(_options.Audio[_audioSequence - 1]); + StopMedia(this.options.Audio[_audioSequence - 1]); } catch (Exception ex) { @@ -795,13 +752,13 @@ private void StopAudio() private void OpenStatRecordForMedia() { // This media has started and is being replaced - _stat = new Stat(); - _stat.type = StatType.Media; - _stat.fromDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - _stat.scheduleID = _options.scheduleId; - _stat.layoutID = _options.layoutId; - _stat.mediaID = _options.mediaid; - _stat.isEnabled = _options.isStatEnabled; + this.stat = new Stat(); + this.stat.type = StatType.Media; + this.stat.fromDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + this.stat.scheduleID = this.options.scheduleId; + this.stat.layoutID = this.options.layoutId; + this.stat.mediaID = this.options.mediaid; + this.stat.isEnabled = this.options.isStatEnabled; } /// @@ -812,10 +769,10 @@ private void CloseCurrentStatRecord() try { // Here we say that this media is expired - _stat.toDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + this.stat.toDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); // Record this stat event in the statLog object - _statLog.RecordStat(_stat); + StatLog.Instance.RecordStat(this.stat); } catch { @@ -828,20 +785,20 @@ private void CloseCurrentStatRecord() /// private void media_DurationElapsedEvent(int filesPlayed) { - Trace.WriteLine(new LogMessage("Region - DurationElapsedEvent", string.Format("Media Elapsed: {0}", _options.uri)), LogType.Audit.ToString()); + Trace.WriteLine(new LogMessage("Region - DurationElapsedEvent", string.Format("Media Elapsed: {0}", this.options.uri)), LogType.Audit.ToString()); if (filesPlayed > 1) // Increment the _current sequence by the number of filesPlayed (minus 1) - _currentSequence = _currentSequence + (filesPlayed - 1); + this.currentSequence = this.currentSequence + (filesPlayed - 1); // If this layout has been expired we know that everything will soon be torn down, so do nothing - if (_layoutExpired) + if (IsLayoutExpired) return; // make some decisions about what to do next try { - EvalOptions(); + StartNext(); } catch (Exception e) { @@ -849,13 +806,32 @@ private void media_DurationElapsedEvent(int filesPlayed) // What do we do if there is an exception moving to the next media node? // For some reason we cannot set a media node... so we need this region to become invalid - _hasExpired = true; - if (DurationElapsedEvent != null) - DurationElapsedEvent(); + IsExpired = true; + + // Fire elapsed + DurationElapsedEvent?.Invoke(); + return; } } + private void SetDimensions(int left, int top, int width, int height) + { + Debug.WriteLine("Setting Dimensions to W:" + width + ", H:" + height + ", (" + left + "," + top + ")"); + + // Evaluate the width, etc + Width = width; + Height = height; + HorizontalAlignment = HorizontalAlignment.Left; + VerticalAlignment = VerticalAlignment.Top; + Margin = new Thickness(left, top, 0, 0); + } + + private void SetDimensions(Point location, Size size) + { + SetDimensions((int)location.X, (int)location.Y, (int)size.Width, (int)size.Height); + } + /// /// Clears the Region of anything that it shouldnt still have... /// called when Destroying a Layout and when Removing an Overlay @@ -868,17 +844,19 @@ public void Clear() StopAudio(); // Stop the current media item - if (_media != null) - StopMedia(_media); + if (this.currentMedia != null) + { + StopMedia(this.currentMedia); + } // What happens if we are disposing this region but we have not yet completed the stat event? - if (string.IsNullOrEmpty(_stat.toDate)) + if (string.IsNullOrEmpty(this.stat.toDate)) { // Say that this media has ended - _stat.toDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + this.stat.toDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); // Record this stat event in the statLog object - _statLog.RecordStat(_stat); + StatLog.Instance.RecordStat(this.stat); } } catch @@ -886,67 +864,5 @@ public void Clear() Trace.WriteLine(new LogMessage("Region - Clear", "Error closing off stat record"), LogType.Error.ToString()); } } - - /// - /// Performs the disposal. - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - try - { - // Tidy up old audio if necessary - foreach (Media audio in _options.Audio) - { - try - { - Debug.WriteLine("Removing audio on region dispose", "Region"); - - // Unbind any events and dispose - audio.DurationElapsedEvent -= audio_DurationElapsedEvent; - audio.Dispose(); - } - catch - { - Trace.WriteLine(new LogMessage("Region - Dispose", "Unable to dispose of audio item"), LogType.Audit.ToString()); - } - } - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Region - Dispose", "Unable to dispose audio for media. Ex = " + ex.Message), LogType.Audit.ToString()); - } - - try - { - _options.Dictionary.Clear(); - _options.Audio.Clear(); - - Debug.WriteLine("Removing media on region dispose", "Region"); - - // Remove media from Controls - Controls.Remove(_media); - - // Unbind and dispose - _media.DurationElapsedEvent -= media_DurationElapsedEvent; - _media.Dispose(); - _media = null; - - Debug.WriteLine("Media Disposed by Region", "Region - Dispose"); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Region - Dispose", "Unable to dispose media. Ex = " + ex.Message), LogType.Audit.ToString()); - } - finally - { - if (_media != null) - _media = null; - } - } - - base.Dispose(disposing); - } } } diff --git a/Rendering/Transitions.cs b/Rendering/Transitions.cs new file mode 100644 index 00000000..5d0207e4 --- /dev/null +++ b/Rendering/Transitions.cs @@ -0,0 +1,283 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ + using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace XiboClient.Rendering +{ + class Transitions + { + /// + /// Select Animation type , pass animation details + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void MoveAnimation(object item, DependencyProperty dp, string type, string direction, double duration, string inOut, int top, int left) + { + switch (type) + { + case "fly": + FlyAnimation(item, direction, duration, inOut, top, left); + break; + case "fadeIn": + FadeIn(item, dp, duration); + break; + case "fadeOut": + FadeOut(item, dp, duration); + break; + } + } + + /// + /// Fade in animation + /// + /// + /// + /// + private static void FadeIn(object item, DependencyProperty dp, double duration) + { + DoubleAnimation doubleAnimationFade = new DoubleAnimation + { + From = 0, + To = 1, + Duration = TimeSpan.FromMilliseconds(duration) + }; + + if (item is System.Windows.Controls.Image) + { + (item as System.Windows.Controls.Image).BeginAnimation(dp, doubleAnimationFade); + } + else if (item is MediaElement) + { + (item as MediaElement).BeginAnimation(dp, doubleAnimationFade); + } + } + + /// + /// FadeOut animation + /// + /// + /// + /// + private static void FadeOut(object item, DependencyProperty dp, double duration) + { + DoubleAnimation doubleAnimationFade = new DoubleAnimation + { + From = 1, + To = 0, + Duration = TimeSpan.FromMilliseconds(duration) + }; + + if (item is System.Windows.Controls.Image) + { + (item as System.Windows.Controls.Image).BeginAnimation(dp, doubleAnimationFade); + } + else if (item is MediaElement) + { + (item as MediaElement).BeginAnimation(dp, doubleAnimationFade); + } + } + + /// + /// item moving animation with all directions + /// + /// + /// + /// + /// + private static void FlyAnimation(object item, string direction, double duration, string inOut, int top, int left) + { + int inValueY = 0; + int inValueX = 0; + + int endValueX = 0; + int endValueY = 0; + + int screenHeight = (int)SystemParameters.PrimaryScreenHeight; + int screenWight = Convert.ToInt32(SystemParameters.PrimaryScreenWidth); + + if (inOut == "in") + { + inValueY = screenHeight; + inValueX = screenWight; + endValueX = 0; + endValueY = 0; + } + else if (inOut == "out") + { + inValueY = 0; + inValueX = 0; + endValueX = screenWight; + endValueY = screenHeight; + } + + DoubleAnimation doubleAnimationX = new DoubleAnimation(); + DoubleAnimation doubleAnimationY = new DoubleAnimation(); + doubleAnimationX.To = endValueX; + doubleAnimationY.To = endValueY; + doubleAnimationX.Duration = TimeSpan.FromMilliseconds(duration); + doubleAnimationY.Duration = TimeSpan.FromMilliseconds(duration); + var trans = new TranslateTransform(); + switch (direction) + { + case "N": + if (inOut == "in") + { + doubleAnimationY.From = (screenHeight - top); + } + else + { + doubleAnimationY.From = top; + } + + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); + break; + case "NE": + if (inOut == "in") + { + doubleAnimationX.From = (screenWight - left); + doubleAnimationY.From = (screenHeight - top); + } + else + { + doubleAnimationX.From = left; + doubleAnimationY.From = top; + } + + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + break; + case "E": + if (inOut == "in") + { + doubleAnimationX.From = -(screenWight - left); + } + else + { + if (left == 0) + { + doubleAnimationX.From = -left; + } + else + { + doubleAnimationX.From = -(screenWight - left); + } + + } + + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + break; + case "SE": + if (inOut == "in") + { + doubleAnimationX.From = left; + doubleAnimationY.From = -(screenHeight - top); + } + else + { + doubleAnimationX.From = (screenWight - left); + doubleAnimationY.From = -(screenHeight - top); + } + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + break; + case "S": + if (inOut == "in") + { + doubleAnimationX.From = -(screenHeight - top); + } + else + { + doubleAnimationX.From = -top; + } + + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationX); + break; + case "SW": + if (inOut == "in") + { + doubleAnimationX.From = (screenWight - left); + doubleAnimationY.From = -top; + } + else + { + doubleAnimationX.From = left; + doubleAnimationY.From = -(screenHeight - left); + } + + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); + break; + case "W": + if (inOut == "in") + { + doubleAnimationX.From = (screenWight - left); + } + else + { + doubleAnimationX.From = -left; + } + + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + + break; + case "NW": + if (inOut == "in") + { + doubleAnimationX.From = (screenWight - left); + doubleAnimationY.From = (screenHeight - top); + } + else + { + doubleAnimationX.From = left; + doubleAnimationY.From = top; + } + + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); + + break; + } + + if (item is System.Windows.Controls.Image) + { + (item as System.Windows.Controls.Image).RenderTransform = trans; + } + else if (item is MediaElement) + { + (item as MediaElement).RenderTransform = trans; + } + } + } +} \ No newline at end of file diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs new file mode 100644 index 00000000..5ffe3fb0 --- /dev/null +++ b/Rendering/WebEdge.cs @@ -0,0 +1,120 @@ +using Microsoft.Toolkit.Wpf.UI.Controls; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Rendering +{ + class WebEdge : WebMedia + { + private WebView mWebView; + + public WebEdge(RegionOptions options) + : base(options) + { + } + + /// + /// Render Media + /// + public override void RenderMedia() + { + // Create the web view we will use + mWebView = new WebView(); + + ((ISupportInitialize)mWebView).BeginInit(); + + mWebView.Width = Width; + mWebView.Height = Height; + mWebView.Visibility = System.Windows.Visibility.Hidden; + mWebView.IsPrivateNetworkClientServerCapabilityEnabled = true; + mWebView.NavigationCompleted += MWebView_NavigationCompleted; + mWebView.DOMContentLoaded += MWebView_DOMContentLoaded; + + this.MediaScene.Children.Add(mWebView); + + ((ISupportInitialize)mWebView).EndInit(); + + // _webBrowser.ScrollBarsEnabled = false; + // _webBrowser.ScriptErrorsSuppressed = true; + + HtmlUpdatedEvent += WebMediaHtmlUdatedEvent; + + if (IsNativeOpen()) + { + // Navigate directly + mWebView.Navigate(_filePath); + } + else if (HtmlReady()) + { + // Write to temporary file + ReadControlMeta(); + + // Navigate to temp file + mWebView.Navigate(_localWebPath); + } + else + { + Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); + } + + // Render media shows the controls and starts timers, etc + base.RenderMedia(); + } + + /// + /// DOM content loaded event + /// + /// + /// + private void MWebView_DOMContentLoaded(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlDOMContentLoadedEventArgs e) + { + Debug.WriteLine(DateTime.Now.ToLongTimeString() + " DOM content loaded", "EdgeWebView"); + } + + /// + /// Navigation completed event - this is the last event we get and signifies the page has loaded completely + /// + /// + /// + private void MWebView_NavigationCompleted(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlNavigationCompletedEventArgs e) + { + if (e.IsSuccess) + { + Debug.WriteLine(DateTime.Now.ToLongTimeString() + " Navigate Completed to " + e.Uri, "EdgeWebView"); + + DocumentCompleted(); + + if (!Expired) + { + // Show the browser + mWebView.Visibility = System.Windows.Visibility.Visible; + } + } + else + { + Trace.WriteLine(new LogMessage("EdgeWebMedia", "Cannot navigate to " + e.Uri + ". e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); + + // This should exipre the media + Duration = 5; + base.RenderMedia(); + } + } + + /// + /// The HTML for this Widget has been updated + /// + /// + private void WebMediaHtmlUdatedEvent(string url) + { + if (mWebView != null) + { + mWebView.Navigate(url); + } + } + } +} diff --git a/Rendering/WebIe.cs b/Rendering/WebIe.cs new file mode 100644 index 00000000..53f3fe8d --- /dev/null +++ b/Rendering/WebIe.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace XiboClient.Rendering +{ + class WebIe : WebMedia + { + private WebBrowser _webBrowser; + + public WebIe(RegionOptions options) + : base(options) + { + } + + /// + /// Render Media + /// + public override void RenderMedia() + { + // Create the web view we will use + _webBrowser = new WebBrowser(); + _webBrowser.Navigated += _webBrowser_Navigated; + _webBrowser.Width = Width; + _webBrowser.Height = Height; + _webBrowser.Visibility = System.Windows.Visibility.Hidden; + + HtmlUpdatedEvent += IeWebMedia_HtmlUpdatedEvent; + + if (IsNativeOpen()) + { + // Navigate directly + _webBrowser.Navigate(_filePath); + } + else if (HtmlReady()) + { + // Write to temporary file + ReadControlMeta(); + + // Navigate to temp file + _webBrowser.Navigate(_localWebPath); + } + else + { + Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); + } + + this.MediaScene.Children.Add(_webBrowser); + + // Render media shows the controls and starts timers, etc + base.RenderMedia(); + } + + /// + /// Web Browser finished loading document + /// + /// + /// + private void _webBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) + { + DocumentCompleted(); + + if (!Expired) + { + // Show the browser + _webBrowser.Visibility = System.Windows.Visibility.Visible; + } + } + + private void IeWebMedia_HtmlUpdatedEvent(string url) + { + if (_webBrowser != null) + { + _webBrowser.Navigate(url); + } + } + } +} diff --git a/Media/WebMedia.cs b/Rendering/WebMedia.cs similarity index 96% rename from Media/WebMedia.cs rename to Rendering/WebMedia.cs index 516e307c..e36daad1 100644 --- a/Media/WebMedia.cs +++ b/Rendering/WebMedia.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Xibo Signage Ltd + * Copyright (C) 2020 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -21,15 +21,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Windows.Forms; +using System.Threading.Tasks; -namespace XiboClient +namespace XiboClient.Rendering { abstract class WebMedia : Media { @@ -69,7 +68,7 @@ abstract class WebMedia : Media /// /// public WebMedia(RegionOptions options) - : base(options.width, options.height, options.top, options.left) + : base(options) { // Collect some options from the Region Options passed in // and store them in member variables. @@ -229,7 +228,7 @@ protected bool HtmlReady() // Refresh the local file cache with any new dimensions, etc. UpdateCacheIfNecessary(); - + return true; } @@ -330,8 +329,8 @@ private void xmds_GetResourceCompleted(object sender, XiboClient.xmds.GetResourc } string html = cachedFile.Replace("", ""); - html = html.Replace("[[ViewPortWidth]]", _width.ToString()); - html += ""; + html = html.Replace("[[ViewPortWidth]]", Width.ToString()); + html += ""; html += ""; // Comment in to write out the update date at the end of the file (in the body) @@ -390,7 +389,7 @@ private void UpdateCacheIfNecessary() // Compare the cached dimensions in the file with the dimensions now, and // regenerate if they are different. - if (cachedFile.Contains("[[ViewPortWidth]]") || !ReadCachedViewPort(cachedFile).Equals(_width.ToString() + "x" + _height.ToString())) + if (cachedFile.Contains("[[ViewPortWidth]]") || !ReadCachedViewPort(cachedFile).Equals(Width.ToString() + "x" + Height.ToString())) { // Regex out the existing replacement if present cachedFile = Regex.Replace(cachedFile, "(.*)", ""); @@ -412,8 +411,8 @@ private void UpdateCacheIfNecessary() } string html = cachedFile.Replace("", ""); - html = html.Replace("[[ViewPortWidth]]", _width.ToString()); - html += ""; + html = html.Replace("[[ViewPortWidth]]", Width.ToString()); + html += ""; html += ""; // Write to the library @@ -464,11 +463,11 @@ public static WebMedia GetConfiguredWebMedia(RegionOptions options) WebMedia media; if (ApplicationSettings.Default.BrowserType.Equals("edge", StringComparison.InvariantCultureIgnoreCase)) { - media = new EdgeWebMedia(options); + media = new WebEdge(options); } else { - media = new IeWebMedia(options); + media = new WebIe(options); } return media; } diff --git a/Resources/licence.txt b/Resources/licence.txt index 62263dba..01d5cc4e 100644 --- a/Resources/licence.txt +++ b/Resources/licence.txt @@ -1,5 +1,5 @@ Xibo - Digital Signage - http://www.xibo.org.uk -Copyright (C) 2006-2017 Spring Signage Ltd +Copyright (C) 2006-2017 Xibo Signage Ltd Xibo is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/Stats/Stat.cs b/Stats/Stat.cs new file mode 100644 index 00000000..02ddf318 --- /dev/null +++ b/Stats/Stat.cs @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Stats +{ + public class Stat + { + public StatType type; + public String fromDate; + public String toDate; + public int layoutID; + public int scheduleID; + public String mediaID; + public String tag; + + /// + /// Is this Stat enabled (if false it will not be recorded) + /// + public bool isEnabled = true; + + public override string ToString() + { + // Format the message into the expected XML sub nodes. + // Just do this with a string builder rather than an XML builder. + String theMessage; + + theMessage = String.Format("", type, fromDate, toDate, layoutID.ToString(), scheduleID.ToString(), mediaID); + + return theMessage; + } + } + + public enum StatType { Layout, Media, Event }; +} diff --git a/Log/StatLog.cs b/Stats/StatLog.cs similarity index 74% rename from Log/StatLog.cs rename to Stats/StatLog.cs index 3abcc678..252510be 100644 --- a/Log/StatLog.cs +++ b/Stats/StatLog.cs @@ -1,6 +1,6 @@ /* * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2009-2015 Spring Signage Ltd + * Copyright (C) 2020 Xibo Signage Ltd * * This file is part of Xibo. * @@ -22,26 +22,27 @@ using System.Collections.ObjectModel; using System.Text; using System.IO; -using System.Windows.Forms; using System.Xml; using System.Diagnostics; using System.Threading; using System.Net; -namespace XiboClient +namespace XiboClient.Stats { - class StatLog + public sealed class StatLog { - public static object _locker = new object(); + private static readonly Lazy + lazy = + new Lazy + (() => new StatLog()); + + public static StatLog Instance { get { return lazy.Value; } } + private Collection _stats; - private HardwareKey _hardwareKey; - public StatLog() + private StatLog() { _stats = new Collection(); - - // Get the key for this display - _hardwareKey = new HardwareKey(); } /// @@ -119,33 +120,4 @@ private void FlushToFile() Debug.WriteLine(new LogMessage("FlushToFile", String.Format("OUT")), LogType.Audit.ToString()); } } - - class Stat - { - public StatType type; - public String fromDate; - public String toDate; - public int layoutID; - public int scheduleID; - public String mediaID; - public String tag; - - /// - /// Is this Stat enabled (if false it will not be recorded) - /// - public bool isEnabled = true; - - public override string ToString() - { - // Format the message into the expected XML sub nodes. - // Just do this with a string builder rather than an XML builder. - String theMessage; - - theMessage = String.Format("", type, fromDate, toDate, layoutID.ToString(), scheduleID.ToString(), mediaID); - - return theMessage; - } - } - - public enum StatType { Layout, Media, Event }; } diff --git a/Web References/xmds/Reference.cs b/Web References/xmds/Reference.cs index c34cbb91..67c0cebc 100644 --- a/Web References/xmds/Reference.cs +++ b/Web References/xmds/Reference.cs @@ -55,7 +55,7 @@ public partial class xmds : System.Web.Services.Protocols.SoapHttpClientProtocol /// public xmds() { - this.Url = "http://192.168.0.28/xmds.php?v=5"; + this.Url = global::XiboClient.Properties.Settings.Default.XiboClient_xmds_xmds; if ((this.IsLocalFileSystemWebService(this.Url) == true)) { this.UseDefaultCredentials = true; this.useDefaultCredentialsSetExplicitly = false; diff --git a/Web References/xmds/Reference.map b/Web References/xmds/Reference.map index 9933f18d..b4eab380 100644 --- a/Web References/xmds/Reference.map +++ b/Web References/xmds/Reference.map @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/Web References/xmds/xmds.wsdl b/Web References/xmds/service_v5.wsdl similarity index 99% rename from Web References/xmds/xmds.wsdl rename to Web References/xmds/service_v5.wsdl index ef22c111..1adda03d 100644 --- a/Web References/xmds/xmds.wsdl +++ b/Web References/xmds/service_v5.wsdl @@ -267,7 +267,7 @@ - + \ No newline at end of file diff --git a/XiboClient.csproj b/XiboClient.csproj index 68ad093e..d8c514f9 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -1,47 +1,25 @@  - + + + Debug AnyCPU - 8.0.50727 - 2.0 - {9E6CFE88-0171-4360-BDF4-84D74A90FD55} + {88215F84-47F4-4A74-B413-BC3A1443A538} WinExe - Properties XiboClient XiboClient - 3F1DD8562B79F6E24F700A1576DB3897470C4CB4 - XiboClient_TemporaryKey.pfx - false - false - false - XiboClient.Program - LocalIntranet - - - 3.5 v4.7.2 - - - new-icon.ico - - - T:\Development\Version 2\Client\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU true full false @@ -49,282 +27,232 @@ DEBUG;TRACE prompt 4 - false + AnyCPU pdbonly true bin\Release\ TRACE prompt 4 - false - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - prompt - false - false + + new-icon.ico - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - prompt - true - false + + XiboClient.App packages\AsyncIO.0.1.69\lib\net40\AsyncIO.dll - - False - wmpdll\AxInterop.WMPLib.dll - False - packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll - - False - False - wmpdll\Interop.WMPLib.dll + + packages\EmbedIO.3.3.3\lib\netstandard2.0\EmbedIO.dll - - - packages\Microsoft.Toolkit.Forms.UI.Controls.WebView.5.1.1\lib\net462\Microsoft.Toolkit.Forms.UI.Controls.WebView.dll + + packages\Microsoft.Toolkit.Wpf.UI.Controls.WebView.6.0.0\lib\net462\Microsoft.Toolkit.Wpf.UI.Controls.WebView.dll packages\NetMQ.4.0.0.207\lib\net40\NetMQ.dll - packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll packages\NodaTime.2.4.7\lib\net45\NodaTime.dll + + packages\Unosquare.Swan.2.4.3\lib\net461\Swan.dll + + + packages\Unosquare.Swan.Lite.2.6.1\lib\net461\Swan.Lite.dll + - - - + - - packages\System.Runtime.CompilerServices.Unsafe.4.6.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - + - - packages\System.Text.Encoding.CodePages.4.6.0\lib\net461\System.Text.Encoding.CodePages.dll + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - - - packages\EmbedIO.2.9.2\lib\netstandard2.0\Unosquare.Labs.EmbedIO.dll - - - packages\Unosquare.Swan.Lite.1.3.1\lib\net461\Unosquare.Swan.Lite.dll + + + + + + + 4.0 + + - + + MSBuild:Compile + Designer + + + + + + - - Form - - - About.cs - + - - - + + - + - + + + + - - - Form - - - Form - - - Form - - - Form - - - Form - - - Form - - - Form - - - - - Form - - - Form - - - ClientInfo.cs - + - - Form + + + + + Media.xaml - - MainForm.cs + + + + + + + + Layout.xaml - - Form + + Region.xaml - + + + True + True + Reference.map - - Form + + OptionsForm.xaml + + + + + + + + + MSBuild:Compile + Designer + + + App.xaml + Code - - OptionForm.cs + + MainWindow.xaml + Code - - - + Designer - MainForm.cs - - + MSBuild:Compile + + Designer - OptionForm.cs - - - ResXFileCodeGenerator - Resources.Designer.cs + MSBuild:Compile + + Designer - + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + Code + True - Resources.resx True + Resources.resx + + + True + Settings.settings + True - - + + ResXFileCodeGenerator + Resources.Designer.cs + + + + SettingsSingleFileGenerator Settings.Designer.cs + MSDiscoCodeGenerator Reference.cs - - - True - Settings.settings - True - - - Component - - - - - - Form - - - - - - Form - - - Form - - - VideoPlayer.cs - - - True - True - Reference.map - - - - - - - + - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 2.0 %28x86%29 - true - - - False - .NET Framework 3.0 %28x86%29 - false - - - False - .NET Framework 3.5 - false - - - False - .NET Framework 3.5 SP1 - false - + - - Static + + + + + + + + + + + + + Dynamic Web References\xmds\ - http://192.168.0.28/xmds.php%3fv=5&WSDL + https://raw.githubusercontent.com/xibosignage/xibo-cms/develop/lib/Xmds/service_v5.wsdl @@ -333,45 +261,20 @@ XiboClient_xmds_xmds - - - Designer - About.cs - - - Designer - VideoPlayer.cs - - - - - - - - Designer - ClientInfo.cs - - - - - - - Always - Designer - - - - - - + echo F|xcopy /Y "$(TargetPath)" "$(TargetDir)\Xibo.scr" - + + + \ No newline at end of file diff --git a/XiboClient.csproj.user b/XiboClient.csproj.user deleted file mode 100644 index f47e3196..00000000 --- a/XiboClient.csproj.user +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - T:\Development\Version 2\Client\ - - - - - - en-US - false - - \ No newline at end of file diff --git a/XiboClient.sln b/XiboClient.sln index 8a54b7ae..b87d5219 100644 --- a/XiboClient.sln +++ b/XiboClient.sln @@ -1,26 +1,25 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Express 2012 for Windows Desktop -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XiboClient", "XiboClient.csproj", "{9E6CFE88-0171-4360-BDF4-84D74A90FD55}" +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29728.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XiboClient", "XiboClient.csproj", "{88215F84-47F4-4A74-B413-BC3A1443A538}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Debug|x86.ActiveCfg = Debug|x86 - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Debug|x86.Build.0 = Debug|x86 - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Release|Any CPU.Build.0 = Release|Any CPU - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Release|x86.ActiveCfg = Release|x86 - {9E6CFE88-0171-4360-BDF4-84D74A90FD55}.Release|x86.Build.0 = Release|x86 + {88215F84-47F4-4A74-B413-BC3A1443A538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88215F84-47F4-4A74-B413-BC3A1443A538}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88215F84-47F4-4A74-B413-BC3A1443A538}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88215F84-47F4-4A74-B413-BC3A1443A538}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6C5AE064-E1EF-45E7-9EDF-7C795DE161E8} + EndGlobalSection EndGlobal diff --git a/XmdsAgents/FileAgent.cs b/XmdsAgents/FileAgent.cs index 124a9850..7c7498c5 100644 --- a/XmdsAgents/FileAgent.cs +++ b/XmdsAgents/FileAgent.cs @@ -1,6 +1,6 @@ /* * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2018 Spring Signage Ltd + * Copyright (C) 2006 - 2018 Xibo Signage Ltd * * This file is part of Xibo. * @@ -27,11 +27,6 @@ using System.Net; using System.Net.Mime; -/// 17/02/12 Dan Created -/// 21/02/12 Dan Added OnComplete Delegate and Event -/// 28/02/12 Dan Added OnPartComplete Delegate and Event -/// 22/04/12 Dan Dispose of XMDS between each request - namespace XiboClient.XmdsAgents { class FileAgent diff --git a/XmdsAgents/LogAgent.cs b/XmdsAgents/LogAgent.cs index 4271b9a5..eda09924 100644 --- a/XmdsAgents/LogAgent.cs +++ b/XmdsAgents/LogAgent.cs @@ -1,6 +1,6 @@ /* * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2016 Daniel Garner + * Copyright (C) 2020 Xibo Signage Ltd * * This file is part of Xibo. * diff --git a/XmdsAgents/ScheduleAgent.cs b/XmdsAgents/ScheduleAgent.cs index c19011d1..a3908193 100644 --- a/XmdsAgents/ScheduleAgent.cs +++ b/XmdsAgents/ScheduleAgent.cs @@ -1,6 +1,6 @@ /* * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2012 Daniel Garner + * Copyright (C) 2020 Xibo Signage Ltd * * This file is part of Xibo. * diff --git a/app.config b/app.config index 788bfbed..25cde2a1 100644 --- a/app.config +++ b/app.config @@ -1,17 +1,30 @@ - + - - - - - - + + +
+ + + + + + + + + + + + + {{XMDS_LOCATION}} + + + - - + + - + \ No newline at end of file diff --git a/default.config.xml b/default.config.xml index 64d27b27..edcc42e3 100644 --- a/default.config.xml +++ b/default.config.xml @@ -53,5 +53,4 @@ 9696 0 - edge \ No newline at end of file diff --git a/packages.config b/packages.config index bca34805..0af3eb40 100644 --- a/packages.config +++ b/packages.config @@ -2,12 +2,15 @@ - - + + + - + - - - + + + + + \ No newline at end of file diff --git a/wmpdll/AxInterop.WMPLib.dll b/wmpdll/AxInterop.WMPLib.dll deleted file mode 100644 index fde591874c0fc69ae655196a883b586e6278338d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 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

H}u-+Y)F`lciF%_Y>(HyxpG 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 From 387ec352dda0a6570e90189e293b0cf63c4848a4 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 5 Feb 2020 18:55:02 +0000 Subject: [PATCH 06/29] WIP: Various refactoring and debugging. Try CEF instead of Edge WebView (seems better and loads transparently). --- Action/XmrSubscriber.cs | 24 ++---- Logic/RequiredFiles.cs | 12 +-- Logic/Schedule.cs | 13 ++-- Logic/ScheduleManager.cs | 34 +++----- MainWindow.xaml.cs | 46 ++++++++--- Properties/Settings.Designer.cs | 2 +- Properties/Settings.settings | 2 +- Rendering/Audio.cs | 22 +++++- Rendering/Layout.xaml.cs | 25 ++++-- Rendering/Media.xaml.cs | 2 + Rendering/Region.xaml.cs | 8 +- Rendering/Transitions.cs | 12 ++- Rendering/WebCef.cs | 115 ++++++++++++++++++++++++++++ Rendering/WebMedia.cs | 23 ++---- XiboClient.csproj | 97 ++++++++++++----------- XiboClient.sln | 6 ++ XmdsAgents/FileAgent.cs | 9 ++- XmdsAgents/ScheduleAndFilesAgent.cs | 90 ++++++++-------------- app.config | 16 ++-- default.config.xml | 1 + 20 files changed, 345 insertions(+), 214 deletions(-) create mode 100644 Rendering/WebCef.cs diff --git a/Action/XmrSubscriber.cs b/Action/XmrSubscriber.cs index dc766611..ee06800c 100644 --- a/Action/XmrSubscriber.cs +++ b/Action/XmrSubscriber.cs @@ -42,18 +42,6 @@ public HardwareKey HardwareKey } private HardwareKey _hardwareKey; - /// - /// Client Info Form - /// - public ClientInfo ClientInfoForm - { - set - { - _clientInfoForm = value; - } - } - private ClientInfo _clientInfoForm; - /// /// The MQ Poller /// @@ -115,7 +103,7 @@ public void Run() socket.ReceiveReady += _socket_ReceiveReady; // Notify - _clientInfoForm.XmrSubscriberStatus = "Connected to " + ApplicationSettings.Default.XmrNetworkAddress + ". Waiting for messages."; + ClientInfo.Instance.XmrSubscriberStatus = "Connected to " + ApplicationSettings.Default.XmrNetworkAddress + ". Waiting for messages."; // Sit and wait, processing messages, indefinitely or until we are interrupted. _poller.Run(); @@ -135,11 +123,11 @@ public void Run() catch (Exception e) { Trace.WriteLine(new LogMessage("XmrSubscriber - Run", "Unable to Subscribe: " + e.Message), LogType.Info.ToString()); - _clientInfoForm.XmrSubscriberStatus = e.Message; + ClientInfo.Instance.XmrSubscriberStatus = e.Message; } // Update status - _clientInfoForm.XmrSubscriberStatus = "Disconnected, waiting to reconnect, last activity: " + LastHeartBeat.ToString(); + ClientInfo.Instance.XmrSubscriberStatus = "Disconnected, waiting to reconnect, last activity: " + LastHeartBeat.ToString(); // Sleep for 60 seconds. _manualReset.WaitOne(60 * 1000); @@ -169,7 +157,7 @@ private void _socket_ReceiveReady(object sender, NetMQSocketEventArgs e) // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("XmrSubscriber - _socket_ReceiveReady", "Exception in Run: " + ex.Message), LogType.Error.ToString()); Trace.WriteLine(new LogMessage("XmrSubscriber - _socket_ReceiveReady", e.ToString()), LogType.Audit.ToString()); - _clientInfoForm.XmrSubscriberStatus = "Error. " + ex.Message; + ClientInfo.Instance.XmrSubscriberStatus = "Error. " + ex.Message; } } @@ -182,7 +170,7 @@ private void processMessage(NetMQMessage message, AsymmetricCipherKeyPair rsaKey string statusMessage = "Connected (" + ApplicationSettings.Default.XmrNetworkAddress + "), last activity: " + DateTime.Now.ToString(); // Write this out to a log - _clientInfoForm.XmrSubscriberStatus = statusMessage; + ClientInfo.Instance.XmrSubscriberStatus = statusMessage; Trace.WriteLine(new LogMessage("XmrSubscriber - Run", statusMessage), LogType.Audit.ToString()); // Deal with heart beat @@ -254,7 +242,7 @@ private void processMessage(NetMQMessage message, AsymmetricCipherKeyPair rsaKey case "screenShot": ScreenShot.TakeAndSend(); - _clientInfoForm.notifyStatusToXmds(); + ClientInfo.Instance.notifyStatusToXmds(); break; default: diff --git a/Logic/RequiredFiles.cs b/Logic/RequiredFiles.cs index 76ef0672..841e1920 100644 --- a/Logic/RequiredFiles.cs +++ b/Logic/RequiredFiles.cs @@ -87,12 +87,12 @@ public RequiredFiles() RequiredFileList = new Collection(); // Create a webservice call - _report = new XiboClient.xmds.xmds(); - - // Start up the Xmds Service Object - _report.Credentials = null; - _report.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=mediaInventory"; - _report.UseDefaultCredentials = false; + _report = new xmds.xmds + { + Credentials = null, + Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=mediaInventory", + UseDefaultCredentials = false + }; } /// diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index c9ce5847..b3c57185 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2014 Daniel Garner +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -34,9 +35,6 @@ using XiboClient.Action; using XiboClient.Control; -/// 17/02/12 Dan Removed Schedule call, introduced ScheduleAgent -/// 21/02/12 Dan Named the threads - namespace XiboClient { /// @@ -155,7 +153,6 @@ public Schedule(string scheduleLocation) // Create a RequiredFilesAgent _scheduleAndRfAgent = new ScheduleAndFilesAgent(); - _scheduleAndRfAgent.CurrentCacheManager = CacheManager.Instance; _scheduleAndRfAgent.CurrentScheduleManager = _scheduleManager; _scheduleAndRfAgent.ScheduleLocation = scheduleLocation; _scheduleAndRfAgent.HardwareKey = _hardwareKey.Key; diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index 353dff45..f766bd10 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006-2016 Daniel Garner +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -33,10 +34,6 @@ using System.Globalization; using XiboClient.Action; -/// 17/02/12 Dan Added a static method to get the schedule XML from disk into a string and to write it to the disk -/// 20/02/12 Dan Tweaked log types on a few trace messages -/// 24/03/12 Dan Move onto its own thread - namespace XiboClient { /// @@ -80,18 +77,6 @@ class ScheduleManager /// private int _currenctLayoutId; - /// - /// Client Info Form - /// - public ClientInfo ClientInfoForm - { - set - { - _clientInfoForm = value; - } - } - private ClientInfo _clientInfoForm; - /// /// Creates a new schedule Manager /// @@ -218,7 +203,7 @@ public void Run() OnRefreshSchedule(); // Update the client info form - _clientInfoForm.ScheduleManagerStatus = LayoutsInSchedule(); + ClientInfo.Instance.ScheduleManagerStatus = LayoutsInSchedule(); // Do we need to take a screenshot? if (ApplicationSettings.Default.ScreenShotRequestInterval > 0 && DateTime.Now > _lastScreenShotDate.AddMinutes(ApplicationSettings.Default.ScreenShotRequestInterval)) @@ -230,7 +215,7 @@ public void Run() _lastScreenShotDate = DateTime.Now; // Notify status to XMDS - _clientInfoForm.notifyStatusToXmds(); + ClientInfo.Instance.notifyStatusToXmds(); } // Run any commands that occur in the next 10 seconds. @@ -260,13 +245,12 @@ public void Run() { // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("ScheduleManager - Run", "Exception in Run: " + ex.Message), LogType.Error.ToString()); - _clientInfoForm.ScheduleStatus = "Error. " + ex.Message; + ClientInfo.Instance.ScheduleStatus = "Error. " + ex.Message; } } // Completed this check - if (OnScheduleManagerCheckComplete != null) - OnScheduleManagerCheckComplete(); + OnScheduleManagerCheckComplete?.Invoke(); // Sleep this thread for 10 seconds _manualReset.WaitOne(10 * 1000); diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 4ac20490..5733c2c7 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -52,6 +52,7 @@ using System.Windows.Threading; using XiboClient.Rendering; using XiboClient.Stats; +using System.Windows.Media.Animation; namespace XiboClient { @@ -82,7 +83,12 @@ public partial class MainWindow : Window private int _scheduleId; private int _layoutId; private bool _screenSaver = false; + + /// + /// Splash Screen Logic + /// private bool _showingSplash = false; + private System.Windows.Controls.Image splashScreen; /// /// Delegates to Invoke various actions after yielding @@ -175,8 +181,6 @@ private void InitializeXibo() // Set the title Title = ApplicationSettings.GetProductNameFromAssembly(); - Thread.CurrentThread.Name = "UI Thread"; - // Check the directories exist if (!Directory.Exists(ApplicationSettings.Default.LibraryPath + @"\backgrounds\")) { @@ -489,9 +493,11 @@ private void ChangeToNextLayout(string layoutPath) // Destroy the Current Layout try { - currentLayout.Stop(); - - DestroyLayout(); + if (this.currentLayout != null) + { + this.currentLayout.Stop(); + DestroyLayout(); + } } catch (Exception e) { @@ -508,11 +514,16 @@ private void ChangeToNextLayout(string layoutPath) currentLayout = PrepareLayout(layoutPath, false); // We have loaded a layout background and therefore are no longer showing the splash screen - _showingSplash = false; + // Remove the Splash Screen Image + RemoveSplashScreen(); // Add this Layout to our controls this.Scene.Children.Add(currentLayout); + // Start + currentLayout.Start(); + + // Update client info ClientInfo.Instance.CurrentLayoutId = layoutPath; _schedule.CurrentLayoutId = _layoutId; } @@ -590,6 +601,7 @@ void splashScreenTimer_Tick(object sender, EventArgs e) DispatcherTimer timer = (DispatcherTimer)sender; timer.Stop(); + // Put the next Layout up _schedule.NextLayout(); } @@ -663,12 +675,23 @@ private void ShowSplashScreen() private void ShowDefaultSplashScreen() { Uri path = new Uri("pack://application:,,,/Resources/splash.jpg"); - System.Windows.Controls.Image img = new System.Windows.Controls.Image() + this.splashScreen = new System.Windows.Controls.Image() { - Name = "Splash" + Name = "Splash", + Source = new BitmapImage(path) }; - img.Source = new BitmapImage(path); - this.Scene.Children.Add(img); + this.Scene.Children.Add(this.splashScreen); + } + + /// + /// Remove the Splash Screen + /// + private void RemoveSplashScreen() + { + this.Scene.Children.Remove(this.splashScreen); + + // We've removed it + this._showingSplash = false; } /// @@ -835,6 +858,9 @@ public void ManageOverlays(Collection overlays) // Add to the Scene OverlayScene.Children.Add(layout); + + // Start + currentLayout.Start(); } catch (DefaultLayoutException) { diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index 0ed9d715..c47d68c2 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -26,7 +26,7 @@ public static Settings Default { [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("{{XMDS_LOCATION}}")] + [global::System.Configuration.DefaultSettingValueAttribute("http://localhost/xmds.php?v=5")] public string XiboClient_xmds_xmds { get { return ((string)(this["XiboClient_xmds_xmds"])); diff --git a/Properties/Settings.settings b/Properties/Settings.settings index 09f5b37c..13516905 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -3,7 +3,7 @@ - {{XMDS_LOCATION}} + http://localhost/xmds.php?v=5 \ No newline at end of file diff --git a/Rendering/Audio.cs b/Rendering/Audio.cs index 442370c3..f539f104 100644 --- a/Rendering/Audio.cs +++ b/Rendering/Audio.cs @@ -1,4 +1,24 @@ -using System; +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 47b3d469..a375aa1e 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -261,14 +261,7 @@ public void loadFromFile(string layoutPath, int layoutId, int scheduleId, bool i } } - // Create a start record for this layout - _stat = new Stat(); - _stat.type = StatType.Layout; - _stat.scheduleID = _scheduleId; - _stat.layoutID = _layoutId; - _stat.fromDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - _stat.isEnabled = isStatEnabled; - + // Parse the regions foreach (XmlNode region in listRegions) { // Is there any media @@ -342,6 +335,22 @@ public void loadFromFile(string layoutPath, int layoutId, int scheduleId, bool i listMedia = null; } + public void Start() + { + // Create a start record for this layout + _stat = new Stat(); + _stat.type = StatType.Layout; + _stat.scheduleID = _scheduleId; + _stat.layoutID = _layoutId; + _stat.fromDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + _stat.isEnabled = isStatEnabled; + + foreach (Region region in _regions) + { + region.Start(); + } + } + public void Stop() { if (_stat != null) diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index be0db414..074e2231 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -65,6 +65,8 @@ public partial class Media : UserControl protected DispatcherTimer _timer; private bool _timerStarted = false; + protected bool IsRenderCalled { get; set; } + /// /// The Region Options /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 54eccfbc..777c9a03 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -188,6 +188,8 @@ private void StartNext() _sizeResetRequired = false; } + Debug.WriteLine("Calling start on media in regionId " + this.options.regionId, "Region"); + StartMedia(newMedia); } catch (Exception ex) @@ -654,12 +656,12 @@ private void StartMedia(Media media) { Trace.WriteLine(new LogMessage("Region - StartMedia", "Starting media"), LogType.Audit.ToString()); - // Render the media, this adds the child controls to the Media UserControls grid - media.RenderMedia(); - // Add to this scene this.RegionScene.Children.Add(media); + // Render the media, this adds the child controls to the Media UserControls grid + media.RenderMedia(); + // Reset the audio sequence and start _audioSequence = 1; startAudio(); diff --git a/Rendering/Transitions.cs b/Rendering/Transitions.cs index 5d0207e4..bf9499d4 100644 --- a/Rendering/Transitions.cs +++ b/Rendering/Transitions.cs @@ -18,7 +18,7 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ - using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -59,6 +59,16 @@ public static void MoveAnimation(object item, DependencyProperty dp, string type } } + public static DoubleAnimation Get(string type, double duration) + { + return new DoubleAnimation + { + From = 0, + To = 1, + Duration = TimeSpan.FromMilliseconds(duration) + }; + } + /// /// Fade in animation /// diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs new file mode 100644 index 00000000..cde82bac --- /dev/null +++ b/Rendering/WebCef.cs @@ -0,0 +1,115 @@ +using CefSharp.Wpf; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace XiboClient.Rendering +{ + class WebCef : WebMedia + { + private ChromiumWebBrowser webView; + private string regionId; + + public WebCef(RegionOptions options) + : base(options) + { + this.regionId = options.regionId; + } + + /// + /// Render Media + /// + public override void RenderMedia() + { + // Create the web view we will use + webView = new ChromiumWebBrowser() + { + Name = "region_" + this.regionId + }; + + webView.Visibility = System.Windows.Visibility.Hidden; + webView.Loaded += WebView_Loaded; + webView.LoadError += WebView_LoadError; + + this.MediaScene.Children.Add(webView); + + HtmlUpdatedEvent += WebMediaHtmlUdatedEvent; + + if (IsNativeOpen()) + { + // Navigate directly + webView.Address = _filePath; + } + else if (HtmlReady()) + { + // Write to temporary file + ReadControlMeta(); + + // Navigate to temp file + webView.Address = _localWebPath; + } + else + { + Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); + } + + // Render media shows the controls and starts timers, etc + base.RenderMedia(); + } + + /// + /// Navigation completed event - this is the last event we get and signifies the page has loaded completely + /// + /// + /// + private void WebView_Loaded(object sender, System.Windows.RoutedEventArgs e) + { + Debug.WriteLine(DateTime.Now.ToLongTimeString() + " Navigate Completed", "EdgeWebView"); + + // We've finished rendering the document + DocumentCompleted(); + + // If we aren't expired yet, we should show it + if (!Expired) + { + // Show the browser after some time + webView.Visibility = System.Windows.Visibility.Visible; + } + } + + private void WebView_LoadError(object sender, CefSharp.LoadErrorEventArgs e) + { + Trace.WriteLine(new LogMessage("EdgeWebMedia", "Cannot navigate. e = " + e.ToString()), LogType.Error.ToString()); + + // This should exipre the media + Duration = 5; + base.RenderMedia(); + } + + /// + /// The HTML for this Widget has been updated + /// + /// + private void WebMediaHtmlUdatedEvent(string url) + { + if (webView != null && !Expired) + { + webView.Address = url; + } + } + + public override void Stop() + { + this.webView.Loaded -= WebView_Loaded; + this.webView.LoadError -= WebView_LoadError; + this.webView.Dispose(); + + base.Stop(); + } + } +} diff --git a/Rendering/WebMedia.cs b/Rendering/WebMedia.cs index e36daad1..8154f9c3 100644 --- a/Rendering/WebMedia.cs +++ b/Rendering/WebMedia.cs @@ -316,7 +316,7 @@ private void xmds_GetResourceCompleted(object sender, XiboClient.xmds.GetResourc string cachedFile = e.Result; // Handle the background - String bodyStyle; + /*String bodyStyle; String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); if (_options.backgroundImage == null || _options.backgroundImage == "") @@ -326,9 +326,9 @@ private void xmds_GetResourceCompleted(object sender, XiboClient.xmds.GetResourc else { bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; - } + }*/ - string html = cachedFile.Replace("", ""); + string html = cachedFile.Replace("", ""); html = html.Replace("[[ViewPortWidth]]", Width.ToString()); html += ""; html += ""; @@ -397,20 +397,7 @@ private void UpdateCacheIfNecessary() cachedFile = Regex.Replace(cachedFile, "", ""); cachedFile = Regex.Replace(cachedFile, "", ""); - // Handle the background - String bodyStyle; - String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); - - if (_options.backgroundImage == null || _options.backgroundImage == "") - { - bodyStyle = "background-color:" + backgroundColor + " ;"; - } - else - { - bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; - } - - string html = cachedFile.Replace("", ""); + string html = cachedFile.Replace("", ""); html = html.Replace("[[ViewPortWidth]]", Width.ToString()); html += ""; html += ""; @@ -463,7 +450,7 @@ public static WebMedia GetConfiguredWebMedia(RegionOptions options) WebMedia media; if (ApplicationSettings.Default.BrowserType.Equals("edge", StringComparison.InvariantCultureIgnoreCase)) { - media = new WebEdge(options); + media = new WebCef(options); } else { diff --git a/XiboClient.csproj b/XiboClient.csproj index d8c514f9..db9bd06e 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -1,6 +1,5 @@  - Debug @@ -43,34 +42,29 @@ XiboClient.App + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + 7.3 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + 7.3 + prompt + MinimumRecommendedRules.ruleset + true + - - packages\AsyncIO.0.1.69\lib\net40\AsyncIO.dll - - - packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll - - - packages\EmbedIO.3.3.3\lib\netstandard2.0\EmbedIO.dll - - - packages\Microsoft.Toolkit.Wpf.UI.Controls.WebView.6.0.0\lib\net462\Microsoft.Toolkit.Wpf.UI.Controls.WebView.dll - - - packages\NetMQ.4.0.0.207\lib\net40\NetMQ.dll - - - packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll - - - packages\NodaTime.2.4.7\lib\net45\NodaTime.dll - - - packages\Unosquare.Swan.2.4.3\lib\net461\Swan.dll - - - packages\Unosquare.Swan.Lite.2.6.1\lib\net461\Swan.Lite.dll - @@ -83,9 +77,6 @@ - - packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - @@ -143,7 +134,7 @@ Media.xaml - + @@ -219,7 +210,6 @@ - SettingsSingleFileGenerator Settings.Designer.cs @@ -261,20 +251,37 @@ XiboClient_xmds_xmds + + + 1.8.5 + + + 79.1.350 + + + 3.3.3 + + + 4.0.0.207 + + + 12.0.3 + + + 2.4.7 + + + 4.7.0 + + + 2.4.3 + + + 2.6.1 + + echo F|xcopy /Y "$(TargetPath)" "$(TargetDir)\Xibo.scr" - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - \ No newline at end of file diff --git a/XiboClient.sln b/XiboClient.sln index b87d5219..be862fc9 100644 --- a/XiboClient.sln +++ b/XiboClient.sln @@ -8,13 +8,19 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {88215F84-47F4-4A74-B413-BC3A1443A538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {88215F84-47F4-4A74-B413-BC3A1443A538}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88215F84-47F4-4A74-B413-BC3A1443A538}.Debug|x86.ActiveCfg = Debug|x86 + {88215F84-47F4-4A74-B413-BC3A1443A538}.Debug|x86.Build.0 = Debug|x86 {88215F84-47F4-4A74-B413-BC3A1443A538}.Release|Any CPU.ActiveCfg = Release|Any CPU {88215F84-47F4-4A74-B413-BC3A1443A538}.Release|Any CPU.Build.0 = Release|Any CPU + {88215F84-47F4-4A74-B413-BC3A1443A538}.Release|x86.ActiveCfg = Release|x86 + {88215F84-47F4-4A74-B413-BC3A1443A538}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/XmdsAgents/FileAgent.cs b/XmdsAgents/FileAgent.cs index 7c7498c5..c481cc0c 100644 --- a/XmdsAgents/FileAgent.cs +++ b/XmdsAgents/FileAgent.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2018 Xibo Signage Ltd +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of diff --git a/XmdsAgents/ScheduleAndFilesAgent.cs b/XmdsAgents/ScheduleAndFilesAgent.cs index 7da98167..7cab8982 100644 --- a/XmdsAgents/ScheduleAndFilesAgent.cs +++ b/XmdsAgents/ScheduleAndFilesAgent.cs @@ -1,13 +1,14 @@ -/* - * Xibo - Digitial Signage - http://www.xibo.org.uk - * Copyright (C) 2006 - 2015 Daniel Garner +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or - * any later version. + * any later version. * * Xibo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -91,30 +92,6 @@ public string HardwareKey } private string _hardwareKey; - /// - /// The Current CacheManager for this Xibo Client - /// - public CacheManager CurrentCacheManager - { - set - { - _cacheManager = value; - } - } - private CacheManager _cacheManager; - - /// - /// Client Info Form - /// - public ClientInfo ClientInfoForm - { - set - { - _clientInfoForm = value; - } - } - private ClientInfo _clientInfoForm; - /// /// Required Files Agent /// @@ -169,13 +146,13 @@ public void Run() // If we are currently downloading something, we have to wait if (filesToDownload > 0) { - _clientInfoForm.RequiredFilesStatus = string.Format("Waiting: {0} Active Downloads", filesToDownload.ToString()); + ClientInfo.Instance.RequiredFilesStatus = string.Format("Waiting: {0} Active Downloads", filesToDownload.ToString()); Trace.WriteLine(new LogMessage("RequiredFilesAgent - Run", "Currently Downloading Files, skipping collect"), LogType.Audit.ToString()); } else { - _clientInfoForm.RequiredFilesStatus = "Running: Requesting connection to Xibo Server"; + ClientInfo.Instance.RequiredFilesStatus = "Running: Requesting connection to Xibo Server"; using (xmds.xmds xmds = new xmds.xmds()) { @@ -189,7 +166,7 @@ public void Run() // Set the flag to indicate we have a connection to XMDS ApplicationSettings.Default.XmdsLastConnection = DateTime.Now; - _clientInfoForm.RequiredFilesStatus = "Running: Data received from Xibo Server"; + ClientInfo.Instance.RequiredFilesStatus = "Running: Data received from Xibo Server"; // Load the XML file RF call XmlDocument xml = new XmlDocument(); @@ -197,7 +174,7 @@ public void Run() // Create a required files object and set it to contain the RF returned this tick _requiredFiles = new RequiredFiles(); - _requiredFiles.CurrentCacheManager = _cacheManager; + _requiredFiles.CurrentCacheManager = CacheManager.Instance; _requiredFiles.RequiredFilesXml = xml; // List of Threads to start @@ -239,7 +216,7 @@ public void Run() _requiredFiles.WriteRequiredFiles(); // Write the Cache Manager to Disk - _cacheManager.WriteCacheManager(); + CacheManager.Instance.WriteCacheManager(); // Report the storage usage reportStorage(); @@ -247,16 +224,17 @@ public void Run() // Set the status on the client info screen if (threadsToStart.Count == 0) { - _clientInfoForm.RequiredFilesStatus = "Sleeping (inside download window)"; - + ClientInfo.Instance.RequiredFilesStatus = "Sleeping (inside download window)"; + // Raise an event to say we've completed - if (OnFullyProvisioned != null) - OnFullyProvisioned(); + OnFullyProvisioned?.Invoke(); } else - _clientInfoForm.RequiredFilesStatus = string.Format("{0} files to download", threadsToStart.Count.ToString()); + { + ClientInfo.Instance.RequiredFilesStatus = string.Format("{0} files to download", threadsToStart.Count.ToString()); + } - _clientInfoForm.UpdateRequiredFiles(RequiredFilesString()); + ClientInfo.Instance.UpdateRequiredFiles(RequiredFilesString()); } } } @@ -268,19 +246,19 @@ public void Run() // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("RequiredFilesAgent - Run", "WebException in Run: " + webEx.Message), LogType.Info.ToString()); - _clientInfoForm.RequiredFilesStatus = "Error: " + webEx.Message; + ClientInfo.Instance.RequiredFilesStatus = "Error: " + webEx.Message; } catch (Exception ex) { // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("RequiredFilesAgent - Run", "Exception in Run: " + ex.Message), LogType.Error.ToString()); - _clientInfoForm.RequiredFilesStatus = "Error: " + ex.Message; + ClientInfo.Instance.RequiredFilesStatus = "Error: " + ex.Message; } } else { - _clientInfoForm.RequiredFilesStatus = string.Format("Outside Download Window {0} - {1}", ApplicationSettings.Default.DownloadStartWindowTime.ToString("HH:mm", CultureInfo.InvariantCulture), ApplicationSettings.Default.DownloadEndWindowTime.ToString("HH:mm", CultureInfo.InvariantCulture)); + ClientInfo.Instance.RequiredFilesStatus = string.Format("Outside Download Window {0} - {1}", ApplicationSettings.Default.DownloadStartWindowTime.ToString("HH:mm", CultureInfo.InvariantCulture), ApplicationSettings.Default.DownloadEndWindowTime.ToString("HH:mm", CultureInfo.InvariantCulture)); } } @@ -314,7 +292,7 @@ private string RequiredFilesString() /// void fileAgent_OnPartComplete(int fileId) { - _clientInfoForm.UpdateRequiredFiles(RequiredFilesString()); + ClientInfo.Instance.UpdateRequiredFiles(RequiredFilesString()); } /// @@ -332,31 +310,29 @@ void fileAgent_OnComplete(int fileId, string fileType) // Set the status on the client info screen if (_requiredFiles.FilesDownloading == 0) { - _clientInfoForm.RequiredFilesStatus = "Sleeping"; + ClientInfo.Instance.RequiredFilesStatus = "Sleeping"; // If we are the last download thread to complete, then we should report media inventory and raise an event to say we've got everything _requiredFiles.ReportInventory(); // Raise an event to say we've completed - if (OnFullyProvisioned != null) - OnFullyProvisioned(); + OnFullyProvisioned?.Invoke(); } else { - _clientInfoForm.RequiredFilesStatus = string.Format("{0} files to download", _requiredFiles.FilesDownloading.ToString()); + ClientInfo.Instance.RequiredFilesStatus = string.Format("{0} files to download", _requiredFiles.FilesDownloading.ToString()); } // Update the RequiredFiles TextBox - _clientInfoForm.UpdateRequiredFiles(RequiredFilesString()); + ClientInfo.Instance.UpdateRequiredFiles(RequiredFilesString()); // Write the Cache Manager to Disk - _cacheManager.WriteCacheManager(); + CacheManager.Instance.WriteCacheManager(); if (rf.FileType == "layout") { // Raise an event to say it is completed - if (OnComplete != null) - OnComplete(rf.SaveAs); + OnComplete?.Invoke(rf.SaveAs); } } @@ -410,7 +386,7 @@ private void scheduleAgent() Trace.WriteLine(new LogMessage("ScheduleAgent - Run", "Thread Woken and Lock Obtained"), LogType.Audit.ToString()); - _clientInfoForm.ScheduleStatus = "Running: Get Data from Xibo Server"; + ClientInfo.Instance.ScheduleStatus = "Running: Get Data from Xibo Server"; using (xmds.xmds xmds = new xmds.xmds()) { @@ -423,7 +399,7 @@ private void scheduleAgent() // Set the flag to indicate we have a connection to XMDS ApplicationSettings.Default.XmdsLastConnection = DateTime.Now; - _clientInfoForm.ScheduleStatus = "Running: Data Received"; + ClientInfo.Instance.ScheduleStatus = "Running: Data Received"; // Hash of the result string md5NewSchedule = Hashes.MD5(scheduleXml); @@ -434,7 +410,7 @@ private void scheduleAgent() { Trace.WriteLine(new LogMessage("Schedule Agent - Run", "Received new schedule")); - _clientInfoForm.ScheduleStatus = "Running: New Schedule Received"; + ClientInfo.Instance.ScheduleStatus = "Running: New Schedule Received"; // Write the result to the schedule xml location ScheduleManager.WriteScheduleXmlToDisk(_scheduleLocation, scheduleXml); @@ -443,7 +419,7 @@ private void scheduleAgent() _scheduleManager.RefreshSchedule = true; } - _clientInfoForm.ScheduleStatus = "Sleeping"; + ClientInfo.Instance.ScheduleStatus = "Sleeping"; } } catch (WebException webEx) @@ -454,13 +430,13 @@ private void scheduleAgent() // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("ScheduleAgent - Run", "WebException in Run: " + webEx.Message), LogType.Info.ToString()); - _clientInfoForm.ScheduleStatus = "Error: " + webEx.Message; + ClientInfo.Instance.ScheduleStatus = "Error: " + webEx.Message; } catch (Exception ex) { // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("ScheduleAgent - Run", "Exception in Run: " + ex.Message), LogType.Error.ToString()); - _clientInfoForm.ScheduleStatus = "Error. " + ex.Message; + ClientInfo.Instance.ScheduleStatus = "Error. " + ex.Message; } } } diff --git a/app.config b/app.config index 25cde2a1..941d39e4 100644 --- a/app.config +++ b/app.config @@ -1,7 +1,7 @@  - +
@@ -12,13 +12,6 @@ - - - - {{XMDS_LOCATION}} - - - @@ -27,4 +20,11 @@ + + + + http://localhost/xmds.php?v=5 + + + \ No newline at end of file diff --git a/default.config.xml b/default.config.xml index edcc42e3..64d27b27 100644 --- a/default.config.xml +++ b/default.config.xml @@ -53,4 +53,5 @@ 9696 0 + edge \ No newline at end of file From b1c929779878e401089149fb4d8d1c7e024b3be0 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 7 Feb 2020 16:58:02 +0000 Subject: [PATCH 07/29] Improve rendering and add in additional modules. Run a "using" tidy up over the project --- Action/Command.cs | 2 - Action/LayoutChangePlayerAction.cs | 3 - Action/OverlayLayoutPlayerAction.cs | 3 - Action/PlayerAction.cs | 3 - Action/PlayerActionInterface.cs | 3 - Action/RevertToSchedulePlayerAction.cs | 9 +- Action/Rs232Command.cs | 2 - Action/ScheduleCommand.cs | 3 - Action/XmrSubscriber.cs | 2 - App.xaml.cs | 5 - Control/EmbeddedServer.cs | 5 +- Control/WatchDogManager.cs | 3 - Error/DefaultLayoutException.cs | 4 - Log/ClientInfo.cs | 4 - Log/ClientInfoTraceListener.cs | 4 - Log/LogMessage.cs | 7 +- Log/XiboTraceListener.cs | 24 +- Logic/ApplicationSettings.cs | 29 +- Logic/BlackList.cs | 8 +- Logic/CacheManager.cs | 11 +- Logic/HardwareKey.cs | 23 +- Logic/Hashes.cs | 3 +- Logic/KeyStore.cs | 2 - Logic/MediaOption.cs | 10 - Logic/MouseInterceptor.cs | 4 +- Logic/OpenSslInterop.cs | 2 - Logic/RegionOptions.cs | 1 - Logic/RequiredFiles.cs | 15 +- Logic/Schedule.cs | 24 +- Logic/ScheduleItem.cs | 2 - Logic/ScheduleManager.cs | 105 ++++--- Logic/ScreenShot.cs | 6 +- MainWindow.xaml.cs | 61 ++-- OptionsForm.xaml.cs | 14 +- Properties/AssemblyInfo.cs | 2 - Rendering/Audio.cs | 114 +------- Rendering/Flash.cs | 74 +++++ Rendering/Image.cs | 123 ++++++-- Rendering/Layout.xaml.cs | 33 ++- Rendering/Media.xaml.cs | 76 +++-- Rendering/PowerPoint.cs | 17 ++ Rendering/Region.xaml.cs | 54 ++-- Rendering/ShellCommand.cs | 219 ++++++++++++++ Rendering/Transitions.cs | 4 - Rendering/Video.cs | 157 ++++++++++ Rendering/WebCef.cs | 46 ++- Rendering/WebEdge.cs | 120 -------- Rendering/WebIe.cs | 70 ++++- Rendering/WebMedia.cs | 36 +-- Resources/HtmlTemplate.htm | 384 ------------------------- Stats/Stat.cs | 4 - Stats/StatLog.cs | 12 +- XiboClient.csproj | 5 +- XmdsAgents/FileAgent.cs | 9 +- XmdsAgents/LibraryAgent.cs | 12 +- XmdsAgents/LogAgent.cs | 4 +- XmdsAgents/RegisterAgent.cs | 11 +- XmdsAgents/ScheduleAgent.cs | 7 +- XmdsAgents/ScheduleAndFilesAgent.cs | 15 +- packages.config | 16 -- 60 files changed, 938 insertions(+), 1092 deletions(-) create mode 100644 Rendering/Flash.cs create mode 100644 Rendering/PowerPoint.cs create mode 100644 Rendering/ShellCommand.cs create mode 100644 Rendering/Video.cs delete mode 100644 Rendering/WebEdge.cs delete mode 100644 Resources/HtmlTemplate.htm delete mode 100644 packages.config diff --git a/Action/Command.cs b/Action/Command.cs index ae3cfc07..302993e0 100644 --- a/Action/Command.cs +++ b/Action/Command.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; namespace XiboClient.Logic { diff --git a/Action/LayoutChangePlayerAction.cs b/Action/LayoutChangePlayerAction.cs index 7328c5f1..666abce4 100644 --- a/Action/LayoutChangePlayerAction.cs +++ b/Action/LayoutChangePlayerAction.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace XiboClient.Action { diff --git a/Action/OverlayLayoutPlayerAction.cs b/Action/OverlayLayoutPlayerAction.cs index 9848e23b..9ea37558 100644 --- a/Action/OverlayLayoutPlayerAction.cs +++ b/Action/OverlayLayoutPlayerAction.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace XiboClient.Action { diff --git a/Action/PlayerAction.cs b/Action/PlayerAction.cs index afb39dc2..b5c976b4 100644 --- a/Action/PlayerAction.cs +++ b/Action/PlayerAction.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace XiboClient.Action { diff --git a/Action/PlayerActionInterface.cs b/Action/PlayerActionInterface.cs index 72985e1c..449d892a 100644 --- a/Action/PlayerActionInterface.cs +++ b/Action/PlayerActionInterface.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace XiboClient.Action { diff --git a/Action/RevertToSchedulePlayerAction.cs b/Action/RevertToSchedulePlayerAction.cs index 9c2c8183..70b38160 100644 --- a/Action/RevertToSchedulePlayerAction.cs +++ b/Action/RevertToSchedulePlayerAction.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace XiboClient.Action +namespace XiboClient.Action { class RevertToSchedulePlayerAction : PlayerActionInterface { public const string Name = "revertToSchedule"; - + public string GetActionName() { return Name; diff --git a/Action/Rs232Command.cs b/Action/Rs232Command.cs index 85d24add..ac092a1c 100644 --- a/Action/Rs232Command.cs +++ b/Action/Rs232Command.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO.Ports; using System.Linq; -using System.Text; namespace XiboClient.Logic { diff --git a/Action/ScheduleCommand.cs b/Action/ScheduleCommand.cs index 5959fc74..1cf146e6 100644 --- a/Action/ScheduleCommand.cs +++ b/Action/ScheduleCommand.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; namespace XiboClient.Logic { diff --git a/Action/XmrSubscriber.cs b/Action/XmrSubscriber.cs index ee06800c..cbe677ee 100644 --- a/Action/XmrSubscriber.cs +++ b/Action/XmrSubscriber.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; using System.Threading; using XiboClient.Action; using XiboClient.Log; diff --git a/App.xaml.cs b/App.xaml.cs index 53b6339f..66af7e3c 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; using System.Diagnostics; -using System.Linq; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; diff --git a/Control/EmbeddedServer.cs b/Control/EmbeddedServer.cs index 5f3745f8..beb9b581 100644 --- a/Control/EmbeddedServer.cs +++ b/Control/EmbeddedServer.cs @@ -1,9 +1,6 @@ using EmbedIO; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; using System.Threading; using XiboClient.Log; @@ -56,7 +53,7 @@ public void Run() // Wait _manualReset.WaitOne(); } - + Trace.WriteLine(new LogMessage("EmbeddedServer - Run", "Server Stopped"), LogType.Info.ToString()); } catch (Exception e) diff --git a/Control/WatchDogManager.cs b/Control/WatchDogManager.cs index d6839825..4d39bb86 100644 --- a/Control/WatchDogManager.cs +++ b/Control/WatchDogManager.cs @@ -18,11 +18,8 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; namespace XiboClient.Control { diff --git a/Error/DefaultLayoutException.cs b/Error/DefaultLayoutException.cs index e9b89c76..99d45515 100644 --- a/Error/DefaultLayoutException.cs +++ b/Error/DefaultLayoutException.cs @@ -18,10 +18,6 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace XiboClient.Error { diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index a02ac96d..1398903d 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -1,13 +1,9 @@ using Newtonsoft.Json; using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; -using System.Windows.Threading; namespace XiboClient.Log { diff --git a/Log/ClientInfoTraceListener.cs b/Log/ClientInfoTraceListener.cs index 85b63bb0..a76bb5bd 100644 --- a/Log/ClientInfoTraceListener.cs +++ b/Log/ClientInfoTraceListener.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Diagnostics; -using XiboClient.Properties; namespace XiboClient.Log { diff --git a/Log/LogMessage.cs b/Log/LogMessage.cs index e54ad2fe..26ed0c83 100644 --- a/Log/LogMessage.cs +++ b/Log/LogMessage.cs @@ -18,12 +18,9 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Text; -using System.Xml; -using System.Diagnostics; -using System.Threading; using System.Security; +using System.Threading; +using System.Xml; namespace XiboClient { diff --git a/Log/XiboTraceListener.cs b/Log/XiboTraceListener.cs index 67cfeb9e..59c2daa9 100644 --- a/Log/XiboTraceListener.cs +++ b/Log/XiboTraceListener.cs @@ -18,16 +18,10 @@ * along with Xibo. If not, see . */ using System; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Text; -using System.Xml; -using System.Diagnostics; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Windows.Forms; -using System.Security; -using System.Threading; -using System.Net; /// 17/02/12 Dan Changed to always Log audit if no category is given @@ -47,9 +41,9 @@ public XiboTraceListener() public XiboTraceListener(string r_strListenerName) : base(r_strListenerName) - { - InitializeListener() ; - } + { + InitializeListener(); + } private void InitializeListener() { @@ -95,7 +89,7 @@ private void AddToCollection(string message, string category) if (ApplicationSettings.Default.LogLevel == "info" && (logtype != LogType.Error && logtype != LogType.Info)) return; - + _traceMessages.Add(new TraceMessage { category = category, @@ -110,7 +104,7 @@ private void AddToCollection(string message, string category) private void FlushToFile() { - if (_traceMessages.Count < 1) + if (_traceMessages.Count < 1) return; try @@ -207,7 +201,7 @@ public override void Fail(string message, string detailMessage) public override void Close() { // Determine if there is anything to flush - if (_traceMessages.Count < 1) + if (_traceMessages.Count < 1) return; // Flush to file (we will send these next time we start up) @@ -220,7 +214,7 @@ public override void Close() public override void Flush() { // Determine if there is anything to flush - if (_traceMessages.Count < 1) + if (_traceMessages.Count < 1) return; FlushToFile(); diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 6eddc97d..d007d126 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -21,13 +21,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using System.Text; using System.Xml; -using System.Xml.Serialization; using XiboClient.Logic; namespace XiboClient @@ -339,9 +336,9 @@ public void PopulateFromXml(XmlDocument document) public string NewCmsKey { get; set; } private string _libraryPath; - public string LibraryPath - { - get + public string LibraryPath + { + get { if (_libraryPath == "DEFAULT") { @@ -357,11 +354,11 @@ public string LibraryPath } return _libraryPath; - } - set + } + set { - _libraryPath = value; - } + _libraryPath = value; + } } /// @@ -374,7 +371,7 @@ public string XiboClient_xmds_xmds return ServerUri.TrimEnd('\\') + @"/xmds.php?v=" + ApplicationSettings.Default.Version; } } - + public string ServerKey { get; set; } private string _displayName; @@ -384,8 +381,8 @@ public string XiboClient_xmds_xmds /// Server Address /// private string _serverUri; - public string ServerUri - { + public string ServerUri + { get { return (string.IsNullOrEmpty(_serverUri)) ? "http://localhost" : _serverUri; @@ -417,9 +414,9 @@ public string ServerUri // Embedded web server config public int EmbeddedServerPort { get; set; } - public string EmbeddedServerAddress - { - get + public string EmbeddedServerAddress + { + get { return "http://localhost:" + ((EmbeddedServerPort == 0) ? 9696 : EmbeddedServerPort) + "/"; } diff --git a/Logic/BlackList.cs b/Logic/BlackList.cs index 7100a136..4b5f8945 100644 --- a/Logic/BlackList.cs +++ b/Logic/BlackList.cs @@ -19,12 +19,10 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text; using System.Xml; -using System.Windows.Forms; -using System.Diagnostics; namespace XiboClient.Logic { @@ -61,7 +59,7 @@ public void Add(string id, BlackListType type, string reason) { // Do some validation if (reason == "") reason = "No reason provided"; - + int mediaId; if (!int.TryParse(id, out mediaId)) { @@ -174,7 +172,7 @@ public Boolean BlackListed(string fileId) using (StreamReader sr = new StreamReader(fileStream)) { string listed = sr.ReadToEnd(); - + return listed.Contains(String.Format("[{0}]", fileId)); } } diff --git a/Logic/CacheManager.cs b/Logic/CacheManager.cs index 1d272c02..e999ec52 100644 --- a/Logic/CacheManager.cs +++ b/Logic/CacheManager.cs @@ -19,14 +19,11 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text; -using System.IO; -using System.Windows.Forms; -using System.Xml.Serialization; using System.Diagnostics; +using System.IO; using System.Xml; +using System.Xml.Serialization; namespace XiboClient { @@ -108,7 +105,7 @@ public String GetMD5(String path) Remove(path); Add(path, md5); - + // Return the new MD5 return md5; } @@ -138,7 +135,7 @@ private string CalcMD5(String path) catch (Exception ex) { Trace.WriteLine(new LogMessage("CalcMD5", "Unable to calc the MD5 because: " + ex.Message), LogType.Error.ToString()); - + // Return a 0 MD5 which will immediately invalidate the file return "0"; } diff --git a/Logic/HardwareKey.cs b/Logic/HardwareKey.cs index fe45a6d1..3771b1fb 100644 --- a/Logic/HardwareKey.cs +++ b/Logic/HardwareKey.cs @@ -17,20 +17,15 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Management; -using System.Text; -using System.Diagnostics; -using System.Net.NetworkInformation; -using System.Security.Cryptography; -using System.Windows.Forms; -using Org.BouncyCastle.Security; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using System; +using System.Diagnostics; using System.IO; -using Org.BouncyCastle.Crypto.Generators; +using System.Management; +using System.Net.NetworkInformation; namespace XiboClient { @@ -97,9 +92,9 @@ public HardwareKey() ///
public string Key { - get - { - return this._hardwareKey; + get + { + return this._hardwareKey; } } diff --git a/Logic/Hashes.cs b/Logic/Hashes.cs index b980187e..2e06506e 100644 --- a/Logic/Hashes.cs +++ b/Logic/Hashes.cs @@ -18,10 +18,9 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Text; using System.IO; using System.Security.Cryptography; +using System.Text; namespace XiboClient { diff --git a/Logic/KeyStore.cs b/Logic/KeyStore.cs index b5e67891..26d5d9ef 100644 --- a/Logic/KeyStore.cs +++ b/Logic/KeyStore.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Windows.Forms; namespace XiboClient diff --git a/Logic/MediaOption.cs b/Logic/MediaOption.cs index f6133e6a..c67303c4 100644 --- a/Logic/MediaOption.cs +++ b/Logic/MediaOption.cs @@ -18,17 +18,7 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Security.Cryptography; -using System.IO; -using System.Text; -using System.Windows.Forms; -using System.Xml; -using System.Xml.Serialization; -using System.Diagnostics; -using System.Collections; namespace XiboClient { diff --git a/Logic/MouseInterceptor.cs b/Logic/MouseInterceptor.cs index f2081a26..19848536 100644 --- a/Logic/MouseInterceptor.cs +++ b/Logic/MouseInterceptor.cs @@ -1,8 +1,6 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Drawing; -using System.Windows; namespace XiboClient.Logic { @@ -15,7 +13,7 @@ class MouseInterceptor { private static LowLevelMouseProc _proc = HookCallback; private static IntPtr _hookID = IntPtr.Zero; - + private static MouseInterceptor s_instance = null; // The KeyPressed Event public event MouseInterceptorEventHandler MouseEvent; diff --git a/Logic/OpenSslInterop.cs b/Logic/OpenSslInterop.cs index 3f073b54..58032cab 100644 --- a/Logic/OpenSslInterop.cs +++ b/Logic/OpenSslInterop.cs @@ -2,8 +2,6 @@ using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; namespace XiboClient.Logic diff --git a/Logic/RegionOptions.cs b/Logic/RegionOptions.cs index 929668f6..360c0b8f 100644 --- a/Logic/RegionOptions.cs +++ b/Logic/RegionOptions.cs @@ -21,7 +21,6 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Text; using System.Xml; using XiboClient.Rendering; diff --git a/Logic/RequiredFiles.cs b/Logic/RequiredFiles.cs index 841e1920..c02c9b86 100644 --- a/Logic/RequiredFiles.cs +++ b/Logic/RequiredFiles.cs @@ -18,16 +18,11 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text; +using System.Diagnostics; using System.IO; -using System.Security.Cryptography; using System.Xml; -using System.Diagnostics; -using System.Windows.Forms; using System.Xml.Serialization; -using XiboClient.Properties; /// 17/02/12 Dan Enriched to also manage currently downloading files /// 28/02/12 Dan Changed the way RequiredFiles are updated @@ -105,8 +100,8 @@ private void SetRequiredFiles() foreach (XmlNode file in fileNodes) { - RequiredFile rf = new RequiredFile(); - + RequiredFile rf = new RequiredFile(); + XmlAttributeCollection attributes = file.Attributes; rf.FileType = attributes["type"].Value; @@ -140,7 +135,7 @@ private void SetRequiredFiles() rf.Path = rf.Path + ".xlf"; rf.SaveAs = rf.Path; } - + rf.ChunkSize = rf.Size; } else if (rf.FileType == "resource") @@ -155,7 +150,7 @@ private void SetRequiredFiles() rf.MediaId = attributes["mediaid"].Value; rf.Path = rf.MediaId + ".htm"; rf.SaveAs = rf.Path; - + // Set the size to something arbitary rf.Size = 10000; diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index b3c57185..1f6fd562 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -19,21 +19,14 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Security.Cryptography; -using System.IO; -using System.Text; -using System.Xml; -using System.Xml.Serialization; using System.Diagnostics; -using XiboClient.XmdsAgents; using System.Threading; -using XiboClient.Properties; -using XiboClient.Log; -using XiboClient.Logic; using XiboClient.Action; using XiboClient.Control; +using XiboClient.Log; +using XiboClient.Logic; +using XiboClient.XmdsAgents; namespace XiboClient { @@ -166,7 +159,7 @@ public Schedule(string scheduleLocation) // Library Agent _libraryAgent = new LibraryAgent(); _libraryAgent.CurrentCacheManager = CacheManager.Instance; - + // Create a thread for the Library Agent to run in - but dont start it up yet. _libraryAgentThread = new Thread(new ThreadStart(_libraryAgent.Run)); _libraryAgentThread.Name = "LibraryAgent"; @@ -195,7 +188,7 @@ public Schedule(string scheduleLocation) /// /// Initialize the Schedule components /// - public void InitializeComponents() + public void InitializeComponents() { // Start the RegisterAgent thread _registerAgentThread.Start(); @@ -285,7 +278,7 @@ void _scheduleManager_OnScheduleManagerCheckComplete() Trace.WriteLine(new LogMessage("Schedule - OnScheduleManagerCheckComplete", "XMR heart beat last received over 5 minutes ago."), LogType.Audit.ToString()); } } - + /// /// Are all the required agent threads alive? /// @@ -330,7 +323,8 @@ void _xmrSubscriber_OnAction(Action.PlayerActionInterface action) { _scheduleManager.ReplaceLayoutChangeActions(((LayoutChangePlayerAction)action)); } - else { + else + { _scheduleManager.AddLayoutChangeAction(((LayoutChangePlayerAction)action)); } @@ -450,7 +444,7 @@ public void NextLayout() // Raise the event ScheduleChangeEvent(_layoutSchedule[_currentLayout].layoutFile, _layoutSchedule[_currentLayout].scheduleid, _layoutSchedule[_currentLayout].id); } - + /// /// The number of active layouts in the current schedule /// diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index 0bc98f22..78cc57bc 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace XiboClient.Logic { diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index f766bd10..97dbce8e 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -21,18 +21,15 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Security.Cryptography; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Text; -using System.Windows.Forms; +using System.Threading; using System.Xml; -using System.Xml.Serialization; -using System.Diagnostics; +using XiboClient.Action; using XiboClient.Log; -using System.Threading; using XiboClient.Logic; -using System.Globalization; -using XiboClient.Action; namespace XiboClient { @@ -71,7 +68,7 @@ class ScheduleManager private bool _refreshSchedule; private DateTime _lastScreenShotDate; - + /// /// The currently playing layout Id /// @@ -140,7 +137,7 @@ public Collection CurrentOverlaySchedule return _currentOverlaySchedule; } } - + /// /// Get or Set the current layout id /// @@ -298,7 +295,7 @@ private bool IsNewScheduleAvailable() } catch (Exception ex) { - Trace.WriteLine(new LogMessage("IsNewScheduleAvailable", string.Format("Unable to load schedule from disk: {0}", ex.Message)), + Trace.WriteLine(new LogMessage("IsNewScheduleAvailable", string.Format("Unable to load schedule from disk: {0}", ex.Message)), LogType.Error.ToString()); // If we cant load the schedule from disk then use an empty schedule. @@ -336,7 +333,7 @@ private bool IsNewScheduleAvailable() } currentScheduleString.Add(layout.ToString()); } - + foreach (ScheduleItem layout in newSchedule) { newScheduleString.Add(layout.ToString()); @@ -397,7 +394,7 @@ private Collection LoadNewSchedule() Collection newSchedule = new Collection(); Collection prioritySchedule = new Collection(); Collection layoutChangeSchedule = new Collection(); - + // Temporary default Layout incase we have no layout nodes. ScheduleItem defaultLayout = new ScheduleItem(); @@ -418,46 +415,46 @@ private Collection LoadNewSchedule() // If we haven't already assessed this layout before, then check that it is valid if (!validLayoutIds.Contains(layout.id)) { - if (!ApplicationSettings.Default.ExpireModifiedLayouts && layout.id == CurrentLayoutId) - { - Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Skipping validity test for current layout."), LogType.Audit.ToString()); - } - else - { - // Is the layout valid in the cachemanager? - try + if (!ApplicationSettings.Default.ExpireModifiedLayouts && layout.id == CurrentLayoutId) { - if (!CacheManager.Instance.IsValidPath(layout.id + ".xlf")) + Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Skipping validity test for current layout."), LogType.Audit.ToString()); + } + else + { + // Is the layout valid in the cachemanager? + try + { + if (!CacheManager.Instance.IsValidPath(layout.id + ".xlf")) + { + invalidLayouts.Add(layout.id); + Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Layout invalid: " + layout.id), LogType.Info.ToString()); + continue; + } + } + catch { + // Ignore this layout.. raise an error? invalidLayouts.Add(layout.id); - Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Layout invalid: " + layout.id), LogType.Info.ToString()); + Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Unable to determine if layout is valid or not"), LogType.Error.ToString()); continue; } - } - catch - { - // Ignore this layout.. raise an error? - invalidLayouts.Add(layout.id); - Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Unable to determine if layout is valid or not"), LogType.Error.ToString()); - continue; - } - // Check dependents - bool validDependents = true; - foreach (string dependent in layout.Dependents) - { - if (!string.IsNullOrEmpty(dependent) && !CacheManager.Instance.IsValidPath(dependent)) + // Check dependents + bool validDependents = true; + foreach (string dependent in layout.Dependents) { - invalidLayouts.Add(layout.id); - Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Layout has invalid dependent: " + dependent), LogType.Info.ToString()); + if (!string.IsNullOrEmpty(dependent) && !CacheManager.Instance.IsValidPath(dependent)) + { + invalidLayouts.Add(layout.id); + Trace.WriteLine(new LogMessage("ScheduleManager - LoadNewSchedule", "Layout has invalid dependent: " + dependent), LogType.Info.ToString()); - validDependents = false; - break; + validDependents = false; + break; + } } - } - if (!validDependents) - continue; + if (!validDependents) + continue; } } @@ -529,7 +526,7 @@ private Collection LoadNewOverlaySchedule() Collection newSchedule = new Collection(); Collection prioritySchedule = new Collection(); Collection overlayActionSchedule = new Collection(); - + // Store the valid layout id's List validLayoutIds = new List(); List invalidLayouts = new List(); @@ -715,14 +712,14 @@ private ScheduleItem ParseNodeIntoScheduleItem(XmlNode node) temp.layoutFile = ApplicationSettings.Default.LibraryPath + @"\" + layoutFile + @".xlf"; temp.id = int.Parse(layoutFile); - // Dependents - if (attributes["dependents"] != null && !string.IsNullOrEmpty(attributes["dependents"].Value)) - { - foreach (string dependent in attributes["dependents"].Value.Split(',')) - { - temp.Dependents.Add(dependent); - } - } + // Dependents + if (attributes["dependents"] != null && !string.IsNullOrEmpty(attributes["dependents"].Value)) + { + foreach (string dependent in attributes["dependents"].Value.Split(',')) + { + temp.Dependents.Add(dependent); + } + } // Get attributes that only exist on the default if (temp.NodeName != "default") @@ -790,7 +787,7 @@ private void LoadScheduleFromLayoutChangeActions() continue; DateTime actionCreateDt = DateTime.Parse(action.createdDt); - + ScheduleItem item = new ScheduleItem(); item.FromDt = actionCreateDt.AddSeconds(-1); item.ToDt = DateTime.MaxValue; @@ -863,9 +860,9 @@ private void SetEmptySchedule() private XmlDocument GetScheduleXml() { Debug.WriteLine("Getting the Schedule XML", LogType.Info.ToString()); - + XmlDocument scheduleXml; - + // Check the schedule file exists if (File.Exists(_location)) { diff --git a/Logic/ScreenShot.cs b/Logic/ScreenShot.cs index 6a85e0de..edd6f591 100644 --- a/Logic/ScreenShot.cs +++ b/Logic/ScreenShot.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Linq; -using System.Text; using System.Windows.Forms; namespace XiboClient.Logic diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 5733c2c7..ff70143e 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -19,40 +19,23 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Net; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; using System.Windows.Input; -using System.Windows.Media; +using System.Windows.Media.Animation; using System.Windows.Media.Imaging; -using System.Windows.Navigation; using System.Windows.Shapes; -using System.Collections; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Data; -using System.Xml; -using System.Net; -using System.IO; -using System.Xml.Serialization; -using System.Diagnostics; +using System.Windows.Threading; +using XiboClient.Error; using XiboClient.Log; -using System.Threading; -using XiboClient.Properties; -using System.Globalization; using XiboClient.Logic; -using XiboClient.Error; -using System.Drawing.Imaging; -using System.Windows.Threading; using XiboClient.Rendering; using XiboClient.Stats; -using System.Windows.Media.Animation; namespace XiboClient { @@ -207,7 +190,7 @@ private void InitializeXibo() Loaded += MainWindow_Loaded; Closing += MainForm_FormClosing; ContentRendered += MainForm_Shown; - + // Define the hotkey /*Keys key; try @@ -596,13 +579,16 @@ private void ChangeToNextLayout(string layoutPath) /// void splashScreenTimer_Tick(object sender, EventArgs e) { - Debug.WriteLine(new LogMessage("timer_Tick", "Loading next layout after splashscreen")); - DispatcherTimer timer = (DispatcherTimer)sender; timer.Stop(); - // Put the next Layout up - _schedule.NextLayout(); + if (_showingSplash) + { + Debug.WriteLine(new LogMessage("timer_Tick", "Loading next layout after splashscreen")); + + // Put the next Layout up + _schedule.NextLayout(); + } } /// @@ -620,7 +606,7 @@ private Layout PrepareLayout(string layoutPath, bool isOverlay) } else { - try + try { // Construct a new Current Layout Layout layout = new Layout(); @@ -688,7 +674,10 @@ private void ShowDefaultSplashScreen() /// private void RemoveSplashScreen() { - this.Scene.Children.Remove(this.splashScreen); + if (this.splashScreen != null) + { + this.Scene.Children.Remove(this.splashScreen); + } // We've removed it this._showingSplash = false; @@ -860,7 +849,7 @@ public void ManageOverlays(Collection overlays) OverlayScene.Children.Add(layout); // Start - currentLayout.Start(); + layout.Start(); } catch (DefaultLayoutException) { @@ -929,9 +918,9 @@ private void SetMainWindowSize() /// private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) { - Trace.WriteLine(new LogMessage("SystemEvents_DisplaySettingsChanged", - "Display Settings have changed, resizing the Player window and moving on to the next Layout. W=" - + SystemParameters.PrimaryScreenWidth.ToString() + ", H=" + Trace.WriteLine(new LogMessage("SystemEvents_DisplaySettingsChanged", + "Display Settings have changed, resizing the Player window and moving on to the next Layout. W=" + + SystemParameters.PrimaryScreenWidth.ToString() + ", H=" + SystemParameters.PrimaryScreenHeight.ToString()), LogType.Info.ToString()); // Reassert the size of our client (should resize if necessary) diff --git a/OptionsForm.xaml.cs b/OptionsForm.xaml.cs index cb7bf738..abb34a67 100644 --- a/OptionsForm.xaml.cs +++ b/OptionsForm.xaml.cs @@ -1,19 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; namespace XiboClient { diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index d24bbbb6..bac91dd2 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; diff --git a/Rendering/Audio.cs b/Rendering/Audio.cs index f539f104..bfc598a5 100644 --- a/Rendering/Audio.cs +++ b/Rendering/Audio.cs @@ -19,26 +19,15 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; namespace XiboClient.Rendering { - class Audio : Media + class Audio : Video { - private string _filePath; - private int _duration; - private bool _detectEnd = false; - private bool isLooping = false; - - private MediaElement mediaElement; - /// /// Constructor /// @@ -46,106 +35,7 @@ class Audio : Media public Audio(RegionOptions options) : base(options) { - _filePath = Uri.UnescapeDataString(options.uri).Replace('+', ' '); - _duration = options.duration; - - this.mediaElement = new MediaElement(); - this.mediaElement.Width = 0; - this.mediaElement.Height = 0; - this.mediaElement.Visibility = Visibility.Hidden; - this.mediaElement.Volume = options.Dictionary.Get("volume", 100); - - // Events - this.mediaElement.MediaEnded += MediaElement_MediaEnded; - this.mediaElement.MediaFailed += MediaElement_MediaFailed; - - // Should we loop? - this.isLooping = (options.Dictionary.Get("loop", "0") == "1" && _duration != 0); - } - - private void MediaElement_MediaFailed(object sender, ExceptionRoutedEventArgs e) - { - // Log and expire - Trace.WriteLine(new LogMessage("Audio", "MediaElement_MediaFailed: Media Failed. E = " + e.ErrorException.Message), LogType.Error.ToString()); - - Expired = true; - } - - private void MediaElement_MediaEnded(object sender, System.Windows.RoutedEventArgs e) - { - // Should we loop? - if (isLooping) - { - this.mediaElement.Position = TimeSpan.Zero; - this.mediaElement.Play(); - } - else - { - Expired = true; - } - } - - public override void RenderMedia() - { - // Check to see if the video exists or not (if it doesnt say we are already expired) - if (!File.Exists(_filePath)) - { - Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Local Video file " + _filePath + " not found.")); - throw new FileNotFoundException(); - } - - // Do we need to determine the end time ourselves? - if (_duration == 0) - { - // Set the duration to 1 second - // this essentially means RenderMedia will set up a timer which ticks every second - // when we're actually expired and we detect the end, we set expired - Duration = 1; - _detectEnd = true; - } - - // Render media as normal (starts the timer, shows the form, etc) - base.RenderMedia(); - - try - { - // Start Player - this.mediaElement.Source = new Uri(_filePath); - - this.MediaScene.Children.Add(this.mediaElement); - - Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Video Started"), LogType.Audit.ToString()); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("Audio - RenderMedia", ex.Message), LogType.Error.ToString()); - - // Unable to start video - expire this media immediately - throw; - } - } - - public override void Stop() - { - // Remove the event handlers - this.mediaElement.MediaEnded -= MediaElement_MediaEnded; - this.mediaElement.MediaFailed -= MediaElement_MediaFailed; - - base.Stop(); - } - - /// - /// Override the timer tick - /// - /// - /// - protected override void timer_Tick(object sender, EventArgs e) - { - if (!_detectEnd || Expired) - { - // We're not end detect, so we pass the timer through - base.timer_Tick(sender, e); - } + this.ShouldBeVisible = false; } } } diff --git a/Rendering/Flash.cs b/Rendering/Flash.cs new file mode 100644 index 00000000..800d4f57 --- /dev/null +++ b/Rendering/Flash.cs @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Rendering +{ + class Flash : WebIe + { + public Flash(RegionOptions options) : base(options) + { + // Set NativeOpen to true + options.Dictionary.Replace("modeid", "1"); + + // This openes a cached file + this._filePath = ApplicationSettings.Default.EmbeddedServerAddress + "flash_" + options.FileId + ".htm"; + + if (!File.Exists(this._filePath)) + { + // Set the body + string html = @" + + + + + + + + + + + + + + + "; + html = string.Format(html, options.uri, options.uri, options.width.ToString(), options.height.ToString()); + + html = this.MakeHtmlSubstitutions(html); + + // Save this file to disk + using (FileStream stream = new FileStream(this._filePath, FileMode.Create)) + { + using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8)) + { + writer.WriteLine(html); + } + } + } + } + } +} diff --git a/Rendering/Image.cs b/Rendering/Image.cs index 3f32a4a5..724bea36 100644 --- a/Rendering/Image.cs +++ b/Rendering/Image.cs @@ -1,36 +1,113 @@ /** - * Copyright (C) 2020 Xibo Signage Ltd - * - * Xibo - Digital Signage - http://www.xibo.org.uk - * - * This file is part of Xibo. - * - * Xibo is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Xibo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Xibo. If not, see . - */ +* Copyright (C) 2020 Xibo Signage Ltd +* +* Xibo - Digital Signage - http://www.xibo.org.uk +* +* This file is part of Xibo. +* +* Xibo is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* any later version. +* +* Xibo is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with Xibo. If not, see . +*/ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics; +using System.IO; +using System.Windows.Controls; +using System.Windows.Media.Imaging; namespace XiboClient.Rendering { class Image : Media { + private System.Windows.Controls.Image image; + private string filePath; + private string scaleType; + private System.Windows.HorizontalAlignment hAlign; + private System.Windows.VerticalAlignment vAlign; + public Image(RegionOptions options) : base(options) { + this.filePath = options.uri; + this.scaleType = options.Dictionary.Get("scaleType", "stretch"); + + // Horizontal Alignment + switch (options.Dictionary.Get("align", "center")) + { + case "left": + hAlign = System.Windows.HorizontalAlignment.Left; + break; + + case "right": + hAlign = System.Windows.HorizontalAlignment.Right; + break; + + default: + hAlign = System.Windows.HorizontalAlignment.Center; + break; + } + + // Vertical Alignment + switch (options.Dictionary.Get("valign", "middle")) + { + case "top": + this.vAlign = System.Windows.VerticalAlignment.Top; + break; + + case "bottom": + this.vAlign = System.Windows.VerticalAlignment.Bottom; + break; + + default: + this.vAlign = System.Windows.VerticalAlignment.Center; + break; + } + } + + public override void RenderMedia() + { + // Check that the file path exists + if (!File.Exists(this.filePath)) + { + Trace.WriteLine(new LogMessage("Image - Dispose", "Cannot Create image object. Invalid Filepath."), LogType.Error.ToString()); + return; + } + + // Create a URI + Uri filePath = new Uri(this.filePath); + + this.image = new System.Windows.Controls.Image() + { + Name = "Img" + this.Id, + }; + + this.image.Source = new BitmapImage(filePath); + // Handle the different scale types supported + if (this.scaleType == "stretch") + { + this.image.Stretch = System.Windows.Media.Stretch.Fill; + } + else + { + this.image.Stretch = System.Windows.Media.Stretch.Uniform; + + // Further worry about alignment + this.image.HorizontalAlignment = this.hAlign; + this.image.VerticalAlignment = this.vAlign; + } + + this.MediaScene.Children.Add(this.image); + + base.RenderMedia(); } } } diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index a375aa1e..c17d688b 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -25,18 +25,9 @@ using System.Drawing.Imaging; using System.Globalization; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using System.Xml; using XiboClient.Stats; @@ -64,7 +55,7 @@ public partial class Layout : UserControl /// /// Regions for this Layout /// - private Collection _regions; + private List _regions; /// /// Last updated time of this Layout @@ -81,14 +72,13 @@ public partial class Layout : UserControl /// Event to signify that this Layout's duration has elapsed /// public delegate void DurationElapsedDelegate(); - public event DurationElapsedDelegate DurationElapsedEvent; public Layout() { InitializeComponent(); // Create a new empty collection of Regions - _regions = new Collection(); + _regions = new List(); } /// @@ -315,6 +305,12 @@ public void loadFromFile(string layoutPath, int layoutId, int scheduleId, bool i Region temp = new Region(); temp.DurationElapsedEvent += new Region.DurationElapsedDelegate(Region_DurationElapsedEvent); + + // ZIndex + if (nodeAttibutes["zindex"] != null) + { + temp.ZIndex = int.Parse(nodeAttibutes["zindex"].Value); + } Debug.WriteLine("Created new region", "MainForm - Prepare Layout"); @@ -323,13 +319,20 @@ public void loadFromFile(string layoutPath, int layoutId, int scheduleId, bool i // Add to our list of Regions _regions.Add(temp); - - // Add this Region to our Scene - LayoutScene.Children.Add(temp); Debug.WriteLine("Adding region", "MainForm - Prepare Layout"); } + // Order all Regions by their ZIndex + _regions.Sort((l, r) => l.ZIndex < r.ZIndex ? -1 : 1); + + // Add all Regions to the Scene + foreach (Region temp in _regions) + { + // Add this Region to our Scene + LayoutScene.Children.Add(temp); + } + // Null stuff listRegions = null; listMedia = null; diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index 074e2231..f73eaa9e 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -19,20 +19,10 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using System.Windows.Media.Animation; using System.Windows.Threading; namespace XiboClient.Rendering @@ -51,6 +41,11 @@ public partial class Media : UserControl public event DurationElapsedDelegate DurationElapsedEvent; protected int _filesPlayed = 1; + /// + /// The Id of this Media + /// + public string Id { get; set; } + /// /// Gets or Sets the duration of this media. Will be 0 if "" /// @@ -65,7 +60,15 @@ public partial class Media : UserControl protected DispatcherTimer _timer; private bool _timerStarted = false; - protected bool IsRenderCalled { get; set; } + /// + /// The Intended Width of this Media + /// + public int WidthIntended { get { return options.width; } } + + /// + /// The Intended Height of this Media + /// + public int HeightIntended { get { return options.width; } } /// /// The Region Options @@ -78,6 +81,7 @@ public Media(RegionOptions options) // Store the options. this.options = options; + this.Id = options.mediaid; } /// @@ -172,7 +176,7 @@ public virtual void Stop() /// Is a region size change required /// /// - public virtual bool RegionSizeChangeRequired() + public bool RegionSizeChangeRequired() { return false; } @@ -181,7 +185,7 @@ public virtual bool RegionSizeChangeRequired() /// Get Region Size /// /// - public virtual Size GetRegionSize() + public Size GetRegionSize() { return new Size(Width, Height); } @@ -190,31 +194,47 @@ public virtual Size GetRegionSize() /// Get Region Location /// /// - public virtual Point GetRegionLocation() + public Point GetRegionLocation() { return new Point(this.options.top, this.options.left); } - public void transitionIn() + public void TransitionIn() { - /*if (this.options.tr != null) + DoubleAnimation animation = new DoubleAnimation + { + From = 0, + To = 1, + Duration = TimeSpan.FromMilliseconds(1500) + }; + BeginAnimation(OpacityProperty, animation); + + /*string transIn = this.options.Dictionary.Get("transIn"); + if (!string.IsNullOrEmpty(transIn)) { + switch (transIn) + { + case "fadeIn": + + } Transitions.MoveAnimation(medaiElemnt, OpacityProperty, transIn, transInDirection, transInDuration, "in", _top, _left); - } */ + } else + { + return null; + }*/ } - public void transitionOut() + public void TransitionOut() { - /*if (transOut != null) + // Transition out? + DoubleAnimation animation = new DoubleAnimation { - var timerTransition = new DispatcherTimer { Interval = TimeSpan.FromSeconds(transOutStartTime) }; - timerTransition.Start(); - timerTransition.Tick += (sender1, args) => - { - timerTransition.Stop(); - Transitions.MoveAnimation(medaiElemnt, OpacityProperty, transOut, transOutDirection, transOutDuration, "out", _top, _left); - }; - }*/ + From = 1, + To = 0, + Duration = TimeSpan.FromMilliseconds(1500) + }; + //animation.Completed += Animation_Completed; + BeginAnimation(OpacityProperty, animation); } } } diff --git a/Rendering/PowerPoint.cs b/Rendering/PowerPoint.cs new file mode 100644 index 00000000..eb5e40c5 --- /dev/null +++ b/Rendering/PowerPoint.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Rendering +{ + class PowerPoint : WebIe + { + public PowerPoint(RegionOptions options) : base(options) + { + // We are a normal WebIe control, opened natively + options.Dictionary.Replace("modeid", "1"); + } + } +} diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 777c9a03..303f168c 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -1,19 +1,8 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using System.Xml; using XiboClient.Logic; using XiboClient.Stats; @@ -35,6 +24,11 @@ public partial class Region : UserControl ///
public bool IsExpired = false; + /// + /// This Regions zIndex + /// + public int ZIndex { get; set; } + /// /// The Region Options /// @@ -66,6 +60,7 @@ public partial class Region : UserControl public Region() { InitializeComponent(); + ZIndex = 0; } public void loadFromOptions(RegionOptions options) @@ -571,35 +566,21 @@ private Media CreateNextMediaNode() media = new Image(options); break; - /*case "powerpoint": + case "powerpoint": options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; media = new PowerPoint(options); - break;*/ - - /*case "video": - options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; - - // Which video engine are we using? - if (ApplicationSettings.Default.VideoRenderingEngine == "DirectShow") - media = new VideoDS(options); - else - media = new Video(options); - break; + case "video": case "localvideo": - // Which video engine are we using? - if (ApplicationSettings.Default.VideoRenderingEngine == "DirectShow") - media = new VideoDS(options); - else - media = new Video(options); - + options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; + media = new Video(options); break; case "audio": options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; media = new Audio(options); - break;*/ + break; case "datasetview": case "embedded": @@ -610,14 +591,14 @@ private Media CreateNextMediaNode() break; - /*case "flash": + case "flash": options.uri = ApplicationSettings.Default.LibraryPath + @"\" + options.uri; media = new Flash(options); break; case "shellcommand": media = new ShellCommand(options); - break;*/ + break; case "htmlpackage": media = WebMedia.GetConfiguredWebMedia(options); @@ -790,12 +771,21 @@ private void media_DurationElapsedEvent(int filesPlayed) Trace.WriteLine(new LogMessage("Region - DurationElapsedEvent", string.Format("Media Elapsed: {0}", this.options.uri)), LogType.Audit.ToString()); if (filesPlayed > 1) + { // Increment the _current sequence by the number of filesPlayed (minus 1) this.currentSequence = this.currentSequence + (filesPlayed - 1); + } // If this layout has been expired we know that everything will soon be torn down, so do nothing if (IsLayoutExpired) + { return; + } + + // TODO: + // Animate out at this point if we need to + // the result of the animate out complete event should then move us on. + // this.currentMedia.TransitionOut(); // make some decisions about what to do next try diff --git a/Rendering/ShellCommand.cs b/Rendering/ShellCommand.cs new file mode 100644 index 00000000..1cf05f1f --- /dev/null +++ b/Rendering/ShellCommand.cs @@ -0,0 +1,219 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using XiboClient.Logic; + +namespace XiboClient.Rendering +{ + class ShellCommand : Media + { + string _command = ""; + string _code = ""; + bool _launchThroughCmd = true; + bool _terminateCommand = false; + bool _useTaskKill = false; + int _processId; + + public ShellCommand(RegionOptions options) : base(options) + { + _command = Uri.UnescapeDataString(options.Dictionary.Get("windowsCommand")).Replace('+', ' '); + _code = options.Dictionary.Get("commandCode"); + + // Default to launching through CMS for backwards compatiblity + _launchThroughCmd = (options.Dictionary.Get("launchThroughCmd", "1") == "1"); + + // Termination + _terminateCommand = (options.Dictionary.Get("terminateCommand") == "1"); + _useTaskKill = (options.Dictionary.Get("useTaskkill") == "1"); + } + + public override void RenderMedia() + { + if (!string.IsNullOrEmpty(_code)) + { + // Stored command + bool success; + + try + { + Command command = Command.GetByCode(_code); + success = command.Run(); + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("ScheduleManager - Run", "Cannot run Command: " + e.Message), LogType.Error.ToString()); + success = false; + } + + // Notify the state of the command (success or failure) + using (xmds.xmds statusXmds = new xmds.xmds()) + { + statusXmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=notifyStatus"; + statusXmds.NotifyStatusAsync(ApplicationSettings.Default.ServerKey, ApplicationSettings.Default.HardwareKey, "{\"lastCommandSuccess\":" + success + "}"); + } + } + else + { + // shell command + + // Is this module enabled? + if (ApplicationSettings.Default.EnableShellCommands) + { + // Check to see if we have an allow list + if (!string.IsNullOrEmpty(ApplicationSettings.Default.ShellCommandAllowList)) + { + // Array of allowed commands + string[] allowedCommands = ApplicationSettings.Default.ShellCommandAllowList.Split(','); + + // Check we are allowed to execute the command + bool found = false; + + foreach (string allowedCommand in allowedCommands) + { + if (_command.StartsWith(allowedCommand)) + { + found = true; + ExecuteShellCommand(); + break; + } + } + + if (!found) + Trace.WriteLine(new LogMessage("ShellCommand - RenderMedia", "Shell Commands not in allow list: " + ApplicationSettings.Default.ShellCommandAllowList), LogType.Error.ToString()); + } + else + { + // All commands are allowed + ExecuteShellCommand(); + } + } + else + { + Trace.WriteLine(new LogMessage("ShellCommand - RenderMedia", "Shell Commands are disabled"), LogType.Error.ToString()); + } + } + + // All shell commands have a duration of 1 + base.RenderMedia(); + } + + /// + /// Execute the shell command + /// + private void ExecuteShellCommand() + { + Trace.WriteLine(new LogMessage("ShellCommand - ExecuteShellCommand", _command), LogType.Info.ToString()); + + // Execute the commend + if (!string.IsNullOrEmpty(_command)) + { + using (Process process = new Process()) + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + + if (_launchThroughCmd) + { + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + startInfo.FileName = "cmd.exe"; + startInfo.Arguments = "/C " + _command; + } + else + { + // Split the command into a command string and arguments. + string[] splitCommand = _command.Split(new[] { ' ' }, 2); + startInfo.FileName = splitCommand[0]; + + if (splitCommand.Length > 1) + startInfo.Arguments = splitCommand[1]; + } + + process.StartInfo = startInfo; + process.Start(); + + // Grab the ID + _processId = process.Id; + } + } + } + + /// + /// Terminates the shell command + /// + private void TerminateCommand() + { + Trace.WriteLine(new LogMessage("ShellCommand - TerminateCommand", _command), LogType.Info.ToString()); + + if (_processId == 0) + { + Trace.WriteLine(new LogMessage("ShellCommand - TerminateCommand", "ProcessID empty for command: " + _command), LogType.Error.ToString()); + return; + } + + if (_useTaskKill) + { + using (Process process = new Process()) + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + startInfo.FileName = "taskkill.exe"; + startInfo.Arguments = "/pid " + _processId.ToString(); + + process.StartInfo = startInfo; + process.Start(); + } + } + else + { + using (Process process = Process.GetProcessById(_processId)) + { + process.Kill(); + } + } + } + + /// + /// Stop + /// + public override void Stop() + { + try + { + // Terminate the command (only if we've been asked to!) + if (_terminateCommand) + { + TerminateCommand(); + } + } + catch + { + Debug.WriteLine(new LogMessage("Unable to terminate command", "Dispose")); + } + + base.Stop(); + } + } +} diff --git a/Rendering/Transitions.cs b/Rendering/Transitions.cs index bf9499d4..d5cbf7ea 100644 --- a/Rendering/Transitions.cs +++ b/Rendering/Transitions.cs @@ -19,10 +19,6 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; diff --git a/Rendering/Video.cs b/Rendering/Video.cs new file mode 100644 index 00000000..051ad6c6 --- /dev/null +++ b/Rendering/Video.cs @@ -0,0 +1,157 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; + +namespace XiboClient.Rendering +{ + class Video : Media + { + private string _filePath; + private int _duration; + private int volume; + private bool _detectEnd = false; + private bool isLooping = false; + protected bool ShouldBeVisible { get; set; } + + private MediaElement mediaElement; + + public Video(RegionOptions options) : base(options) + { + this.ShouldBeVisible = true; + + _filePath = Uri.UnescapeDataString(options.uri).Replace('+', ' '); + _duration = options.duration; + + // Handle Volume + this.volume = options.Dictionary.Get("volume", 100); + + // Should we loop? + this.isLooping = (options.Dictionary.Get("loop", "0") == "1" && _duration != 0); + } + + private void MediaElement_MediaFailed(object sender, ExceptionRoutedEventArgs e) + { + // Log and expire + Trace.WriteLine(new LogMessage("Audio", "MediaElement_MediaFailed: Media Failed. E = " + e.ErrorException.Message), LogType.Error.ToString()); + + Expired = true; + } + + private void MediaElement_MediaEnded(object sender, System.Windows.RoutedEventArgs e) + { + // Should we loop? + if (isLooping) + { + this.mediaElement.Position = TimeSpan.Zero; + this.mediaElement.Play(); + } + else + { + Expired = true; + } + } + + public override void RenderMedia() + { + // Check to see if the video exists or not (if it doesnt say we are already expired) + if (!File.Exists(_filePath)) + { + Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Local Video file " + _filePath + " not found.")); + throw new FileNotFoundException(); + } + + // Create a Media Element + this.mediaElement = new MediaElement(); + this.mediaElement.Volume = this.volume; + + if (!this.ShouldBeVisible) { + this.mediaElement.Width = 0; + this.mediaElement.Height = 0; + this.mediaElement.Visibility = Visibility.Hidden; + } + + // Events + this.mediaElement.MediaEnded += MediaElement_MediaEnded; + this.mediaElement.MediaFailed += MediaElement_MediaFailed; + + // Do we need to determine the end time ourselves? + if (_duration == 0) + { + // Set the duration to 1 second + // this essentially means RenderMedia will set up a timer which ticks every second + // when we're actually expired and we detect the end, we set expired + Duration = 1; + _detectEnd = true; + } + + // Render media as normal (starts the timer, shows the form, etc) + base.RenderMedia(); + + try + { + // Start Player + this.mediaElement.Source = new Uri(_filePath); + + this.MediaScene.Children.Add(this.mediaElement); + + Trace.WriteLine(new LogMessage("Audio - RenderMedia", "Video Started"), LogType.Audit.ToString()); + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("Audio - RenderMedia", ex.Message), LogType.Error.ToString()); + + // Unable to start video - expire this media immediately + throw; + } + } + + public override void Stop() + { + // Remove the event handlers + this.mediaElement.MediaEnded -= MediaElement_MediaEnded; + this.mediaElement.MediaFailed -= MediaElement_MediaFailed; + + base.Stop(); + } + + /// + /// Override the timer tick + /// + /// + /// + protected override void timer_Tick(object sender, EventArgs e) + { + if (!_detectEnd || Expired) + { + // We're not end detect, so we pass the timer through + base.timer_Tick(sender, e); + } + } + } +} diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index cde82bac..7138afbe 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -1,12 +1,27 @@ -using CefSharp.Wpf; +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using CefSharp.Wpf; using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Threading; +using System.Windows.Media.Animation; namespace XiboClient.Rendering { @@ -79,6 +94,8 @@ private void WebView_Loaded(object sender, System.Windows.RoutedEventArgs e) { // Show the browser after some time webView.Visibility = System.Windows.Visibility.Visible; + + //this.TransitionIn(); } } @@ -111,5 +128,20 @@ public override void Stop() base.Stop(); } + + /// + /// Override for Make File Substitutions + /// For CEF we set Background to Transparent + /// + /// + /// + protected override string MakeHtmlSubstitutions(string cachedFile) + { + string html = cachedFile.Replace("", ""); + html = html.Replace("[[ViewPortWidth]]", WidthIntended.ToString()); + html += ""; + html += ""; + return html; + } } } diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs deleted file mode 100644 index 5ffe3fb0..00000000 --- a/Rendering/WebEdge.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Microsoft.Toolkit.Wpf.UI.Controls; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace XiboClient.Rendering -{ - class WebEdge : WebMedia - { - private WebView mWebView; - - public WebEdge(RegionOptions options) - : base(options) - { - } - - /// - /// Render Media - /// - public override void RenderMedia() - { - // Create the web view we will use - mWebView = new WebView(); - - ((ISupportInitialize)mWebView).BeginInit(); - - mWebView.Width = Width; - mWebView.Height = Height; - mWebView.Visibility = System.Windows.Visibility.Hidden; - mWebView.IsPrivateNetworkClientServerCapabilityEnabled = true; - mWebView.NavigationCompleted += MWebView_NavigationCompleted; - mWebView.DOMContentLoaded += MWebView_DOMContentLoaded; - - this.MediaScene.Children.Add(mWebView); - - ((ISupportInitialize)mWebView).EndInit(); - - // _webBrowser.ScrollBarsEnabled = false; - // _webBrowser.ScriptErrorsSuppressed = true; - - HtmlUpdatedEvent += WebMediaHtmlUdatedEvent; - - if (IsNativeOpen()) - { - // Navigate directly - mWebView.Navigate(_filePath); - } - else if (HtmlReady()) - { - // Write to temporary file - ReadControlMeta(); - - // Navigate to temp file - mWebView.Navigate(_localWebPath); - } - else - { - Debug.WriteLine("HTML Resource is not ready to be shown (meaning the file doesn't exist at all) - wait for the download the occur and then show"); - } - - // Render media shows the controls and starts timers, etc - base.RenderMedia(); - } - - /// - /// DOM content loaded event - /// - /// - /// - private void MWebView_DOMContentLoaded(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlDOMContentLoadedEventArgs e) - { - Debug.WriteLine(DateTime.Now.ToLongTimeString() + " DOM content loaded", "EdgeWebView"); - } - - /// - /// Navigation completed event - this is the last event we get and signifies the page has loaded completely - /// - /// - /// - private void MWebView_NavigationCompleted(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlNavigationCompletedEventArgs e) - { - if (e.IsSuccess) - { - Debug.WriteLine(DateTime.Now.ToLongTimeString() + " Navigate Completed to " + e.Uri, "EdgeWebView"); - - DocumentCompleted(); - - if (!Expired) - { - // Show the browser - mWebView.Visibility = System.Windows.Visibility.Visible; - } - } - else - { - Trace.WriteLine(new LogMessage("EdgeWebMedia", "Cannot navigate to " + e.Uri + ". e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); - - // This should exipre the media - Duration = 5; - base.RenderMedia(); - } - } - - /// - /// The HTML for this Widget has been updated - /// - /// - private void WebMediaHtmlUdatedEvent(string url) - { - if (mWebView != null) - { - mWebView.Navigate(url); - } - } - } -} diff --git a/Rendering/WebIe.cs b/Rendering/WebIe.cs index 53f3fe8d..8569bbbc 100644 --- a/Rendering/WebIe.cs +++ b/Rendering/WebIe.cs @@ -1,9 +1,25 @@ -using System; -using System.Collections.Generic; +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Controls; namespace XiboClient.Rendering @@ -12,9 +28,18 @@ class WebIe : WebMedia { private WebBrowser _webBrowser; + private string backgroundColor; + private string backgroundImage; + private int backgroundLeft; + private int backgroundTop; + public WebIe(RegionOptions options) : base(options) { + this.backgroundColor = options.Dictionary.Get("backgroundColor", options.backgroundColor); + this.backgroundImage = options.backgroundImage; + this.backgroundLeft = options.backgroundLeft; + this.backgroundTop = options.backgroundTop; } /// @@ -78,5 +103,40 @@ private void IeWebMedia_HtmlUpdatedEvent(string url) _webBrowser.Navigate(url); } } + + public override void Stop() + { + this._webBrowser.Navigated -= _webBrowser_Navigated; + this._webBrowser.Dispose(); + + base.Stop(); + } + + /// + /// Override for Make File Substitutions + /// For CEF we set Background to Transparent + /// + /// + /// + protected override string MakeHtmlSubstitutions(string cachedFile) + { + // Handle the background + String bodyStyle; + + if (this.backgroundImage == null || this.backgroundImage == "") + { + bodyStyle = "background-color:" + backgroundColor + " ;"; + } + else + { + bodyStyle = "background-image: url('" + this.backgroundImage + "'); background-attachment:fixed; background-color:" + this.backgroundColor + "; background-repeat: no-repeat; background-position: " + this.backgroundLeft + "px " + this.backgroundTop + "px;"; + } + + string html = cachedFile.Replace("", ""); + html = html.Replace("[[ViewPortWidth]]", WidthIntended.ToString()); + html += ""; + html += ""; + return html; + } } } diff --git a/Rendering/WebMedia.cs b/Rendering/WebMedia.cs index 8154f9c3..e41151cd 100644 --- a/Rendering/WebMedia.cs +++ b/Rendering/WebMedia.cs @@ -19,14 +19,10 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace XiboClient.Rendering { @@ -316,22 +312,7 @@ private void xmds_GetResourceCompleted(object sender, XiboClient.xmds.GetResourc string cachedFile = e.Result; // Handle the background - /*String bodyStyle; - String backgroundColor = _options.Dictionary.Get("backgroundColor", _options.backgroundColor); - - if (_options.backgroundImage == null || _options.backgroundImage == "") - { - bodyStyle = "background-color:" + backgroundColor + " ;"; - } - else - { - bodyStyle = "background-image: url('" + _options.backgroundImage + "'); background-attachment:fixed; background-color:" + backgroundColor + "; background-repeat: no-repeat; background-position: " + _options.backgroundLeft + "px " + _options.backgroundTop + "px;"; - }*/ - - string html = cachedFile.Replace("", ""); - html = html.Replace("[[ViewPortWidth]]", Width.ToString()); - html += ""; - html += ""; + string html = MakeHtmlSubstitutions(cachedFile); // Comment in to write out the update date at the end of the file (in the body) // This is useful if you want to check how frequently the file is updating @@ -396,11 +377,9 @@ private void UpdateCacheIfNecessary() cachedFile = Regex.Replace(cachedFile, "", ""); cachedFile = Regex.Replace(cachedFile, "", ""); cachedFile = Regex.Replace(cachedFile, "", ""); - - string html = cachedFile.Replace("", ""); - html = html.Replace("[[ViewPortWidth]]", Width.ToString()); - html += ""; - html += ""; + + /// File should be back to its original form, ready to run through the subs again + string html = MakeHtmlSubstitutions(cachedFile); // Write to the library using (FileStream fileStream = File.Open(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) @@ -414,6 +393,13 @@ private void UpdateCacheIfNecessary() } } + /// + /// Make Substitutions to the Cached File + /// + /// + /// + protected abstract string MakeHtmlSubstitutions(string cachedFile); + /// /// Pulls the duration out of the temporary file and sets the media Duration to the same /// diff --git a/Resources/HtmlTemplate.htm b/Resources/HtmlTemplate.htm deleted file mode 100644 index 02c616e1..00000000 --- a/Resources/HtmlTemplate.htm +++ /dev/null @@ -1,384 +0,0 @@ - - - - Xibo Open Source Digital Signage - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Stats/Stat.cs b/Stats/Stat.cs index 02ddf318..2214414f 100644 --- a/Stats/Stat.cs +++ b/Stats/Stat.cs @@ -19,10 +19,6 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace XiboClient.Stats { diff --git a/Stats/StatLog.cs b/Stats/StatLog.cs index 252510be..68b30de1 100644 --- a/Stats/StatLog.cs +++ b/Stats/StatLog.cs @@ -18,14 +18,10 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text; -using System.IO; -using System.Xml; using System.Diagnostics; -using System.Threading; -using System.Net; +using System.IO; +using System.Text; namespace XiboClient.Stats { @@ -51,7 +47,7 @@ private StatLog() /// public void RecordStat(Stat stat) { - if (!ApplicationSettings.Default.StatsEnabled || !stat.isEnabled) + if (!ApplicationSettings.Default.StatsEnabled || !stat.isEnabled) return; Debug.WriteLine(String.Format("Recording a Stat Record. Current Count = {0}", _stats.Count.ToString()), LogType.Audit.ToString()); @@ -75,7 +71,7 @@ public void Flush() Debug.WriteLine(new LogMessage("Flush", String.Format("IN")), LogType.Audit.ToString()); // Determine if there is anything to flush - if (_stats.Count < 1) + if (_stats.Count < 1) return; // Flush to File diff --git a/XiboClient.csproj b/XiboClient.csproj index db9bd06e..98db924c 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -129,11 +129,15 @@ + Media.xaml + + + @@ -230,7 +234,6 @@ - diff --git a/XmdsAgents/FileAgent.cs b/XmdsAgents/FileAgent.cs index c481cc0c..ce302167 100644 --- a/XmdsAgents/FileAgent.cs +++ b/XmdsAgents/FileAgent.cs @@ -19,14 +19,11 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using XiboClient.Properties; using System.Diagnostics; using System.IO; using System.Net; -using System.Net.Mime; +using System.Text; +using System.Threading; namespace XiboClient.XmdsAgents { @@ -111,7 +108,7 @@ public Semaphore FileDownloadLimit /// public FileAgent() { - + } /// diff --git a/XmdsAgents/LibraryAgent.cs b/XmdsAgents/LibraryAgent.cs index 33913434..f77879b2 100644 --- a/XmdsAgents/LibraryAgent.cs +++ b/XmdsAgents/LibraryAgent.cs @@ -20,13 +20,9 @@ */ using System; using System.Collections.Generic; -using System.Text; -using System.Threading; -using XiboClient.Properties; using System.Diagnostics; -using System.Xml; -using XiboClient.Log; using System.IO; +using System.Threading; /// 09/04/12 Dan Created @@ -110,14 +106,14 @@ public void Run() // Build a list of files in the library DirectoryInfo directory = new DirectoryInfo(ApplicationSettings.Default.LibraryPath); - + // Check each one and see if it is in required files foreach (FileInfo fileInfo in directory.GetFiles()) { // Never delete certain system files // Also do not delete log/stat files as they are managed by their respective agents - if (_persistentFiles.Contains(fileInfo.Name) || - fileInfo.Name.Contains(ApplicationSettings.Default.LogLocation) || + if (_persistentFiles.Contains(fileInfo.Name) || + fileInfo.Name.Contains(ApplicationSettings.Default.LogLocation) || fileInfo.Name.Contains(ApplicationSettings.Default.StatsLogFile) ) continue; diff --git a/XmdsAgents/LogAgent.cs b/XmdsAgents/LogAgent.cs index eda09924..01f0dc12 100644 --- a/XmdsAgents/LogAgent.cs +++ b/XmdsAgents/LogAgent.cs @@ -18,10 +18,8 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Text; using System.Threading; @@ -114,7 +112,7 @@ private void ProcessFiles(xmds.xmds xmds, string key, string type) { // Test for old files DateTime testDate = DateTime.Now.AddDays(ApplicationSettings.Default.LibraryAgentInterval * -1); - + // Track processed files int filesProcessed = 0; diff --git a/XmdsAgents/RegisterAgent.cs b/XmdsAgents/RegisterAgent.cs index 20e90d3a..64991ecf 100644 --- a/XmdsAgents/RegisterAgent.cs +++ b/XmdsAgents/RegisterAgent.cs @@ -18,14 +18,11 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; using System.Diagnostics; -using XiboClient.Log; +using System.Net; +using System.Threading; using System.Xml; using XiboClient.Logic; -using System.Net; namespace XiboClient.XmdsAgents { @@ -107,7 +104,7 @@ public void Run() // Have we been asked to move CMS instance? // CMS MOVE // -------- - if (!string.IsNullOrEmpty(ApplicationSettings.Default.NewCmsAddress) + if (!string.IsNullOrEmpty(ApplicationSettings.Default.NewCmsAddress) && !string.IsNullOrEmpty(ApplicationSettings.Default.NewCmsKey) && ApplicationSettings.Default.NewCmsAddress != ApplicationSettings.Default.ServerUri ) @@ -215,7 +212,7 @@ public static string ProcessRegisterXml(string xml) // Load the result into an XML document XmlDocument result = new XmlDocument(); result.LoadXml(xml); - + // Test the XML if (result.DocumentElement.Attributes["code"].Value == "READY") { diff --git a/XmdsAgents/ScheduleAgent.cs b/XmdsAgents/ScheduleAgent.cs index a3908193..7a8af41d 100644 --- a/XmdsAgents/ScheduleAgent.cs +++ b/XmdsAgents/ScheduleAgent.cs @@ -18,13 +18,10 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using XiboClient.Properties; using System.Diagnostics; -using XiboClient.Log; using System.Net; +using System.Threading; +using XiboClient.Log; /// 17/02/12 Dan Created /// 20/02/12 Dan Added ClientInfo diff --git a/XmdsAgents/ScheduleAndFilesAgent.cs b/XmdsAgents/ScheduleAndFilesAgent.cs index 7cab8982..4c525b70 100644 --- a/XmdsAgents/ScheduleAndFilesAgent.cs +++ b/XmdsAgents/ScheduleAndFilesAgent.cs @@ -18,18 +18,17 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ +using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Net; using System.Text; using System.Threading; -using XiboClient.Properties; -using System.Diagnostics; using System.Xml; using XiboClient.Log; -using System.Net; -using System.Globalization; -using System.IO; -using Newtonsoft.Json; /// 17/02/12 Dan Created /// 20/02/12 Dan Added ClientInfo @@ -136,7 +135,7 @@ public void Run() { // Run the schedule Agent thread scheduleAgent(); - + if (ApplicationSettings.Default.InDownloadWindow) { try @@ -360,7 +359,7 @@ private void reportStorage() break; } } - + writer.WriteEndObject(); // Report diff --git a/packages.config b/packages.config deleted file mode 100644 index 0af3eb40..00000000 --- a/packages.config +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file From f402942f0461c1abd906909fbea57eefaf2ec2a3 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 8 Feb 2020 15:08:16 +0000 Subject: [PATCH 08/29] Options form implementation --- Logic/ApplicationSettings.cs | 2 +- OptionsForm.xaml | 63 +++++++++- OptionsForm.xaml.cs | 202 ++++++++++++++++++++++++++++++- Properties/Resources.Designer.cs | 20 +++ Properties/Resources.resx | 3 + Resources/licence.txt | 2 +- XiboClient.csproj | 3 + 7 files changed, 285 insertions(+), 10 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index d007d126..cbffc7dd 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -340,7 +340,7 @@ public string LibraryPath { get { - if (_libraryPath == "DEFAULT") + if (string.IsNullOrEmpty(_libraryPath) || _libraryPath == "DEFAULT") { // Get the users document space for a library _libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + @"\" + GetProductNameFromAssembly() + " Library"; diff --git a/OptionsForm.xaml b/OptionsForm.xaml index 7e71a5ec..bfe05a0c 100644 --- a/OptionsForm.xaml +++ b/OptionsForm.xaml @@ -1,12 +1,69 @@ - + Title="Player Options" Height="450" Width="800" ResizeMode="NoResize"> - + + + + + + + + + + + + + public partial class OptionsForm : Window { + private xmds.xmds xmds; + + private HardwareKey hardwareKey; + public OptionsForm() { InitializeComponent(); - } + // Create a new XMDS + this.xmds = new xmds.xmds(); + this.xmds.RegisterDisplayCompleted += Xmds_RegisterDisplayCompleted; + + // Create a Hardware key + this.hardwareKey = new HardwareKey(); + + // Set the fields up with the current settings + // Settings Tab + textBoxCmsAddress.Text = ApplicationSettings.Default.ServerUri; + textBoxCmsKey.Text = ApplicationSettings.Default.ServerKey; + textBoxLibraryPath.Text = ApplicationSettings.Default.LibraryPath; + textBoxHardwareKey.Text = ApplicationSettings.Default.HardwareKey; + + // Proxy Tab + textBoxProxyUser.Text = ApplicationSettings.Default.ProxyUser; + textBoxProxyPass.Password = ApplicationSettings.Default.ProxyPassword; + textBoxProxyDomain.Text = ApplicationSettings.Default.ProxyDomain; + + // Appearance Tab + textBoxSplashScreenReplacement.Text = ApplicationSettings.Default.SplashOverride; + + // Switch to TLS 2.1 + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + + // Populate the About tab + textBoxLicence.AppendText(Properties.Resources.licence); + labelPlayerVersion.Content = ApplicationSettings.Default.ClientVersion + " R" + ApplicationSettings.Default.ClientCodeVersion; + } /// /// Sets up the global proxy /// public static void SetGlobalProxy() { - Debug.WriteLine("[IN]", "SetGlobalProxy"); - - Debug.WriteLine("Trying to detect a proxy.", "SetGlobalProxy"); + Debug.WriteLine("[IN] Trying to detect a proxy.", "SetGlobalProxy"); if (ApplicationSettings.Default.ProxyUser != "") { @@ -69,5 +123,143 @@ public static void SetGlobalProxy() return; } + + /// + /// + /// + /// + /// + private void Button_Exit_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + private void Button_Connect_Click(object sender, RoutedEventArgs e) + { + textBoxStatus.Clear(); + try + { + textBoxStatus.AppendText("Saving with CMS... Please wait..."); + buttonConnect.IsEnabled = false; + + // Simple settings + ApplicationSettings.Default.ServerUri = textBoxCmsAddress.Text; + ApplicationSettings.Default.ServerKey = textBoxCmsKey.Text; + ApplicationSettings.Default.LibraryPath = textBoxLibraryPath.Text.TrimEnd('\\'); + ApplicationSettings.Default.HardwareKey = textBoxHardwareKey.Text; + + // Also tweak the address of the xmds1 + this.xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=registerDisplay"; + + // Proxy Settings + ApplicationSettings.Default.ProxyUser = textBoxProxyUser.Text; + ApplicationSettings.Default.ProxyPassword = textBoxProxyPass.Password; + ApplicationSettings.Default.ProxyDomain = textBoxProxyDomain.Text; + + // Change the default Proxy class + SetGlobalProxy(); + + // Client settings + ApplicationSettings.Default.SplashOverride = textBoxSplashScreenReplacement.Text; + + // Commit these changes back to the user settings + ApplicationSettings.Default.Save(); + + // Call register + this.xmds.RegisterDisplayAsync( + ApplicationSettings.Default.ServerKey, + ApplicationSettings.Default.HardwareKey, + ApplicationSettings.Default.DisplayName, + "windows", + ApplicationSettings.Default.ClientVersion, + ApplicationSettings.Default.ClientCodeVersion, + Environment.OSVersion.ToString(), + this.hardwareKey.MacAddress, + this.hardwareKey.Channel, + this.hardwareKey.getXmrPublicKey()); + } + catch (Exception ex) + { + textBoxStatus.AppendText(ex.Message); + } + } + + /// + /// Register Display has been completed + /// + /// + /// + private void Xmds_RegisterDisplayCompleted(object sender, xmds.RegisterDisplayCompletedEventArgs e) + { + buttonConnect.IsEnabled = true; + + textBoxStatus.Clear(); + + if (e.Error != null) + { + textBoxStatus.AppendText("Status" + Environment.NewLine); + textBoxStatus.AppendText(e.Error.Message); + + Debug.WriteLine("Error returned from Call to XMDS Register Display.", "xmds1_RegisterDisplayCompleted"); + Debug.WriteLine(e.Error.Message, "xmds1_RegisterDisplayCompleted"); + Debug.WriteLine(e.Error.StackTrace, "xmds1_RegisterDisplayCompleted"); + } + else + { + textBoxStatus.AppendText(RegisterAgent.ProcessRegisterXml(e.Result)); + } + } + + private void Button_LibraryBrowse_Click(object sender, RoutedEventArgs e) + { + VistaFolderBrowserDialog dialog = new VistaFolderBrowserDialog(); + dialog.Description = "Select a folder to store the Player's downloaded files."; + dialog.UseDescriptionForTitle = true; + + if ((bool)dialog.ShowDialog(this)) + { + textBoxLibraryPath.Text = dialog.SelectedPath; + } + } + + private void Button_SplashScreenReplacement_Click(object sender, RoutedEventArgs e) + { + OpenFileDialog dialog = new OpenFileDialog(); + + // Set filter for file extension and default file extension + dialog.DefaultExt = ".png"; + dialog.Filter = "JPEG Files (*.jpeg)|*.jpeg|PNG Files (*.png)|*.png|JPG Files (*.jpg)|*.jpg"; + + + // Display OpenFileDialog by calling ShowDialog method + Nullable result = dialog.ShowDialog(); + + // Get the selected file name and display in a TextBox + if (result == true) + { + // Open document + string filename = dialog.FileName; + textBoxSplashScreenReplacement.Text = filename; + } + } + + private void Button_DisplayAdmin_Click(object sender, RoutedEventArgs e) + { + // open URL in separate instance of default browser + try + { + Process.Start(ApplicationSettings.Default.ServerUri + @"/index.php?p=display"); + } + catch + { + MessageBox.Show("No web browser installed"); + } + } + + private void Button_LaunchPlayer_Click(object sender, RoutedEventArgs e) + { + Close(); + Process.Start(Process.GetCurrentProcess().MainModule.FileName); + } } } diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index 90c7c9a4..1b005dc3 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -60,6 +60,26 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to Xibo - Digital Signage - http://www.xibo.org.uk + ///Copyright (C) 2020 Xibo Signage Ltd + /// + ///Xibo is free software: you can redistribute it and/or modify + ///it under the terms of the GNU Affero General Public License as published by + ///the Free Software Foundation, either version 3 of the License, or + ///any later version. + /// + ///Xibo is distributed in the hope that it will be useful, + ///but WITHOUT ANY WARRANTY; without even the implied warranty of + ///MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ///GNU Affero [rest of string was truncated]";. + /// + internal static string licence { + get { + return ResourceManager.GetString("licence", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/Properties/Resources.resx b/Properties/Resources.resx index 6c2e23ce..76edce59 100644 --- a/Properties/Resources.resx +++ b/Properties/Resources.resx @@ -118,6 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\licence.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + ..\Resources\logo.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/Resources/licence.txt b/Resources/licence.txt index 01d5cc4e..46343978 100644 --- a/Resources/licence.txt +++ b/Resources/licence.txt @@ -1,5 +1,5 @@ Xibo - Digital Signage - http://www.xibo.org.uk -Copyright (C) 2006-2017 Xibo Signage Ltd +Copyright (C) 2020 Xibo Signage Ltd Xibo is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/XiboClient.csproj b/XiboClient.csproj index 98db924c..00432204 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -273,6 +273,9 @@ 2.4.7 + + 1.1.0 + 4.7.0 From d4ad38691d6e62bda7c58b00033c353498805ace Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 8 Feb 2020 17:13:53 +0000 Subject: [PATCH 09/29] Add InfoScreen --- Action/XmrSubscriber.cs | 2 +- App.xaml.cs | 50 +++++++++------ InfoScreen.xaml | 37 +++++++++++ InfoScreen.xaml.cs | 105 ++++++++++++++++++++++++++++++++ Log/ClientInfo.cs | 72 ++++++++++++---------- Log/ClientInfoTraceListener.cs | 13 ++-- Log/ConcurrentCircularBuffer.cs | 41 +++++++++++++ Log/LogMessage.cs | 14 ++--- Logic/MouseInterceptor.cs | 1 - Logic/ScheduleManager.cs | 2 +- MainWindow.xaml.cs | 98 +++++++++++++++++------------ XiboClient.csproj | 8 +++ 12 files changed, 333 insertions(+), 110 deletions(-) create mode 100644 InfoScreen.xaml create mode 100644 InfoScreen.xaml.cs create mode 100644 Log/ConcurrentCircularBuffer.cs diff --git a/Action/XmrSubscriber.cs b/Action/XmrSubscriber.cs index cbe677ee..06bfa4bd 100644 --- a/Action/XmrSubscriber.cs +++ b/Action/XmrSubscriber.cs @@ -240,7 +240,7 @@ private void processMessage(NetMQMessage message, AsymmetricCipherKeyPair rsaKey case "screenShot": ScreenShot.TakeAndSend(); - ClientInfo.Instance.notifyStatusToXmds(); + ClientInfo.Instance.NotifyStatusToXmds(); break; default: diff --git a/App.xaml.cs b/App.xaml.cs index 66af7e3c..bb3bf61b 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -50,20 +50,12 @@ protected override void OnStartup(StartupEventArgs e) // Preview the screen saver case "/p": // args[1] is the handle to the preview window - KeyInterceptor.SetHook(); - MouseInterceptor.SetHook(); RunClient(new IntPtr(long.Parse(e.Args[1]))); - KeyInterceptor.UnsetHook(); - MouseInterceptor.UnsetHook(); break; // Show the screen saver case "/s": - KeyInterceptor.SetHook(); - MouseInterceptor.SetHook(); RunClient(true); - KeyInterceptor.UnsetHook(); - MouseInterceptor.UnsetHook(); break; // Configure the screesaver's settings @@ -74,11 +66,7 @@ protected override void OnStartup(StartupEventArgs e) // Show the screen saver default: - KeyInterceptor.SetHook(); - MouseInterceptor.SetHook(); RunClient(true); - KeyInterceptor.UnsetHook(); - MouseInterceptor.UnsetHook(); break; } } @@ -98,13 +86,6 @@ protected override void OnStartup(StartupEventArgs e) Trace.Flush(); } - private static void RunClient() - { - Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); - MainWindow windowMain = new MainWindow(); - windowMain.ShowDialog(); - } - private static void RunSettings() { // If we are showing the options form, enable visual styles @@ -112,11 +93,42 @@ private static void RunSettings() windowMain.ShowDialog(); } + /// + /// Run the Player + /// + private static void RunClient() + { + RunClient(false); + } + + /// + /// Run the Player + /// + /// private static void RunClient(bool screenSaver) { Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); + + KeyInterceptor.SetHook(); + if (screenSaver) + { + MouseInterceptor.SetHook(); + } + + MainWindow windowMain = new MainWindow(screenSaver); + windowMain.ShowDialog(); + + KeyInterceptor.UnsetHook(); + if (screenSaver) + { + MouseInterceptor.UnsetHook(); + } } + /// + /// Run the Player + /// + /// private static void RunClient(IntPtr previewWindow) { Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); diff --git a/InfoScreen.xaml b/InfoScreen.xaml new file mode 100644 index 00000000..0e69c1b1 --- /dev/null +++ b/InfoScreen.xaml @@ -0,0 +1,37 @@ + + + + + + diff --git a/InfoScreen.xaml.cs b/InfoScreen.xaml.cs new file mode 100644 index 00000000..05f0b9e1 --- /dev/null +++ b/InfoScreen.xaml.cs @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using System.Windows.Threading; +using XiboClient.Log; + +namespace XiboClient +{ + /// + /// Interaction logic for InfoScreen.xaml + /// + public partial class InfoScreen : Window + { + private DispatcherTimer timer; + + public InfoScreen() + { + InitializeComponent(); + + Loaded += InfoScreen_Loaded; + Unloaded += InfoScreen_Unloaded; + } + + private void InfoScreen_Unloaded(object sender, RoutedEventArgs e) + { + // Unbind events + Loaded -= InfoScreen_Loaded; + Unloaded -= InfoScreen_Unloaded; + + // Stop the Timer + this.timer.Tick -= Timer_Tick; + this.timer.Stop(); + } + + private void InfoScreen_Loaded(object sender, RoutedEventArgs e) + { + // Update + Update(); + + // Create a timer to update the info screen + timer = new DispatcherTimer() + { + Interval = TimeSpan.FromSeconds(5) + }; + timer.Tick += Timer_Tick; + timer.Start(); + } + + private void Timer_Tick(object sender, EventArgs e) + { + Update(); + } + + /// + /// Update from ClientInfo + /// + private void Update() + { + labelScheduleStatus.Content = ClientInfo.Instance.ScheduleStatus; + labelRequiredFilesStatus.Content = ClientInfo.Instance.RequiredFilesStatus; + labelXmrStatus.Content = ClientInfo.Instance.XmrSubscriberStatus; + labelCurrentlyPlaying.Content = ClientInfo.Instance.CurrentlyPlaying; + labelControlCount.Content = ClientInfo.Instance.ControlCount; + + textBoxSchedule.Text = ClientInfo.Instance.ScheduleManagerStatus; + textBoxRequiredFiles.Text = ClientInfo.Instance.RequiredFilesList; + + // Log grid + foreach (LogMessage message in ClientInfo.Instance.LogMessages.Read()) + { + logDataGridView.Items.Add(message); + } + } + } +} diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index 1398903d..ba63bf40 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -1,4 +1,24 @@ -using Newtonsoft.Json; +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using Newtonsoft.Json; using System; using System.Diagnostics; using System.IO; @@ -26,6 +46,11 @@ private static readonly Lazy ///
public string RequiredFilesStatus; + /// + /// Set the required files List + /// + public string RequiredFilesList; + /// /// Set the schedule manager status /// @@ -47,26 +72,21 @@ private static readonly Lazy public int ControlCount; /// - /// The title + /// What is currently playing /// - private string Title; + public string CurrentlyPlaying { get; set; } /// - /// Client Info Object + /// Log messages /// - private ClientInfo() - { - // Put the XMDS url on the title window - Title = "Player Information and Status - " + ApplicationSettings.Default.ServerUri; - } + public ConcurrentCircularBuffer LogMessages; /// - /// Sets the currently playing layout name + /// Client Info Object /// - /// - public void SetCurrentlyPlaying(string layoutName) + private ClientInfo() { - Title = "Client Information and Status - " + ApplicationSettings.Default.ServerUri + " - Currently Showing: " + layoutName; + this.LogMessages = new ConcurrentCircularBuffer(10); } /// @@ -75,20 +95,7 @@ public void SetCurrentlyPlaying(string layoutName) /// public void AddToLogGrid(string message, LogType logType) { - /*if (InvokeRequired) - { - BeginInvoke(new AddLogMessage(AddToLogGrid), new object[] { message, logType }); - return; - } - - // Prevent the log grid getting too large (clear at 500 messages) - if (logDataGridView.RowCount > 500) - logDataGridView.Rows.Clear(); - - int newRow = logDataGridView.Rows.Add(); - LogMessage logMessage; - try { logMessage = new LogMessage(message); @@ -98,11 +105,7 @@ public void AddToLogGrid(string message, LogType logType) logMessage = new LogMessage("Unknown", message); } - logDataGridView.Rows[newRow].Cells[0].Value = logMessage._thread; - logDataGridView.Rows[newRow].Cells[1].Value = logMessage.LogDate.ToString(); - logDataGridView.Rows[newRow].Cells[2].Value = logType.ToString(); - logDataGridView.Rows[newRow].Cells[3].Value = logMessage._method; - logDataGridView.Rows[newRow].Cells[4].Value = logMessage._message;*/ + this.LogMessages.Put(logMessage); } /// @@ -110,7 +113,7 @@ public void AddToLogGrid(string message, LogType logType) /// public void UpdateRequiredFiles(string requiredFilesString) { - RequiredFilesStatus = requiredFilesString; + RequiredFilesList = requiredFilesString; } /// @@ -129,7 +132,10 @@ public void UpdateStatusMarkerFile() } } - public void notifyStatusToXmds() + /// + /// Notify Status to XMDS + /// + public void NotifyStatusToXmds() { try { diff --git a/Log/ClientInfoTraceListener.cs b/Log/ClientInfoTraceListener.cs index a76bb5bd..8e9409c2 100644 --- a/Log/ClientInfoTraceListener.cs +++ b/Log/ClientInfoTraceListener.cs @@ -4,11 +4,8 @@ namespace XiboClient.Log { class ClientInfoTraceListener : TraceListener { - private ClientInfo _clientInfo; - - public ClientInfoTraceListener(ClientInfo clientInfo) + public ClientInfoTraceListener() { - _clientInfo = clientInfo; } /// @@ -38,7 +35,7 @@ public override void Write(string message) if (ApplicationSettings.Default.LogLevel != "audit") return; - _clientInfo.AddToLogGrid(message, LogType.Audit); + ClientInfo.Instance.AddToLogGrid(message, LogType.Audit); } public override void Write(object o) @@ -46,7 +43,7 @@ public override void Write(object o) if (ApplicationSettings.Default.LogLevel != "audit") return; - _clientInfo.AddToLogGrid(o.ToString(), LogType.Audit); + ClientInfo.Instance.AddToLogGrid(o.ToString(), LogType.Audit); } public override void Write(string message, string category) @@ -60,7 +57,7 @@ public override void Write(string message, string category) if (ApplicationSettings.Default.LogLevel == "info" && (logtype != LogType.Error && logtype != LogType.Info)) return; - _clientInfo.AddToLogGrid(message, logtype); + ClientInfo.Instance.AddToLogGrid(message, logtype); } public override void Write(object o, string category) @@ -74,7 +71,7 @@ public override void Write(object o, string category) if (ApplicationSettings.Default.LogLevel == "info" && (logtype != LogType.Error && logtype != LogType.Info)) return; - _clientInfo.AddToLogGrid(o.ToString(), logtype); + ClientInfo.Instance.AddToLogGrid(o.ToString(), logtype); } public override void WriteLine(string message) diff --git a/Log/ConcurrentCircularBuffer.cs b/Log/ConcurrentCircularBuffer.cs new file mode 100644 index 00000000..34a1bd23 --- /dev/null +++ b/Log/ConcurrentCircularBuffer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Log +{ + /// + /// A buffer for holding log messages + /// With thanks: https://codereview.stackexchange.com/a/135565 + /// + public sealed class ConcurrentCircularBuffer + { + private readonly LinkedList _buffer; + private int _maxItemCount; + + public ConcurrentCircularBuffer(int maxItemCount) + { + _maxItemCount = maxItemCount; + _buffer = new LinkedList(); + } + + public void Put(LogMessage item) + { + lock (_buffer) + { + _buffer.AddFirst(item); + if (_buffer.Count > _maxItemCount) + { + _buffer.RemoveLast(); + } + } + } + + public IEnumerable Read() + { + lock (_buffer) { return _buffer.ToArray(); } + } + } +} diff --git a/Log/LogMessage.cs b/Log/LogMessage.cs index 26ed0c83..0f13265a 100644 --- a/Log/LogMessage.cs +++ b/Log/LogMessage.cs @@ -26,13 +26,13 @@ namespace XiboClient { public class LogMessage { - public string _thread; - public string _method; - public string _message; - public int _scheduleId; - public int _layoutId; - public int _mediaId; - public DateTime LogDate; + public string _thread { get; set; } + public string _method { get; set; } + public string _message { get; set; } + public int _scheduleId { get; set; } + public int _layoutId { get; set; } + public int _mediaId { get; set; } + public DateTime LogDate { get; set; } public LogMessage(String method, String message) { diff --git a/Logic/MouseInterceptor.cs b/Logic/MouseInterceptor.cs index 19848536..677ee380 100644 --- a/Logic/MouseInterceptor.cs +++ b/Logic/MouseInterceptor.cs @@ -43,7 +43,6 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) if (MouseMessages.WM_LBUTTONDOWN == (MouseMessages)wParam || MouseMessages.WM_MOUSEMOVE == (MouseMessages)wParam) { MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT)); - Console.WriteLine(hookStruct.pt.x + ", " + hookStruct.pt.y); if (Math.Abs(_mouseLocation.X - hookStruct.pt.x) > 5 || Math.Abs(_mouseLocation.Y - hookStruct.pt.y) > 5) { diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index 97dbce8e..b9ac1f60 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -212,7 +212,7 @@ public void Run() _lastScreenShotDate = DateTime.Now; // Notify status to XMDS - ClientInfo.Instance.notifyStatusToXmds(); + ClientInfo.Instance.NotifyStatusToXmds(); } // Run any commands that occur in the next 10 seconds. diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index ff70143e..f62e19b2 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -26,6 +26,7 @@ using System.Net; using System.Runtime.InteropServices; using System.Windows; +using System.Windows.Forms; using System.Windows.Input; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; @@ -80,6 +81,11 @@ public partial class MainWindow : Window private delegate void ChangeToNextLayoutDelegate(string layoutPath); private delegate void ManageOverlaysDelegate(Collection overlays); + /// + /// The InfoScreen + /// + private InfoScreen infoScreen; + #region DLL Imports [FlagsAttribute] @@ -153,12 +159,6 @@ public MainWindow(bool screenSaver) InitializeXibo(); } - public MainWindow() - { - InitializeComponent(); - InitializeXibo(); - } - private void InitializeXibo() { // Set the title @@ -183,34 +183,16 @@ private void InitializeXibo() // Show in taskbar ShowInTaskbar = ApplicationSettings.Default.ShowInTaskbar; - // Setup the proxy information - OptionsForm.SetGlobalProxy(); - // Events Loaded += MainWindow_Loaded; Closing += MainForm_FormClosing; ContentRendered += MainForm_Shown; - // Define the hotkey - /*Keys key; - try - { - key = (Keys)Enum.Parse(typeof(Keys), ApplicationSettings.Default.ClientInformationKeyCode.ToUpper()); - } - catch - { - // Default back to I - key = Keys.I; - } - - KeyStore.Instance.AddKeyDefinition("ClientInfo", key, ((ApplicationSettings.Default.ClientInfomationCtrlKey) ? Keys.Control : Keys.None)); - - // Register a handler for the key event - KeyStore.Instance.KeyPress += Instance_KeyPress;*/ - // Trace listener for Client Info - ClientInfoTraceListener clientInfoTraceListener = new ClientInfoTraceListener(ClientInfo.Instance); - clientInfoTraceListener.Name = "ClientInfo TraceListener"; + ClientInfoTraceListener clientInfoTraceListener = new ClientInfoTraceListener + { + Name = "ClientInfo TraceListener" + }; Trace.Listeners.Add(clientInfoTraceListener); // Log to disk? @@ -270,28 +252,35 @@ void Instance_MouseEvent() /// Handle the Key Event /// /// - /*void Instance_KeyPress(string name) + void Instance_KeyPress(string name) { Debug.WriteLine("KeyPress " + name); if (name == "ClientInfo") { - // Toggle - if (_clientInfoForm.Visible) + if (this.infoScreen == null) { - _clientInfoForm.Hide(); + this.infoScreen = new InfoScreen(); + this.infoScreen.Closed += InfoScreen_Closed; + this.infoScreen.Show(); + #if !DEBUG + // Make our window not topmost so that we can see the info screen if (!_screenSaver) - TopMost = true; + { + TopMost = False; + } #endif } else { + this.infoScreen.Close(); + #if !DEBUG if (!_screenSaver) - TopMost = false; + { + TopMost = true; + } #endif - _clientInfoForm.Show(); - _clientInfoForm.BringToFront(); } } else if (name == "ScreenSaver") @@ -302,7 +291,18 @@ void Instance_MouseEvent() Close(); } - }*/ + } + + /// + /// InfoScreen Closed + /// + /// + /// + private void InfoScreen_Closed(object sender, EventArgs e) + { + this.infoScreen.Closed -= InfoScreen_Closed; + this.infoScreen = null; + } /// /// main window loding event @@ -329,6 +329,23 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e) // Change the default Proxy class OptionsForm.SetGlobalProxy(); + // Define the hotkey + Keys key; + try + { + key = (Keys)Enum.Parse(typeof(Keys), ApplicationSettings.Default.ClientInformationKeyCode.ToUpper()); + } + catch + { + // Default back to I + key = Keys.I; + } + + KeyStore.Instance.AddKeyDefinition("ClientInfo", key, ((ApplicationSettings.Default.ClientInfomationCtrlKey) ? Keys.Control : Keys.None)); + + // Register a handler for the key event + KeyStore.Instance.KeyPress += Instance_KeyPress; + // UserApp data Debug.WriteLine(new LogMessage("MainForm_Load", "User AppData Path: " + ApplicationSettings.Default.LibraryPath), LogType.Info.ToString()); } @@ -363,7 +380,7 @@ void MainForm_Shown(object sender, EventArgs e) catch (Exception ex) { Debug.WriteLine(ex.Message, LogType.Error.ToString()); - MessageBox.Show("Fatal Error initialising the application. " + ex.Message, "Fatal Error"); + System.Windows.MessageBox.Show("Fatal Error initialising the application. " + ex.Message, "Fatal Error"); Close(); } } @@ -379,7 +396,7 @@ private void MainForm_Load(object sender, EventArgs e) if (!ApplicationSettings.Default.EnableMouse) { // Hide the cursor - Mouse.OverrideCursor = Cursors.None; + Mouse.OverrideCursor = System.Windows.Input.Cursors.None; } // Move the cursor to the starting place @@ -507,7 +524,8 @@ private void ChangeToNextLayout(string layoutPath) currentLayout.Start(); // Update client info - ClientInfo.Instance.CurrentLayoutId = layoutPath; + ClientInfo.Instance.CurrentLayoutId = _layoutId + ""; + ClientInfo.Instance.CurrentlyPlaying = layoutPath; _schedule.CurrentLayoutId = _layoutId; } catch (DefaultLayoutException) diff --git a/XiboClient.csproj b/XiboClient.csproj index 00432204..06ba7708 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -109,6 +109,9 @@ + + InfoScreen.xaml + @@ -127,6 +130,7 @@ + @@ -164,6 +168,10 @@ + + Designer + MSBuild:Compile + MSBuild:Compile Designer From a1775455cfd60a91c98707bbc96fb431f5159ab5 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 8 Feb 2020 17:42:26 +0000 Subject: [PATCH 10/29] Fixes for Release config --- App.xaml.cs | 1 - MainWindow.xaml.cs | 9 +++++---- XiboClient.csproj | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/App.xaml.cs b/App.xaml.cs index bb3bf61b..4dd47196 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -27,7 +27,6 @@ protected override void OnStartup(StartupEventArgs e) #if !DEBUG // Catch unhandled exceptions AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); - Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException); TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; #endif diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index f62e19b2..193b5a6c 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -32,6 +32,7 @@ using System.Windows.Media.Imaging; using System.Windows.Shapes; using System.Windows.Threading; +using XiboClient.Control; using XiboClient.Error; using XiboClient.Log; using XiboClient.Logic; @@ -209,7 +210,7 @@ private void InitializeXibo() try { // Update/write the status.json file - File.WriteAllText(Path.Combine(ApplicationSettings.Default.LibraryPath, "status.json"), "{\"lastActivity\":\"" + DateTime.Now.ToString() + "\"}"); + File.WriteAllText(System.IO.Path.Combine(ApplicationSettings.Default.LibraryPath, "status.json"), "{\"lastActivity\":\"" + DateTime.Now.ToString() + "\"}"); // Start watchdog WatchDogManager.Start(); @@ -267,7 +268,7 @@ void Instance_KeyPress(string name) // Make our window not topmost so that we can see the info screen if (!_screenSaver) { - TopMost = False; + Topmost = false; } #endif } @@ -278,7 +279,7 @@ void Instance_KeyPress(string name) #if !DEBUG if (!_screenSaver) { - TopMost = true; + Topmost = true; } #endif } @@ -374,7 +375,7 @@ void MainForm_Shown(object sender, EventArgs e) // Set this form to topmost #if !DEBUG if (!_screenSaver) - TopMost = true; + Topmost = true; #endif } catch (Exception ex) diff --git a/XiboClient.csproj b/XiboClient.csproj index 06ba7708..9b3551e8 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -63,6 +63,7 @@ prompt MinimumRecommendedRules.ruleset true + Off @@ -288,10 +289,10 @@ 4.7.0 - 2.4.3 + 2.6.2 - 2.6.1 + 2.6.2 From 96fdf2aacdbed358ab407a075511c587947d1d27 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sun, 9 Feb 2020 12:27:42 +0000 Subject: [PATCH 11/29] Add Cef Initialisation settings (move cache and log paths). Fix some null refernece exceptions --- Log/LogMessage.cs | 9 +++--- Logic/ApplicationSettings.cs | 15 ++++++--- Logic/CacheManager.cs | 53 ++++++++++++++++--------------- Logic/XmlHelper.cs | 46 +++++++++++++++++++++++++++ MainWindow.xaml.cs | 45 +++++++++----------------- Rendering/Layout.xaml.cs | 9 +++--- Rendering/Region.xaml.cs | 4 +++ Rendering/Spacer.cs | 39 +++++++++++++++++++++++ Rendering/WebCef.cs | 3 +- XiboClient.csproj | 6 ++++ app.manifest | 61 ++++++++++++++++++++++++++++++++++++ 11 files changed, 221 insertions(+), 69 deletions(-) create mode 100644 Logic/XmlHelper.cs create mode 100644 Rendering/Spacer.cs create mode 100644 app.manifest diff --git a/Log/LogMessage.cs b/Log/LogMessage.cs index 0f13265a..873b61ba 100644 --- a/Log/LogMessage.cs +++ b/Log/LogMessage.cs @@ -21,6 +21,7 @@ using System.Security; using System.Threading; using System.Xml; +using XiboClient.Logic; namespace XiboClient { @@ -74,10 +75,10 @@ public LogMessage(string xmlMessage) try { - LogDate = DateTime.Parse(xml.GetElementsByTagName("logdate").Item(0).InnerText.ToString()); - _message = xml.GetElementsByTagName("message").Item(0).InnerText.ToString(); - _method = xml.GetElementsByTagName("method").Item(0).InnerText.ToString(); - _thread = xml.GetElementsByTagName("thread").Item(0).InnerText.ToString(); + LogDate = DateTime.Parse(XmlHelper.SelectFirstElementInnerTextOrDefault(xml, "logdate", "1970-01-01")); + _message = XmlHelper.SelectFirstElementInnerTextOrDefault(xml, "message", ""); + _method = XmlHelper.SelectFirstElementInnerTextOrDefault(xml, "method", ""); + _thread = XmlHelper.SelectFirstElementInnerTextOrDefault(xml, "thread", ""); } catch (NullReferenceException) { diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index cbffc7dd..bdc9f1c4 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -231,12 +231,19 @@ public object this[string propertyName] get { PropertyInfo property = GetType().GetProperty(propertyName); - return property.GetValue(this, null); + return property?.GetValue(this, null); } set { PropertyInfo property = GetType().GetProperty(propertyName); - property.SetValue(this, value, null); + + if (property != null) + { + property.SetValue(this, value, null); + } else + { + Debug.WriteLine("Null Property: " + propertyName, "ApplicationSettings"); + } } } @@ -257,8 +264,8 @@ public void PopulateFromXml(XmlDocument document) { Command command = new Command(); command.Code = commandNode.Name; - command.CommandString = commandNode.SelectSingleNode("commandString").InnerText; - command.Validation = commandNode.SelectSingleNode("validationString").InnerText; + command.CommandString = XmlHelper.SelectNodeInnerTextOrDefault(commandNode, "commandString", ""); + command.Validation = XmlHelper.SelectNodeInnerTextOrDefault(commandNode, "validationString", ""); commands.Add(command); } diff --git a/Logic/CacheManager.cs b/Logic/CacheManager.cs index e999ec52..e85017b2 100644 --- a/Logic/CacheManager.cs +++ b/Logic/CacheManager.cs @@ -34,48 +34,49 @@ private static readonly Lazy new Lazy (() => new CacheManager()); - public static CacheManager Instance { get { return lazy.Value; } } + public static CacheManager Instance + => lazy.Value; - private static readonly object _locker = new object(); - public Collection _files; + private readonly object _locker = new object(); + public Collection _files = new Collection(); private CacheManager() { - _files = new Collection(); - - SetCacheManager(); } /// /// Sets the CacheManager /// - private void SetCacheManager() + public void SetCacheManager() { - try + lock (_locker) { - // Deserialise a saved cache manager, and use its files to set our instance. - using (FileStream fileStream = File.Open(ApplicationSettings.Default.LibraryPath + @"\" + ApplicationSettings.Default.CacheManagerFile, FileMode.Open)) + try { - XmlSerializer xmlSerializer = new XmlSerializer(typeof(CacheManager)); + // Deserialise a saved cache manager, and use its files to set our instance. + using (FileStream fileStream = File.Open(ApplicationSettings.Default.LibraryPath + @"\" + ApplicationSettings.Default.CacheManagerFile, FileMode.Open)) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(CacheManager)); - CacheManager manager = (CacheManager)xmlSerializer.Deserialize(fileStream); + CacheManager manager = (CacheManager)xmlSerializer.Deserialize(fileStream); - // Set its files on ourselves - Instance._files = manager._files; + // Set its files on ourselves + Instance._files = manager._files; + } + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("CacheManager", "Unable to reuse the Cache Manager because: " + ex.Message)); } - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("CacheManager", "Unable to reuse the Cache Manager because: " + ex.Message)); - } - try - { - Instance.Regenerate(); - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("CacheManager", "Regenerate failed because: " + ex.Message)); + try + { + Instance.Regenerate(); + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("CacheManager", "Regenerate failed because: " + ex.Message)); + } } } diff --git a/Logic/XmlHelper.cs b/Logic/XmlHelper.cs new file mode 100644 index 00000000..f719d17c --- /dev/null +++ b/Logic/XmlHelper.cs @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace XiboClient.Logic +{ + public sealed class XmlHelper + { + public static string SelectNodeInnerTextOrDefault(XmlNode node, string nodeName, string defaultValue) + { + XmlNode select = node.SelectSingleNode(nodeName); + + return select == null ? defaultValue : select.InnerText; + } + + public static string SelectFirstElementInnerTextOrDefault(XmlDocument doc, string tagName, string defaultValue) + { + XmlNodeList list = doc.GetElementsByTagName(tagName); + + return (list.Count <= 0) ? defaultValue : list.Item(0).InnerText; + } + } +} diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 193b5a6c..b84ad922 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -150,6 +150,9 @@ enum EXECUTION_STATE : uint public MainWindow(bool screenSaver) { + // Set the Cache Manager + CacheManager.Instance.SetCacheManager(); + InitializeComponent(); if (screenSaver) @@ -316,6 +319,7 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e) if (!ApplicationSettings.Default.EnableMouse) { // Hide the cursor + Mouse.OverrideCursor = System.Windows.Input.Cursors.None; } // Move the cursor to the starting place @@ -349,6 +353,13 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e) // UserApp data Debug.WriteLine(new LogMessage("MainForm_Load", "User AppData Path: " + ApplicationSettings.Default.LibraryPath), LogType.Info.ToString()); + + // Initialise CEF + CefSharp.Wpf.CefSettings settings = new CefSharp.Wpf.CefSettings(); + settings.CachePath = ApplicationSettings.Default.LibraryPath + @"\CEF"; + settings.LogFile = ApplicationSettings.Default.LibraryPath + @"\cef.log"; + settings.LogSeverity = CefSharp.LogSeverity.Error; + CefSharp.Cef.Initialize(settings); } /// @@ -386,34 +397,6 @@ void MainForm_Shown(object sender, EventArgs e) } } - /// - /// Called before the form has loaded for the first time - /// - /// - /// - private void MainForm_Load(object sender, EventArgs e) - { - // Is the mouse enabled? - if (!ApplicationSettings.Default.EnableMouse) - { - // Hide the cursor - Mouse.OverrideCursor = System.Windows.Input.Cursors.None; - } - - // Move the cursor to the starting place - if (!_screenSaver) - SetCursorStartPosition(); - - // Show the splash screen - ShowSplashScreen(); - - // Change the default Proxy class - OptionsForm.SetGlobalProxy(); - - // UserApp data - Debug.WriteLine(new LogMessage("MainForm_Load", "User AppData Path: " + ApplicationSettings.Default.LibraryPath), LogType.Info.ToString()); - } - /// /// Called as the Main Form starts to close /// @@ -427,8 +410,10 @@ private void MainForm_FormClosing(object sender, CancelEventArgs e) try { // Close the client info screen - //if (_clientInfoForm != null) - // _clientInfoForm.Hide(); + if (this.infoScreen != null) + { + this.infoScreen.Close(); + } // Stop the schedule object if (_schedule != null) diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index c17d688b..4bb68546 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -377,11 +377,11 @@ public void Remove() { try { - // Remove the region from the list of controls - this.LayoutScene.Children.Remove(region); - // Clear the region region.Clear(); + + // Remove the region from the list of controls + this.LayoutScene.Children.Remove(region); } catch (Exception e) { @@ -423,7 +423,8 @@ private void Region_DurationElapsedEvent() } // If we are sure we have expired after checking all regions, then set the layout expired flag on them all - if (isExpired) + // if we are an overlay, then don't raise this event + if (isExpired && !this.isOverlay) { // Inform each region that the layout containing it has expired foreach (Region temp in _regions) diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 303f168c..39c3f44c 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -605,6 +605,10 @@ private Media CreateNextMediaNode() ((WebMedia)media).ConfigureForHtmlPackage(); break; + case "spacer": + media = new Spacer(options); + break; + default: if (options.render == "html") { diff --git a/Rendering/Spacer.cs b/Rendering/Spacer.cs new file mode 100644 index 00000000..a7aa6510 --- /dev/null +++ b/Rendering/Spacer.cs @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Rendering +{ + /// + /// This doesn't put anything on the screen at all, it just has a timer. + /// + class Spacer : Media + { + public Spacer(RegionOptions options) : base(options) + { + + } + } +} diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index 7138afbe..cf42d90a 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -21,7 +21,6 @@ using CefSharp.Wpf; using System; using System.Diagnostics; -using System.Windows.Media.Animation; namespace XiboClient.Rendering { @@ -41,6 +40,8 @@ public WebCef(RegionOptions options) /// public override void RenderMedia() { + Debug.WriteLine("Created CEF Renderer for " + this.regionId, "WebCef"); + // Create the web view we will use webView = new ChromiumWebBrowser() { diff --git a/XiboClient.csproj b/XiboClient.csproj index 9b3551e8..29142a4a 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -65,6 +65,9 @@ true Off + + app.manifest + @@ -129,6 +132,7 @@ + @@ -141,6 +145,7 @@ + @@ -221,6 +226,7 @@ Resources.Designer.cs + diff --git a/app.manifest b/app.manifest new file mode 100644 index 00000000..1fa619f1 --- /dev/null +++ b/app.manifest @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + true/PM + + + + + + + + + + + From efd6d5ea488a19ef8de4578422d12b7b4b220856 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 10 Feb 2020 13:58:33 +0000 Subject: [PATCH 12/29] Do nothing on Screen Saver Preview (WPF doesn't handle that well). Tidy up imports --- App.xaml | 3 +- App.xaml.cs | 23 ++++----- Control/EmbeddedServer.cs | 3 +- InfoScreen.xaml.cs | 11 ----- Log/ConcurrentCircularBuffer.cs | 5 +- Logic/ApplicationSettings.cs | 3 +- Logic/XmlHelper.cs | 5 -- MainWindow.xaml.cs | 85 +++++++++++---------------------- Rendering/Audio.cs | 5 -- Rendering/Flash.cs | 4 -- Rendering/Image.cs | 3 +- Rendering/Layout.xaml.cs | 3 +- Rendering/Media.xaml.cs | 2 +- Rendering/PowerPoint.cs | 8 +--- Rendering/ShellCommand.cs | 4 -- Rendering/Spacer.cs | 5 -- Rendering/Video.cs | 7 +-- Rendering/WebMedia.cs | 2 +- 18 files changed, 47 insertions(+), 134 deletions(-) diff --git a/App.xaml b/App.xaml index 407d88ed..15bc61ec 100644 --- a/App.xaml +++ b/App.xaml @@ -1,8 +1,7 @@  + xmlns:local="clr-namespace:XiboClient"> diff --git a/App.xaml.cs b/App.xaml.cs index 4dd47196..008e1db6 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -48,8 +48,7 @@ protected override void OnStartup(StartupEventArgs e) { // Preview the screen saver case "/p": - // args[1] is the handle to the preview window - RunClient(new IntPtr(long.Parse(e.Args[1]))); + // Do nothing break; // Show the screen saver @@ -72,7 +71,7 @@ protected override void OnStartup(StartupEventArgs e) } else { - RunClient(); + RunClient(true); } } catch (Exception ex) @@ -85,6 +84,9 @@ protected override void OnStartup(StartupEventArgs e) Trace.Flush(); } + /// + /// Run the Settings Window + /// private static void RunSettings() { // If we are showing the options form, enable visual styles @@ -124,15 +126,7 @@ private static void RunClient(bool screenSaver) } } - /// - /// Run the Player - /// - /// - private static void RunClient(IntPtr previewWindow) - { - Trace.WriteLine(new LogMessage("Main", "Client Started"), LogType.Info.ToString()); - } - + #region Exception Handlers static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) { HandleUnhandledException(e.Exception); @@ -190,9 +184,6 @@ static void HandleUnhandledException(Object o) Environment.Exit(0); } - [DllImport("User32.dll")] - public static extern int ShowWindowAsync(IntPtr hWnd, int swCommand); - internal static class NativeMethods { [DllImport("kernel32.dll")] @@ -208,5 +199,7 @@ internal enum ErrorModes : uint SEM_NOGPFAULTERRORBOX = 0x0002, SEM_NOOPENFILEERRORBOX = 0x8000 } + + #endregion } } diff --git a/Control/EmbeddedServer.cs b/Control/EmbeddedServer.cs index beb9b581..1491f45f 100644 --- a/Control/EmbeddedServer.cs +++ b/Control/EmbeddedServer.cs @@ -61,8 +61,7 @@ public void Run() Trace.WriteLine(new LogMessage("EmbeddedServer - Run", "Exception running server: " + e.Message), LogType.Error.ToString()); } - if (OnServerClosed != null) - OnServerClosed(); + OnServerClosed?.Invoke(); } /// diff --git a/InfoScreen.xaml.cs b/InfoScreen.xaml.cs index 05f0b9e1..fb3f2b9d 100644 --- a/InfoScreen.xaml.cs +++ b/InfoScreen.xaml.cs @@ -19,18 +19,7 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; using System.Windows.Threading; using XiboClient.Log; diff --git a/Log/ConcurrentCircularBuffer.cs b/Log/ConcurrentCircularBuffer.cs index 34a1bd23..78d173d4 100644 --- a/Log/ConcurrentCircularBuffer.cs +++ b/Log/ConcurrentCircularBuffer.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace XiboClient.Log { diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index bdc9f1c4..8cafe15b 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -240,7 +240,8 @@ public object this[string propertyName] if (property != null) { property.SetValue(this, value, null); - } else + } + else { Debug.WriteLine("Null Property: " + propertyName, "ApplicationSettings"); } diff --git a/Logic/XmlHelper.cs b/Logic/XmlHelper.cs index f719d17c..20c5c696 100644 --- a/Logic/XmlHelper.cs +++ b/Logic/XmlHelper.cs @@ -18,11 +18,6 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Xml; namespace XiboClient.Logic diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index b84ad922..64139d31 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -28,11 +28,8 @@ using System.Windows; using System.Windows.Forms; using System.Windows.Input; -using System.Windows.Media.Animation; using System.Windows.Media.Imaging; -using System.Windows.Shapes; using System.Windows.Threading; -using XiboClient.Control; using XiboClient.Error; using XiboClient.Log; using XiboClient.Logic; @@ -101,53 +98,15 @@ enum EXECUTION_STATE : uint [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); - // Changes the parent window of the specified child window - [DllImport("user32.dll")] - private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); - - // Changes an attribute of the specified window - [DllImport("user32.dll")] - private static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - // Retrieves information about the specified window - [DllImport("user32.dll", SetLastError = true)] - private static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - // Retrieves the coordinates of a window's client area - [DllImport("user32.dll")] - private static extern bool GetClientRect(IntPtr hWnd, out Rectangle lpRect); - [DllImport("User32.dll")] private static extern bool SetCursorPos(int X, int Y); #endregion - /*public MainWindow(IntPtr previewHandle) - { - InitializeComponent(); - - // Set the preview window of the screen saver selection - // dialog in Windows as the parent of this form. - SetParent(this.Handle, previewHandle); - - // Set this form to a child form, so that when the screen saver selection - // dialog in Windows is closed, this form will also close. - SetWindowLong(this.Handle, -16, new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000)); - - // Set the size of the screen saver to the size of the screen saver - // preview window in the screen saver selection dialog in Windows. - Rectangle ParentRect; - GetClientRect(previewHandle, out ParentRect); - - ApplicationSettings.Default.SizeX = ParentRect.Size.Width; - ApplicationSettings.Default.SizeY = ParentRect.Size.Height; - ApplicationSettings.Default.OffsetX = 0; - ApplicationSettings.Default.OffsetY = 0; - - InitializeScreenSaver(true); - InitializeXibo(); - }*/ - + /// + /// Initialise Player + /// + /// public MainWindow(bool screenSaver) { // Set the Cache Manager @@ -157,12 +116,15 @@ public MainWindow(bool screenSaver) if (screenSaver) { - InitializeScreenSaver(false); + InitializeScreenSaver(); } InitializeXibo(); } + /// + /// Initialise Xibo + /// private void InitializeXibo() { // Set the title @@ -234,20 +196,22 @@ private void InitializeXibo() Trace.WriteLine(new LogMessage("MainForm", "Player Initialised"), LogType.Info.ToString()); } - private void InitializeScreenSaver(bool preview) + /// + /// Initialise the Screen Saver + /// + private void InitializeScreenSaver() { _screenSaver = true; // Configure some listeners for the mouse (to quit) - if (!preview) - { - KeyStore.Instance.ScreenSaver = true; - - MouseInterceptor.Instance.MouseEvent += Instance_MouseEvent; - } + KeyStore.Instance.ScreenSaver = true; + MouseInterceptor.Instance.MouseEvent += Instance_MouseEvent; } - void Instance_MouseEvent() + /// + /// Handle Mouse Events + /// + private void Instance_MouseEvent() { Close(); } @@ -263,10 +227,6 @@ void Instance_KeyPress(string name) { if (this.infoScreen == null) { - this.infoScreen = new InfoScreen(); - this.infoScreen.Closed += InfoScreen_Closed; - this.infoScreen.Show(); - #if !DEBUG // Make our window not topmost so that we can see the info screen if (!_screenSaver) @@ -274,12 +234,17 @@ void Instance_KeyPress(string name) Topmost = false; } #endif + + this.infoScreen = new InfoScreen(); + this.infoScreen.Closed += InfoScreen_Closed; + this.infoScreen.Show(); } else { this.infoScreen.Close(); #if !DEBUG + // Bring the window back to Topmost if we need to if (!_screenSaver) { Topmost = true; @@ -695,6 +660,8 @@ private void DestroyLayout() Debug.WriteLine("Destroying Layout", "MainForm - DestoryLayout"); this.currentLayout.Remove(); + + this.Scene.Children.Remove(this.currentLayout); } /// @@ -903,6 +870,8 @@ private void SetMainWindowSize() Debug.WriteLine("SetMainWindowSize: Use Monitor Size"); // Use the primary monitor size + Top = 0; + Left = 0; Width = SystemParameters.PrimaryScreenWidth; Height = SystemParameters.PrimaryScreenHeight; } diff --git a/Rendering/Audio.cs b/Rendering/Audio.cs index bfc598a5..f7b193bf 100644 --- a/Rendering/Audio.cs +++ b/Rendering/Audio.cs @@ -18,11 +18,6 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using System; -using System.Diagnostics; -using System.IO; -using System.Windows; -using System.Windows.Controls; namespace XiboClient.Rendering { diff --git a/Rendering/Flash.cs b/Rendering/Flash.cs index 800d4f57..15ac7446 100644 --- a/Rendering/Flash.cs +++ b/Rendering/Flash.cs @@ -18,12 +18,8 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace XiboClient.Rendering { diff --git a/Rendering/Image.cs b/Rendering/Image.cs index 724bea36..49a3d633 100644 --- a/Rendering/Image.cs +++ b/Rendering/Image.cs @@ -21,7 +21,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Windows.Controls; using System.Windows.Media.Imaging; namespace XiboClient.Rendering @@ -90,7 +89,7 @@ public override void RenderMedia() }; this.image.Source = new BitmapImage(filePath); - + // Handle the different scale types supported if (this.scaleType == "stretch") { diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 4bb68546..98247897 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -20,7 +20,6 @@ */ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Drawing.Imaging; using System.Globalization; @@ -305,7 +304,7 @@ public void loadFromFile(string layoutPath, int layoutId, int scheduleId, bool i Region temp = new Region(); temp.DurationElapsedEvent += new Region.DurationElapsedDelegate(Region_DurationElapsedEvent); - + // ZIndex if (nodeAttibutes["zindex"] != null) { diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index f73eaa9e..c4afb842 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -64,7 +64,7 @@ public partial class Media : UserControl /// The Intended Width of this Media /// public int WidthIntended { get { return options.width; } } - + /// /// The Intended Height of this Media /// diff --git a/Rendering/PowerPoint.cs b/Rendering/PowerPoint.cs index eb5e40c5..fd6fe364 100644 --- a/Rendering/PowerPoint.cs +++ b/Rendering/PowerPoint.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace XiboClient.Rendering +namespace XiboClient.Rendering { class PowerPoint : WebIe { diff --git a/Rendering/ShellCommand.cs b/Rendering/ShellCommand.cs index 1cf05f1f..9b9f945e 100644 --- a/Rendering/ShellCommand.cs +++ b/Rendering/ShellCommand.cs @@ -19,11 +19,7 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using XiboClient.Logic; namespace XiboClient.Rendering diff --git a/Rendering/Spacer.cs b/Rendering/Spacer.cs index a7aa6510..7c2b1dae 100644 --- a/Rendering/Spacer.cs +++ b/Rendering/Spacer.cs @@ -18,11 +18,6 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace XiboClient.Rendering { diff --git a/Rendering/Video.cs b/Rendering/Video.cs index 051ad6c6..7b05e40d 100644 --- a/Rendering/Video.cs +++ b/Rendering/Video.cs @@ -19,12 +19,8 @@ * along with Xibo. If not, see . */ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -90,7 +86,8 @@ public override void RenderMedia() this.mediaElement = new MediaElement(); this.mediaElement.Volume = this.volume; - if (!this.ShouldBeVisible) { + if (!this.ShouldBeVisible) + { this.mediaElement.Width = 0; this.mediaElement.Height = 0; this.mediaElement.Visibility = Visibility.Hidden; diff --git a/Rendering/WebMedia.cs b/Rendering/WebMedia.cs index e41151cd..3c854d0f 100644 --- a/Rendering/WebMedia.cs +++ b/Rendering/WebMedia.cs @@ -377,7 +377,7 @@ private void UpdateCacheIfNecessary() cachedFile = Regex.Replace(cachedFile, "", ""); cachedFile = Regex.Replace(cachedFile, "", ""); cachedFile = Regex.Replace(cachedFile, "", ""); - + /// File should be back to its original form, ready to run through the subs again string html = MakeHtmlSubstitutions(cachedFile); From d65e0345b3ab49542242bbe0749f6dc2625c5625 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 12 Feb 2020 18:21:11 +0000 Subject: [PATCH 13/29] Surpress script errors --- App.xaml.cs | 10 +------ Helpers/CefJsDialogHandler.cs | 53 +++++++++++++++++++++++++++++++++++ Logic/ApplicationSettings.cs | 2 +- MainWindow.xaml.cs | 1 + Properties/AssemblyInfo.cs | 4 +-- Rendering/WebCef.cs | 2 ++ Rendering/WebIe.cs | 11 ++++++++ Rendering/WebMedia.cs | 6 ++-- XiboClient.csproj | 1 + default.config.xml | 2 +- 10 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 Helpers/CefJsDialogHandler.cs diff --git a/App.xaml.cs b/App.xaml.cs index 008e1db6..8a06327b 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -71,7 +71,7 @@ protected override void OnStartup(StartupEventArgs e) } else { - RunClient(true); + RunClient(false); } } catch (Exception ex) @@ -94,14 +94,6 @@ private static void RunSettings() windowMain.ShowDialog(); } - /// - /// Run the Player - /// - private static void RunClient() - { - RunClient(false); - } - /// /// Run the Player /// diff --git a/Helpers/CefJsDialogHandler.cs b/Helpers/CefJsDialogHandler.cs new file mode 100644 index 00000000..56d7161d --- /dev/null +++ b/Helpers/CefJsDialogHandler.cs @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using CefSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Helpers +{ + class CefJsDialogHandler : IJsDialogHandler + { + public bool OnJSDialog(IWebBrowser browserControl, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage) + { + suppressMessage = true; + return false; + } + + public bool OnBeforeUnloadDialog(IWebBrowser browserControl, IBrowser browser, string message, bool isReload, IJsDialogCallback callback) + { + return true; + } + + public void OnResetDialogState(IWebBrowser browserControl, IBrowser browser) + { + + } + + public void OnDialogClosed(IWebBrowser browserControl, IBrowser browser) + { + + } + } +} diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 8cafe15b..e7975426 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -513,7 +513,6 @@ public bool InDownloadWindow public int MaxConcurrentDownloads { get; set; } public int ScreenShotRequestInterval { get; set; } public int ScreenShotSize { get; set; } - public string BrowserType { get; set; } private int _maxLogFileUploads; public int MaxLogFileUploads { get { return ((_maxLogFileUploads == 0) ? 10 : _maxLogFileUploads); } set { _maxLogFileUploads = value; } } @@ -529,6 +528,7 @@ public bool InDownloadWindow public bool SendCurrentLayoutAsStatusUpdate { get; set; } public bool PreventSleep { get; set; } public bool ScreenShotRequested { get; set; } + public bool FallbackToInternetExplorer { get; set; } // XMDS Status Flags private DateTime _xmdsLastConnection; diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 64139d31..d5019351 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -30,6 +30,7 @@ using System.Windows.Input; using System.Windows.Media.Imaging; using System.Windows.Threading; +using XiboClient.Control; using XiboClient.Error; using XiboClient.Log; using XiboClient.Logic; diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index bac91dd2..944da813 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.2.0.202")] -[assembly: AssemblyFileVersion("2.2.0.202")] +[assembly: AssemblyVersion("2.252.1.0")] +[assembly: AssemblyFileVersion("2.252.1.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index cf42d90a..bfdbb8fa 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -21,6 +21,7 @@ using CefSharp.Wpf; using System; using System.Diagnostics; +using XiboClient.Helpers; namespace XiboClient.Rendering { @@ -51,6 +52,7 @@ public override void RenderMedia() webView.Visibility = System.Windows.Visibility.Hidden; webView.Loaded += WebView_Loaded; webView.LoadError += WebView_LoadError; + webView.JsDialogHandler = new CefJsDialogHandler(); this.MediaScene.Children.Add(webView); diff --git a/Rendering/WebIe.cs b/Rendering/WebIe.cs index 8569bbbc..743d0f0d 100644 --- a/Rendering/WebIe.cs +++ b/Rendering/WebIe.cs @@ -20,6 +20,7 @@ */ using System; using System.Diagnostics; +using System.Reflection; using System.Windows.Controls; namespace XiboClient.Rendering @@ -87,6 +88,16 @@ public override void RenderMedia() /// private void _webBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) { + dynamic activeX = this._webBrowser.GetType().InvokeMember( + "ActiveXInstance", + BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic, + null, + this._webBrowser, + new object[] { } + ); + + activeX.Silent = true; + DocumentCompleted(); if (!Expired) diff --git a/Rendering/WebMedia.cs b/Rendering/WebMedia.cs index 3c854d0f..609aa247 100644 --- a/Rendering/WebMedia.cs +++ b/Rendering/WebMedia.cs @@ -434,13 +434,13 @@ private string ReadCachedViewPort(string html) public static WebMedia GetConfiguredWebMedia(RegionOptions options) { WebMedia media; - if (ApplicationSettings.Default.BrowserType.Equals("edge", StringComparison.InvariantCultureIgnoreCase)) + if (ApplicationSettings.Default.FallbackToInternetExplorer) { - media = new WebCef(options); + media = new WebIe(options); } else { - media = new WebIe(options); + media = new WebCef(options); } return media; } diff --git a/XiboClient.csproj b/XiboClient.csproj index 29142a4a..43097c55 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -113,6 +113,7 @@ + InfoScreen.xaml diff --git a/default.config.xml b/default.config.xml index 64d27b27..e3a376c7 100644 --- a/default.config.xml +++ b/default.config.xml @@ -53,5 +53,5 @@ 9696 0 - edge + false \ No newline at end of file From f2e78992e9fe3a2c7afbe57575195bfb1eaa54a9 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 14 Feb 2020 17:27:12 +0000 Subject: [PATCH 14/29] Add back edge for selected media rendering (with special tag in embedded) Unbind htmlupdated events Add save button to InfoScreen Update CEFSharp --- InfoScreen.xaml | 4 +- InfoScreen.xaml.cs | 30 ++++++++ Rendering/Region.xaml.cs | 6 +- Rendering/WebCef.cs | 3 +- Rendering/WebEdge.cs | 146 +++++++++++++++++++++++++++++++++++++++ Rendering/WebIe.cs | 1 + Rendering/WebMedia.cs | 51 ++++++++++++++ XiboClient.csproj | 6 +- 8 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 Rendering/WebEdge.cs diff --git a/InfoScreen.xaml b/InfoScreen.xaml index 0e69c1b1..1d6c5126 100644 --- a/InfoScreen.xaml +++ b/InfoScreen.xaml @@ -22,16 +22,16 @@ - + + private List ExcludedProperties; - public string ClientVersion { get; } = "2 R252.4"; + public string ClientVersion { get; } = "2 R252.6"; public string Version { get; } = "5"; public int ClientCodeVersion { get; } = 252; @@ -214,8 +214,8 @@ public void Save() { try { - if (property.CanRead - && !_globalProperties.Contains(property.Name) + if (property.CanRead + && !_globalProperties.Contains(property.Name) && !ExcludedProperties.Contains(property.Name) && property.Name != "HardwareKey") { diff --git a/Logic/RegionOptions.cs b/Logic/RegionOptions.cs index 8c56aa5f..27cc7108 100644 --- a/Logic/RegionOptions.cs +++ b/Logic/RegionOptions.cs @@ -20,7 +20,6 @@ */ using System; using System.Collections.Generic; -using System.Drawing; using System.Xml; using XiboClient.Rendering; diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index fddd56e8..8b27cfa0 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -30,7 +30,6 @@ using System.Windows.Input; using System.Windows.Media.Imaging; using System.Windows.Threading; -using XiboClient.Control; using XiboClient.Error; using XiboClient.Log; using XiboClient.Logic; diff --git a/OptionsForm.xaml.cs b/OptionsForm.xaml.cs index bf2fb3bc..45b5c7b9 100644 --- a/OptionsForm.xaml.cs +++ b/OptionsForm.xaml.cs @@ -29,7 +29,6 @@ using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Windows; using XiboClient.XmdsAgents; @@ -444,7 +443,7 @@ private static async Task GenerateCode() writer.WriteEndObject(); } } - + using (var stringContent = new StringContent(sb.ToString(), Encoding.UTF8, "application/json")) { request.Content = stringContent; @@ -456,7 +455,7 @@ private static async Task GenerateCode() response.EnsureSuccessStatusCode(); var jsonString = await response.Content.ReadAsStringAsync(); var json = JsonConvert.DeserializeObject(jsonString); - + if (json.ContainsKey("message")) { throw new Exception("Request returned a message in the body, discard"); @@ -503,7 +502,7 @@ private static async Task CheckCode(string UserCode, string DeviceCode) Debug.WriteLine(jsonString, "CheckCode"); return false; } - } + } catch { Debug.WriteLine("Non 200/300 status code", "CheckCode"); diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 9b9f201c..140c042f 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.252.5.0")] -[assembly: AssemblyFileVersion("2.252.5.0")] +[assembly: AssemblyVersion("2.252.6.0")] +[assembly: AssemblyFileVersion("2.252.6.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index 9f46b0ac..a2435f45 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -287,7 +287,7 @@ public void TransitionOut() break; } } - else if(!this._stopped) + else if (!this._stopped) { this._stopped = true; this.MediaStoppedEvent?.Invoke(this); @@ -379,7 +379,7 @@ private void FlyAnimation(string direction, double duration, bool isInbound) doubleAnimationY.From = top; } - + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); RenderTransform = trans; diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index ac7346a4..febeb001 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -723,7 +723,7 @@ private void StopMedia(Media media, bool regionStopped) catch (Exception ex) { Trace.WriteLine(new LogMessage("Region - Stop Media", "Unable to dispose. Ex = " + ex.Message), LogType.Audit.ToString()); - + // Remove the controls RegionScene.Children.Remove(media); } @@ -736,7 +736,7 @@ private void StopMedia(Media media, bool regionStopped) private void Media_MediaStoppedEvent(Media media) { media.MediaStoppedEvent -= Media_MediaStoppedEvent; - + // Remove the controls RegionScene.Children.Remove(media); } diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs index df703d6d..8f80c83b 100644 --- a/Rendering/WebEdge.cs +++ b/Rendering/WebEdge.cs @@ -20,11 +20,7 @@ */ using Microsoft.Toolkit.Wpf.UI.Controls; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace XiboClient.Rendering { diff --git a/Rendering/WebIe.cs b/Rendering/WebIe.cs index a4533814..4800ec03 100644 --- a/Rendering/WebIe.cs +++ b/Rendering/WebIe.cs @@ -107,10 +107,10 @@ private void _webBrowser_Navigated(object sender, System.Windows.Navigation.Navi } dynamic activeX = this._webBrowser.GetType().InvokeMember( - "ActiveXInstance", - BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic, - null, - this._webBrowser, + "ActiveXInstance", + BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic, + null, + this._webBrowser, new object[] { } ); @@ -151,7 +151,7 @@ protected override string MakeHtmlSubstitutions(string cachedFile) } else { - bodyStyle = "background-image: url('" + this.backgroundImage + "'); background-attachment:fixed; background-color:" + this.backgroundColor + bodyStyle = "background-image: url('" + this.backgroundImage + "'); background-attachment:fixed; background-color:" + this.backgroundColor + "; background-repeat: no-repeat; background-position: " + this.backgroundLeft + "px " + this.backgroundTop + "px;"; } diff --git a/XmdsAgents/ScheduleAndFilesAgent.cs b/XmdsAgents/ScheduleAndFilesAgent.cs index 8132f8fb..3d2bdf48 100644 --- a/XmdsAgents/ScheduleAndFilesAgent.cs +++ b/XmdsAgents/ScheduleAndFilesAgent.cs @@ -18,14 +18,11 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Net; -using System.Text; using System.Threading; using System.Xml; using XiboClient.Log;