From 108cf8923efb499b6c128de492a94117f5593f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ch=C3=BDlek?= <info@chylex.com> Date: Thu, 2 Nov 2017 03:08:43 +0100 Subject: [PATCH] Implement analytics (#176) * Implement analytics report generation w/ user config and basic system info * Add HW and plugin info to analytics report generation * Add a way of displaying the full analytics report * Fix issues in analytics report and include design theme * Ensure tab config is saved when switching tabs * Fix compilation error in TabSettingsFeedback and safeguard nulls * Add locale to analytics report * Work on analytics (utils, last collection label, dependency refactoring) * Add analytics state file and implement sending reports every week * Send an analytics report after each update --- Core/FormBrowser.cs | 25 +- Core/Other/Analytics/AnalyticsFile.cs | 56 ++++ Core/Other/Analytics/AnalyticsManager.cs | 110 +++++++ Core/Other/Analytics/AnalyticsReport.cs | 57 ++++ .../Analytics/AnalyticsReportGenerator.cs | 280 ++++++++++++++++++ Core/Other/FormSettings.cs | 17 +- .../DialogSettingsAnalytics.Designer.cs | 90 ++++++ .../Dialogs/DialogSettingsAnalytics.cs | 24 ++ .../Settings/TabSettingsFeedback.Designer.cs | 56 +++- Core/Other/Settings/TabSettingsFeedback.cs | 24 +- Core/Utils/BrowserUtils.cs | 7 +- Core/Utils/StringUtils.cs | 11 + Program.cs | 1 + Resources/Plugins/edit-design/browser.js | 11 +- TweetDuck.csproj | 10 + 15 files changed, 748 insertions(+), 31 deletions(-) create mode 100644 Core/Other/Analytics/AnalyticsFile.cs create mode 100644 Core/Other/Analytics/AnalyticsManager.cs create mode 100644 Core/Other/Analytics/AnalyticsReport.cs create mode 100644 Core/Other/Analytics/AnalyticsReportGenerator.cs create mode 100644 Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs create mode 100644 Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs index b22670ba..240f2318 100644 --- a/Core/FormBrowser.cs +++ b/Core/FormBrowser.cs @@ -8,6 +8,7 @@ using TweetDuck.Core.Notification; using TweetDuck.Core.Notification.Screenshot; using TweetDuck.Core.Other; +using TweetDuck.Core.Other.Analytics; using TweetDuck.Core.Other.Management; using TweetDuck.Core.Other.Settings; using TweetDuck.Core.Utils; @@ -47,10 +48,11 @@ public bool IsWaiting{ private bool isLoaded; private FormWindowState prevState; - + private TweetScreenshotManager notificationScreenshotManager; private SoundNotification soundNotification; private VideoPlayer videoPlayer; + private AnalyticsManager analytics; public FormBrowser(UpdaterSettings updaterSettings){ InitializeComponent(); @@ -77,6 +79,7 @@ public FormBrowser(UpdaterSettings updaterSettings){ notificationScreenshotManager?.Dispose(); soundNotification?.Dispose(); videoPlayer?.Dispose(); + analytics?.Dispose(); }; this.trayIcon.ClickRestore += trayIcon_ClickRestore; @@ -108,6 +111,10 @@ private void RestoreWindow(){ Config.BrowserWindow.Restore(this, true); prevState = WindowState; isLoaded = true; + + if (Config.AllowDataCollection){ + analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath); + } } private void UpdateTrayIcon(){ @@ -321,6 +328,10 @@ public void OnIntroductionClosed(bool showGuide, bool allowDataCollection){ Config.FirstRun = false; Config.AllowDataCollection = allowDataCollection; Config.Save(); + + if (allowDataCollection && analytics == null){ + analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath); + } } if (showGuide){ @@ -340,7 +351,7 @@ public void OpenSettings(Type startTab){ if (!FormManager.TryBringToFront<FormSettings>()){ bool prevEnableUpdateCheck = Config.EnableUpdateCheck; - FormSettings form = new FormSettings(this, plugins, updates, startTab); + FormSettings form = new FormSettings(this, plugins, updates, analytics, startTab); form.FormClosed += (sender, args) => { if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){ @@ -355,6 +366,16 @@ public void OpenSettings(Type startTab){ } browser.RefreshMemoryTracker(); + + if (Config.AllowDataCollection){ + if (analytics == null){ + analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath); + } + } + else if (analytics != null){ + analytics.Dispose(); + analytics = null; + } if (form.ShouldReloadBrowser){ FormManager.TryFind<FormPlugins>()?.Close(); diff --git a/Core/Other/Analytics/AnalyticsFile.cs b/Core/Other/Analytics/AnalyticsFile.cs new file mode 100644 index 00000000..3730b41d --- /dev/null +++ b/Core/Other/Analytics/AnalyticsFile.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using TweetDuck.Core.Utils; +using TweetDuck.Data.Serialization; + +namespace TweetDuck.Core.Other.Analytics{ + sealed class AnalyticsFile{ + private static readonly FileSerializer<AnalyticsFile> Serializer = new FileSerializer<AnalyticsFile>(); + + static AnalyticsFile(){ + Serializer.RegisterTypeConverter(typeof(DateTime), new SingleTypeConverter<DateTime>{ + ConvertToString = value => value.ToBinary().ToString(), + ConvertToObject = value => DateTime.FromBinary(long.Parse(value)) + }); + } + + // STATE PROPERTIES + + public DateTime LastDataCollection { get; set; } = DateTime.MinValue; + public string LastCollectionVersion { get; set; } = null; + public string LastCollectionMessage { get; set; } = null; + + // END OF DATA + + private readonly string file; + + private AnalyticsFile(string file){ + this.file = file; + } + + public bool Save(){ + try{ + WindowsUtils.CreateDirectoryForFile(file); + Serializer.Write(file, this); + return true; + }catch(Exception e){ + Program.Reporter.HandleException("Analytics File Error", "Could not save the analytics file.", true, e); + return false; + } + } + + public static AnalyticsFile Load(string file){ + AnalyticsFile config = new AnalyticsFile(file); + + try{ + Serializer.Read(file, config); + }catch(FileNotFoundException){ + }catch(DirectoryNotFoundException){ + }catch(Exception e){ + Program.Reporter.HandleException("Analytics File Error", "Could not open the analytics file.", true, e); + } + + return config; + } + } +} diff --git a/Core/Other/Analytics/AnalyticsManager.cs b/Core/Other/Analytics/AnalyticsManager.cs new file mode 100644 index 00000000..9d85cdb2 --- /dev/null +++ b/Core/Other/Analytics/AnalyticsManager.cs @@ -0,0 +1,110 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using System.Timers; +using TweetDuck.Core.Controls; +using TweetDuck.Core.Utils; +using TweetDuck.Plugins; + +namespace TweetDuck.Core.Other.Analytics{ + sealed class AnalyticsManager : IDisposable{ + private static readonly TimeSpan CollectionInterval = TimeSpan.FromDays(7); + private static readonly Uri CollectionUrl = new Uri("https://tweetduck.chylex.com/breadcrumb/report"); + + public string LastCollectionMessage => file.LastCollectionMessage; + + private readonly FormBrowser browser; + private readonly PluginManager plugins; + private readonly AnalyticsFile file; + private readonly Timer currentTimer; + + public AnalyticsManager(FormBrowser browser, PluginManager plugins, string file){ + this.browser = browser; + this.plugins = plugins; + this.file = AnalyticsFile.Load(file); + + this.currentTimer = new Timer{ SynchronizingObject = browser }; + this.currentTimer.Elapsed += currentTimer_Elapsed; + + if (this.file.LastCollectionVersion != Program.VersionTag){ + ScheduleReportIn(TimeSpan.FromHours(12), string.Empty); + } + else{ + RestartTimer(); + } + } + + public void Dispose(){ + currentTimer.Dispose(); + } + + private void ScheduleReportIn(TimeSpan delay, string message = null){ + SetLastDataCollectionTime(DateTime.Now.Subtract(CollectionInterval).Add(delay), message); + } + + private void SetLastDataCollectionTime(DateTime dt, string message = null){ + file.LastDataCollection = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind); + file.LastCollectionVersion = Program.VersionTag; + file.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture); + + file.Save(); + RestartTimer(); + } + + private void RestartTimer(){ + TimeSpan diff = DateTime.Now.Subtract(file.LastDataCollection); + int minutesTillNext = (int)(CollectionInterval.TotalMinutes-Math.Floor(diff.TotalMinutes)); + + currentTimer.Interval = Math.Max(minutesTillNext, 1)*60000; + currentTimer.Start(); + } + + private void currentTimer_Elapsed(object sender, ElapsedEventArgs e){ + currentTimer.Stop(); + + TimeSpan diff = DateTime.Now.Subtract(file.LastDataCollection); + + if (Math.Floor(diff.TotalMinutes) >= CollectionInterval.TotalMinutes){ + SendReport(); + } + else{ + RestartTimer(); + } + } + + private void SendReport(){ + AnalyticsReportGenerator.ExternalInfo info = AnalyticsReportGenerator.ExternalInfo.From(browser); + + Task.Factory.StartNew(() => { + AnalyticsReport report = AnalyticsReportGenerator.Create(info, plugins); + BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection()); + }).ContinueWith(task => browser.InvokeAsyncSafe(() => { + if (task.Status == TaskStatus.RanToCompletion){ + SetLastDataCollectionTime(DateTime.Now); + } + else if (task.Exception != null){ + string message = null; + + if (task.Exception.InnerException is WebException e){ + switch(e.Status){ + case WebExceptionStatus.ConnectFailure: + message = "Connection Error"; + break; + + case WebExceptionStatus.NameResolutionFailure: + message = "DNS Error"; + break; + + case WebExceptionStatus.ProtocolError: + HttpWebResponse response = e.Response as HttpWebResponse; + message = "HTTP Error "+(response != null ? $"{(int)response.StatusCode} ({response.StatusDescription})" : "(unknown code)"); + break; + } + } + + ScheduleReportIn(TimeSpan.FromHours(4), message ?? "Error: "+(task.Exception.InnerException?.Message ?? task.Exception.Message)); + } + })); + } + } +} diff --git a/Core/Other/Analytics/AnalyticsReport.cs b/Core/Other/Analytics/AnalyticsReport.cs new file mode 100644 index 00000000..fc7b8033 --- /dev/null +++ b/Core/Other/Analytics/AnalyticsReport.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Specialized; +using System.Text; + +namespace TweetDuck.Core.Other.Analytics{ + sealed class AnalyticsReport : IEnumerable{ + private OrderedDictionary data = new OrderedDictionary(32); + private int separators; + + public void Add(int ignored){ // adding separators to pretty print + data.Add((++separators).ToString(), null); + } + + public void Add(string key, string value){ + data.Add(key, value); + } + + public AnalyticsReport FinalizeReport(){ + if (!data.IsReadOnly){ + data = data.AsReadOnly(); + } + + return this; + } + + public IEnumerator GetEnumerator(){ + return data.GetEnumerator(); + } + + public NameValueCollection ToNameValueCollection(){ + NameValueCollection collection = new NameValueCollection(); + + foreach(DictionaryEntry entry in data){ + if (entry.Value != null){ + collection.Add((string)entry.Key, (string)entry.Value); + } + } + + return collection; + } + + public override string ToString(){ + StringBuilder build = new StringBuilder(); + + foreach(DictionaryEntry entry in data){ + if (entry.Value == null){ + build.AppendLine(); + } + else{ + build.AppendLine(entry.Key+": "+entry.Value); + } + } + + return build.ToString(); + } + } +} diff --git a/Core/Other/Analytics/AnalyticsReportGenerator.cs b/Core/Other/Analytics/AnalyticsReportGenerator.cs new file mode 100644 index 00000000..64a32cfc --- /dev/null +++ b/Core/Other/Analytics/AnalyticsReportGenerator.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Windows.Forms; +using Microsoft.Win32; +using TweetDuck.Configuration; +using System.Linq; +using System.Management; +using System.Text.RegularExpressions; +using TweetDuck.Core.Notification; +using TweetDuck.Core.Utils; +using TweetDuck.Plugins; + +namespace TweetDuck.Core.Other.Analytics{ + static class AnalyticsReportGenerator{ + public static AnalyticsReport Create(ExternalInfo info, PluginManager plugins){ + Dictionary<string, string> editLayoutDesign = EditLayoutDesignPluginData; + + return new AnalyticsReport{ + { "App Version" , Program.VersionTag }, + { "App Type" , Program.IsPortable ? "portable" : "installed" }, + 0, + { "System Name" , SystemName }, + { "System Edition" , SystemEdition }, + { "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" }, + { "System Build" , SystemBuild }, + { "System Locale" , Program.Culture.Name.ToLower() }, + 0, + { "RAM" , Exact(RamSize) }, + { "GPU" , GpuVendor }, + 0, + { "Screen Count" , Exact(Screen.AllScreens.Length) }, + { "Screen Resolution" , info.Resolution ?? "(unknown)" }, + { "Screen DPI" , info.DPI != null ? Exact(info.DPI.Value) : "(unknown)" }, + 0, + { "Hardware Acceleration" , Bool(SysConfig.HardwareAcceleration) }, + { "Browser GC Reload" , Bool(SysConfig.EnableBrowserGCReload) }, + { "Browser GC Threshold" , Exact(SysConfig.BrowserMemoryThreshold) }, + 0, + { "Expand Links" , Bool(UserConfig.ExpandLinksOnHover) }, + { "Switch Account Selectors" , Bool(UserConfig.SwitchAccountSelectors) }, + { "Search In First Column" , Bool(UserConfig.OpenSearchInFirstColumn) }, + { "Best Image Quality" , Bool(UserConfig.BestImageQuality) }, + { "Spell Check" , Bool(UserConfig.EnableSpellCheck) }, + { "Zoom" , Exact(UserConfig.ZoomLevel) }, + 0, + { "Updates" , Bool(UserConfig.EnableUpdateCheck) }, + { "Update Dismissed" , Bool(!string.IsNullOrEmpty(UserConfig.DismissedUpdate)) }, + 0, + { "Tray" , TrayMode }, + { "Tray Highlight" , Bool(UserConfig.EnableTrayHighlight) }, + 0, + { "Notification Position" , NotificationPosition }, + { "Notification Size" , NotificationSize }, + { "Notification Timer" , NotificationTimer }, + { "Notification Timer Speed" , RoundUp(UserConfig.NotificationDurationValue, 5) }, + { "Notification Scroll Speed" , Exact(UserConfig.NotificationScrollSpeed) }, + { "Notification Column Title" , Bool(UserConfig.DisplayNotificationColumn) }, + { "Notification Media Previews" , Bool(UserConfig.NotificationMediaPreviews) }, + { "Notification Link Skip" , Bool(UserConfig.NotificationSkipOnLinkClick) }, + { "Notification Non-Intrusive" , Bool(UserConfig.NotificationNonIntrusiveMode) }, + { "Notification Idle Pause" , Exact(UserConfig.NotificationIdlePauseSeconds) }, + { "Custom Sound Notification" , string.IsNullOrEmpty(UserConfig.NotificationSoundPath) ? "off" : Path.GetExtension(UserConfig.NotificationSoundPath) }, + 0, + { "Program Arguments" , List(ProgramArguments) }, + { "Custom CEF Arguments" , RoundUp((UserConfig.CustomCefArgs ?? string.Empty).Length, 10) }, + { "Custom Browser CSS" , RoundUp((UserConfig.CustomBrowserCSS ?? string.Empty).Length, 50) }, + { "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) }, + 0, + { "Plugins All" , List(plugins.Plugins.Select(plugin => plugin.Identifier)) }, + { "Plugins Enabled" , List(plugins.Plugins.Where(plugin => plugins.Config.IsEnabled(plugin)).Select(plugin => plugin.Identifier)) }, + 0, + { "Theme" , Dict(editLayoutDesign, "_theme", "light/def") }, + { "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") }, + { "Font Size" , Dict(editLayoutDesign, "fontSize", "12px/def") }, + { "Large Quote Font" , Dict(editLayoutDesign, "increaseQuoteTextSize", "false/def") }, + { "Small Compose Font" , Dict(editLayoutDesign, "smallComposeTextSize", "false/def") }, + { "Avatar Radius" , Dict(editLayoutDesign, "avatarRadius", "2/def") }, + { "Hide Tweet Actions" , Dict(editLayoutDesign, "hideTweetActions", "true/def") }, + { "Move Tweet Actions" , Dict(editLayoutDesign, "moveTweetActionsToRight", "true/def") }, + { "Theme Color Tweaks" , Dict(editLayoutDesign, "themeColorTweaks", "true/def") }, + { "Revert Icons" , Dict(editLayoutDesign, "revertIcons", "true/def") }, + { "Optimize Animations" , Dict(editLayoutDesign, "optimizeAnimations", "true/def") }, + { "Reply Account Mode" , ReplyAccountConfigFromPlugin }, + { "Template Count" , Exact(TemplateCountFromPlugin) }, + }.FinalizeReport(); + } + + private static UserConfig UserConfig => Program.UserConfig; + private static SystemConfig SysConfig => Program.SystemConfig; + + private static string Bool(bool value) => value ? "on" : "off"; + private static string Exact(int value) => value.ToString(); + private static string RoundUp(int value, int multiple) => (multiple*(int)Math.Ceiling((double)value/multiple)).ToString(); + private static string Dict(Dictionary<string, string> dict, string key, string def = "(unknown)") => dict.TryGetValue(key, out string value) ? value : def; + private static string List(IEnumerable<string> list) => string.Join("|", list.DefaultIfEmpty("(none)")); + + private static string SystemName { get; } + private static string SystemEdition { get; } + private static string SystemBuild { get; } + private static int RamSize { get; } + private static string GpuVendor { get; } + private static string[] ProgramArguments { get; } + + static AnalyticsReportGenerator(){ + string osName, osEdition, osBuild; + + try{ + using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false)){ + // ReSharper disable once PossibleNullReferenceException + osName = key.GetValue("ProductName") as string; + osEdition = key.GetValue("EditionID") as string; + osBuild = key.GetValue("CurrentBuild") as string; + + if (osName != null && osEdition != null){ + osName = osName.Replace(osEdition, "").TrimEnd(); + } + } + }catch{ + osName = osEdition = osBuild = null; + } + + SystemName = osName ?? "Windows (unknown)"; + SystemEdition = osEdition ?? "(unknown)"; + SystemBuild = osBuild ?? "(unknown)"; + + try{ + using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory")){ + foreach(ManagementBaseObject obj in searcher.Get()){ + RamSize += (int)((ulong)obj["Capacity"]/(1024L*1024L)); + } + } + }catch{ + RamSize = 0; + } + + string gpu = null; + + try{ + using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController")){ + foreach(ManagementBaseObject obj in searcher.Get()){ + string vendor = obj["Caption"] as string; + + if (!string.IsNullOrEmpty(vendor)){ + gpu = vendor; + } + } + } + }catch{ + // rip + } + + GpuVendor = gpu ?? "(unknown)"; + + Dictionary<string, string> args = new Dictionary<string, string>(); + Arguments.GetCurrentClean().ToDictionary(args); + ProgramArguments = args.Keys.Select(key => key.TrimStart('-')).ToArray(); + } + + private static string TrayMode{ + get{ + switch(UserConfig.TrayBehavior){ + case TrayIcon.Behavior.DisplayOnly: return "icon"; + case TrayIcon.Behavior.MinimizeToTray: return "minimize"; + case TrayIcon.Behavior.CloseToTray: return "close"; + case TrayIcon.Behavior.Combined: return "combined"; + default: return "off"; + } + } + } + + private static string NotificationPosition{ + get{ + switch(UserConfig.NotificationPosition){ + case TweetNotification.Position.TopLeft: return "top left"; + case TweetNotification.Position.TopRight: return "top right"; + case TweetNotification.Position.BottomLeft: return "bottom left"; + case TweetNotification.Position.BottomRight: return "bottom right"; + default: return "custom"; + } + } + } + + private static string NotificationSize{ + get{ + switch(UserConfig.NotificationSize){ + case TweetNotification.Size.Auto: return "auto"; + default: return RoundUp(UserConfig.CustomNotificationSize.Width, 20)+"x"+RoundUp(UserConfig.CustomNotificationSize.Height, 20); + } + } + } + + private static string NotificationTimer{ + get{ + if (!UserConfig.DisplayNotificationTimer){ + return "off"; + } + else{ + return UserConfig.NotificationTimerCountDown ? "count down" : "count up"; + } + } + } + + private static Dictionary<string, string> EditLayoutDesignPluginData{ + get{ + Dictionary<string, string> dict = new Dictionary<string, string>(); + + try{ + string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "edit-design", "config.json")); + + foreach(Match match in Regex.Matches(data, "\"(\\w+?)\":(.*?)[,}]")){ + dict[match.Groups[1].Value] = match.Groups[2].Value.Trim('"'); + } + }catch{ + // rip + } + + return dict; + } + } + + private static int TemplateCountFromPlugin{ + get{ + try{ + string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "templates", "config.json")); + return Math.Min(StringUtils.CountOccurrences(data, "{\"name\":"), StringUtils.CountOccurrences(data, ",\"contents\":")); + }catch{ + return 0; + } + } + } + + private static string ReplyAccountConfigFromPlugin{ + get{ + try{ + string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "reply-account", "configuration.js")).Replace(" ", ""); + + Match matchType = Regex.Match(data, "defaultAccount:\"([#@])(.*?)\"(?:,|$)"); + Match matchAdvanced = Regex.Match(data, "useAdvancedSelector:(.*?)(?:,|$)", RegexOptions.Multiline); + + if (!matchType.Success){ + return "(unknown)"; + } + + string accType = matchType.Groups[1].Value == "#" ? matchType.Groups[2].Value : "account"; + return matchAdvanced.Success && !matchAdvanced.Value.Contains("false") ? "advanced/"+accType : accType; + }catch{ + return "(unknown)"; + } + } + } + + public class ExternalInfo{ + public static ExternalInfo From(Form form){ + if (form == null){ + return new ExternalInfo(); + } + else{ + Screen screen = Screen.FromControl(form); + int dpi; + + using(Graphics graphics = form.CreateGraphics()){ + dpi = (int)graphics.DpiY; + } + + return new ExternalInfo{ + Resolution = screen.Bounds.Width+"x"+screen.Bounds.Height, + DPI = dpi + }; + } + } + + public string Resolution { get; private set; } + public int? DPI { get; private set; } + + private ExternalInfo(){} + } + } +} diff --git a/Core/Other/FormSettings.cs b/Core/Other/FormSettings.cs index 8840039b..66ae4885 100644 --- a/Core/Other/FormSettings.cs +++ b/Core/Other/FormSettings.cs @@ -4,6 +4,7 @@ using System.Windows.Forms; using TweetDuck.Core.Controls; using TweetDuck.Core.Notification.Example; +using TweetDuck.Core.Other.Analytics; using TweetDuck.Core.Other.Settings; using TweetDuck.Core.Other.Settings.Dialogs; using TweetDuck.Core.Utils; @@ -22,7 +23,7 @@ sealed partial class FormSettings : Form{ public bool ShouldReloadBrowser { get; private set; } - public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, Type startTab){ + public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, AnalyticsManager analytics, Type startTab){ InitializeComponent(); Text = Program.BrandName+" Options"; @@ -38,16 +39,17 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler up AddButton("System Tray", () => new TabSettingsTray()); AddButton("Notifications", () => new TabSettingsNotifications(new FormNotificationExample(browser, plugins))); AddButton("Sounds", () => new TabSettingsSounds()); - AddButton("Feedback", () => new TabSettingsFeedback()); - AddButton("Advanced", () => new TabSettingsAdvanced(browser.ReinjectCustomCSS)); + AddButton("Feedback", () => new TabSettingsFeedback(analytics, AnalyticsReportGenerator.ExternalInfo.From(this.browser), this.plugins)); + AddButton("Advanced", () => new TabSettingsAdvanced(this.browser.ReinjectCustomCSS)); SelectTab(tabs[startTab ?? typeof(TabSettingsGeneral)]); } private void FormSettings_FormClosing(object sender, FormClosingEventArgs e){ + currentTab.Control.OnClosing(); + foreach(SettingsTab tab in tabs.Values){ if (tab.IsInitialized){ - tab.Control.OnClosing(); tab.Control.Dispose(); } } @@ -57,11 +59,7 @@ private void FormSettings_FormClosing(object sender, FormClosingEventArgs e){ } private void btnManageOptions_Click(object sender, EventArgs e){ - foreach(SettingsTab tab in tabs.Values){ - if (tab.IsInitialized){ - tab.Control.OnClosing(); - } - } + currentTab.Control.OnClosing(); using(DialogSettingsManage dialog = new DialogSettingsManage(plugins)){ if (dialog.ShowDialog() == DialogResult.OK){ @@ -114,6 +112,7 @@ private void SelectTab<T>() where T : BaseTabSettings{ private void SelectTab(SettingsTab tab){ if (currentTab != null){ currentTab.Button.BackColor = SystemColors.Control; + currentTab.Control.OnClosing(); } tab.Button.BackColor = tab.Button.FlatAppearance.MouseDownBackColor; diff --git a/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs new file mode 100644 index 00000000..5af8b77f --- /dev/null +++ b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.Designer.cs @@ -0,0 +1,90 @@ +namespace TweetDuck.Core.Other.Settings.Dialogs { + partial class DialogSettingsAnalytics { + /// <summary> + /// Required designer variable. + /// </summary> + private System.ComponentModel.IContainer components = null; + + /// <summary> + /// Clean up any resources being used. + /// </summary> + /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> + protected override void Dispose(bool disposing) { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// <summary> + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// </summary> + private void InitializeComponent() { + this.textBoxReport = new System.Windows.Forms.TextBox(); + this.btnClose = new System.Windows.Forms.Button(); + this.labelInfo = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // textBoxReport + // + this.textBoxReport.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textBoxReport.Font = new System.Drawing.Font("Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); + this.textBoxReport.Location = new System.Drawing.Point(12, 41); + this.textBoxReport.Multiline = true; + this.textBoxReport.Name = "textBoxReport"; + this.textBoxReport.ReadOnly = true; + this.textBoxReport.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.textBoxReport.Size = new System.Drawing.Size(460, 480); + this.textBoxReport.TabIndex = 1; + // + // btnClose + // + this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnClose.Location = new System.Drawing.Point(416, 527); + this.btnClose.Name = "btnClose"; + this.btnClose.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); + this.btnClose.Size = new System.Drawing.Size(56, 23); + this.btnClose.TabIndex = 2; + this.btnClose.Text = "Close"; + this.btnClose.UseVisualStyleBackColor = true; + this.btnClose.Click += new System.EventHandler(this.btnClose_Click); + // + // labelInfo + // + this.labelInfo.Location = new System.Drawing.Point(12, 9); + this.labelInfo.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3); + this.labelInfo.Name = "labelInfo"; + this.labelInfo.Size = new System.Drawing.Size(460, 26); + this.labelInfo.TabIndex = 0; + this.labelInfo.Text = "When enabled, this data will be sent over a secure network roughly once every wee" + + "k.\r\nSome numbers in the report were made imprecise on purpose."; + // + // DialogSettingsAnalytics + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(484, 562); + this.Controls.Add(this.labelInfo); + this.Controls.Add(this.btnClose); + this.Controls.Add(this.textBoxReport); + this.MinimumSize = new System.Drawing.Size(450, 340); + this.Name = "DialogSettingsAnalytics"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TextBox textBoxReport; + private System.Windows.Forms.Button btnClose; + private System.Windows.Forms.Label labelInfo; + } +} \ No newline at end of file diff --git a/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs new file mode 100644 index 00000000..3cd169cf --- /dev/null +++ b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs @@ -0,0 +1,24 @@ +using System; +using System.Windows.Forms; +using TweetDuck.Core.Controls; +using TweetDuck.Core.Other.Analytics; + +namespace TweetDuck.Core.Other.Settings.Dialogs{ + sealed partial class DialogSettingsAnalytics : Form{ + public string CefArgs => textBoxReport.Text; + + public DialogSettingsAnalytics(AnalyticsReport report){ + InitializeComponent(); + + Text = Program.BrandName+" Options - Analytics Report"; + + textBoxReport.EnableMultilineShortcuts(); + textBoxReport.Text = report.ToString().TrimEnd(); + textBoxReport.Select(0, 0); + } + + private void btnClose_Click(object sender, EventArgs e){ + Close(); + } + } +} diff --git a/Core/Other/Settings/TabSettingsFeedback.Designer.cs b/Core/Other/Settings/TabSettingsFeedback.Designer.cs index 013db5bc..bcbc5615 100644 --- a/Core/Other/Settings/TabSettingsFeedback.Designer.cs +++ b/Core/Other/Settings/TabSettingsFeedback.Designer.cs @@ -25,12 +25,14 @@ protected override void Dispose(bool disposing) { private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.panelFeedback = new System.Windows.Forms.Panel(); + this.labelDataCollectionMessage = new System.Windows.Forms.Label(); + this.btnViewReport = new System.Windows.Forms.Button(); + this.btnSendFeedback = new System.Windows.Forms.Button(); this.labelDataCollectionLink = new System.Windows.Forms.LinkLabel(); this.checkDataCollection = new System.Windows.Forms.CheckBox(); this.labelDataCollection = new System.Windows.Forms.Label(); this.labelFeedback = new System.Windows.Forms.Label(); this.toolTip = new System.Windows.Forms.ToolTip(this.components); - this.btnSendFeedback = new System.Windows.Forms.Button(); this.panelFeedback.SuspendLayout(); this.SuspendLayout(); // @@ -38,15 +40,48 @@ private void InitializeComponent() { // this.panelFeedback.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.panelFeedback.Controls.Add(this.labelDataCollectionMessage); + this.panelFeedback.Controls.Add(this.btnViewReport); this.panelFeedback.Controls.Add(this.btnSendFeedback); this.panelFeedback.Controls.Add(this.labelDataCollectionLink); this.panelFeedback.Controls.Add(this.checkDataCollection); this.panelFeedback.Controls.Add(this.labelDataCollection); this.panelFeedback.Location = new System.Drawing.Point(9, 31); this.panelFeedback.Name = "panelFeedback"; - this.panelFeedback.Size = new System.Drawing.Size(322, 80); + this.panelFeedback.Size = new System.Drawing.Size(322, 188); this.panelFeedback.TabIndex = 1; // + // labelDataCollectionMessage + // + this.labelDataCollectionMessage.Location = new System.Drawing.Point(6, 114); + this.labelDataCollectionMessage.Margin = new System.Windows.Forms.Padding(6); + this.labelDataCollectionMessage.Name = "labelDataCollectionMessage"; + this.labelDataCollectionMessage.Size = new System.Drawing.Size(310, 67); + this.labelDataCollectionMessage.TabIndex = 5; + // + // btnViewReport + // + this.btnViewReport.AutoSize = true; + this.btnViewReport.Location = new System.Drawing.Point(6, 82); + this.btnViewReport.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3); + this.btnViewReport.Name = "btnViewReport"; + this.btnViewReport.Size = new System.Drawing.Size(144, 23); + this.btnViewReport.TabIndex = 4; + this.btnViewReport.Text = "View My Analytics Report"; + this.btnViewReport.UseVisualStyleBackColor = true; + // + // btnSendFeedback + // + this.btnSendFeedback.AutoSize = true; + this.btnSendFeedback.Location = new System.Drawing.Point(5, 3); + this.btnSendFeedback.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3); + this.btnSendFeedback.Name = "btnSendFeedback"; + this.btnSendFeedback.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); + this.btnSendFeedback.Size = new System.Drawing.Size(164, 23); + this.btnSendFeedback.TabIndex = 0; + this.btnSendFeedback.Text = "Send Feedback / Bug Report"; + this.btnSendFeedback.UseVisualStyleBackColor = true; + // // labelDataCollectionLink // this.labelDataCollectionLink.AutoSize = true; @@ -60,7 +95,6 @@ private void InitializeComponent() { this.labelDataCollectionLink.TabStop = true; this.labelDataCollectionLink.Text = "(learn more)"; this.labelDataCollectionLink.UseCompatibleTextRendering = true; - this.labelDataCollectionLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.labelDataCollectionLink_LinkClicked); // // checkDataCollection // @@ -94,18 +128,6 @@ private void InitializeComponent() { this.labelFeedback.TabIndex = 0; this.labelFeedback.Text = "Feedback"; // - // btnSendFeedback - // - this.btnSendFeedback.AutoSize = true; - this.btnSendFeedback.Location = new System.Drawing.Point(5, 3); - this.btnSendFeedback.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3); - this.btnSendFeedback.Name = "btnSendFeedback"; - this.btnSendFeedback.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); - this.btnSendFeedback.Size = new System.Drawing.Size(164, 23); - this.btnSendFeedback.TabIndex = 0; - this.btnSendFeedback.Text = "Send Feedback / Bug Report"; - this.btnSendFeedback.UseVisualStyleBackColor = true; - // // TabSettingsFeedback // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -113,7 +135,7 @@ private void InitializeComponent() { this.Controls.Add(this.panelFeedback); this.Controls.Add(this.labelFeedback); this.Name = "TabSettingsFeedback"; - this.Size = new System.Drawing.Size(340, 122); + this.Size = new System.Drawing.Size(340, 230); this.panelFeedback.ResumeLayout(false); this.panelFeedback.PerformLayout(); this.ResumeLayout(false); @@ -130,5 +152,7 @@ private void InitializeComponent() { private System.Windows.Forms.ToolTip toolTip; private System.Windows.Forms.LinkLabel labelDataCollectionLink; private System.Windows.Forms.Button btnSendFeedback; + private System.Windows.Forms.Button btnViewReport; + private System.Windows.Forms.Label labelDataCollectionMessage; } } diff --git a/Core/Other/Settings/TabSettingsFeedback.cs b/Core/Other/Settings/TabSettingsFeedback.cs index e0bf74da..3c265f3d 100644 --- a/Core/Other/Settings/TabSettingsFeedback.cs +++ b/Core/Other/Settings/TabSettingsFeedback.cs @@ -1,18 +1,34 @@ using System; using System.Windows.Forms; +using TweetDuck.Core.Other.Analytics; +using TweetDuck.Core.Other.Settings.Dialogs; using TweetDuck.Core.Utils; +using TweetDuck.Plugins; namespace TweetDuck.Core.Other.Settings{ sealed partial class TabSettingsFeedback : BaseTabSettings{ - public TabSettingsFeedback(){ + private readonly AnalyticsReportGenerator.ExternalInfo analyticsInfo; + private readonly PluginManager plugins; + + public TabSettingsFeedback(AnalyticsManager analytics, AnalyticsReportGenerator.ExternalInfo analyticsInfo, PluginManager plugins){ InitializeComponent(); + this.analyticsInfo = analyticsInfo; + this.plugins = plugins; + checkDataCollection.Checked = Config.AllowDataCollection; + + if (analytics != null){ + string collectionTime = analytics.LastCollectionMessage; + labelDataCollectionMessage.Text = string.IsNullOrEmpty(collectionTime) ? "No collection yet" : "Last collection: "+collectionTime; + } } public override void OnReady(){ btnSendFeedback.Click += btnSendFeedback_Click; checkDataCollection.CheckedChanged += checkDataCollection_CheckedChanged; + labelDataCollectionLink.LinkClicked += labelDataCollectionLink_LinkClicked; + btnViewReport.Click += btnViewReport_Click; } private void btnSendFeedback_Click(object sender, EventArgs e){ @@ -26,5 +42,11 @@ private void checkDataCollection_CheckedChanged(object sender, EventArgs e){ private void labelDataCollectionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e){ BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki/Send-anonymous-data"); } + + private void btnViewReport_Click(object sender, EventArgs e){ + using(DialogSettingsAnalytics dialog = new DialogSettingsAnalytics(AnalyticsReportGenerator.Create(analyticsInfo, plugins))){ + dialog.ShowDialog(); + } + } } } diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs index ffabc543..226c9207 100644 --- a/Core/Utils/BrowserUtils.cs +++ b/Core/Utils/BrowserUtils.cs @@ -95,9 +95,14 @@ public static string GetErrorName(CefErrorCode code){ return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty); } - public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){ + public static WebClient CreateWebClient(){ WebClient client = new WebClient{ Proxy = null }; client.Headers[HttpRequestHeader.UserAgent] = HeaderUserAgent; + return client; + } + + public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){ + WebClient client = CreateWebClient(); client.DownloadFileCompleted += (sender, args) => { if (args.Cancelled){ diff --git a/Core/Utils/StringUtils.cs b/Core/Utils/StringUtils.cs index 79bf0e95..156c03e6 100644 --- a/Core/Utils/StringUtils.cs +++ b/Core/Utils/StringUtils.cs @@ -18,5 +18,16 @@ public static int[] ParseInts(string str, char separator){ public static string ConvertPascalCaseToScreamingSnakeCase(string str){ return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpper(); } + + public static int CountOccurrences(string source, string substring){ + int count = 0, index = 0; + + while((index = source.IndexOf(substring, index)) != -1){ + index += substring.Length; + ++count; + } + + return count; + } } } diff --git a/Program.cs b/Program.cs index 68c0c931..a60d8088 100644 --- a/Program.cs +++ b/Program.cs @@ -36,6 +36,7 @@ static class Program{ public static string UserConfigFilePath => Path.Combine(StoragePath, "TD_UserConfig.cfg"); public static string SystemConfigFilePath => Path.Combine(StoragePath, "TD_SystemConfig.cfg"); public static string PluginConfigFilePath => Path.Combine(StoragePath, "TD_PluginConfig.cfg"); + public static string AnalyticsFilePath => Path.Combine(StoragePath, "TD_Analytics.cfg"); private static string ErrorLogFilePath => Path.Combine(StoragePath, "TD_Log.txt"); private static string ConsoleLogFilePath => Path.Combine(StoragePath, "TD_Console.txt"); diff --git a/Resources/Plugins/edit-design/browser.js b/Resources/Plugins/edit-design/browser.js index a8bb7574..27d027e6 100644 --- a/Resources/Plugins/edit-design/browser.js +++ b/Resources/Plugins/edit-design/browser.js @@ -6,6 +6,7 @@ enabled(){ this.config = null; this.defaultConfig = { + _theme: "light", columnWidth: "310px", fontSize: "12px", hideTweetActions: true, @@ -38,11 +39,13 @@ enabled(){ this.currentStage = 1; } else if (this.tmpConfig !== null){ + let needsResave = !("_theme" in this.tmpConfig); + this.config = $.extend(this.defaultConfig, this.tmpConfig); this.tmpConfig = null; this.reinjectAll(); - if (this.firstTimeLoad){ + if (this.firstTimeLoad || needsResave){ $TDP.writeFile(this.$token, configFile, JSON.stringify(this.config)); } } @@ -60,6 +63,8 @@ enabled(){ } else{ $(document).one("dataSettingsValues", () => { + this.defaultConfig._theme = TD.settings.getTheme(); + switch(TD.settings.getColumnWidth()){ case "wide": this.defaultConfig.columnWidth = "350px"; break; case "narrow": this.defaultConfig.columnWidth = "270px"; break; @@ -234,9 +239,11 @@ enabled(){ modal.find("[data-td-theme='"+TD.settings.getTheme()+"']").prop("checked", true); modal.find("[data-td-theme]").change(function(){ + me.config._theme = $(this).attr("data-td-theme"); + setTimeout(function(){ - TD.settings.setTheme($(this).attr("data-td-theme")); $(document).trigger("uiToggleTheme"); + me.saveConfig(); me.reinjectAll(); }, 1); }); diff --git a/TweetDuck.csproj b/TweetDuck.csproj index 36f77695..1e2d05b3 100644 --- a/TweetDuck.csproj +++ b/TweetDuck.csproj @@ -131,6 +131,10 @@ </Compile> <Compile Include="Core\Notification\SoundNotification.cs" /> <Compile Include="Core\Notification\TweetNotification.cs" /> + <Compile Include="Core\Other\Analytics\AnalyticsFile.cs" /> + <Compile Include="Core\Other\Analytics\AnalyticsManager.cs" /> + <Compile Include="Core\Other\Analytics\AnalyticsReport.cs" /> + <Compile Include="Core\Other\Analytics\AnalyticsReportGenerator.cs" /> <Compile Include="Core\Other\FormAbout.cs"> <SubType>Form</SubType> </Compile> @@ -156,6 +160,12 @@ <DependentUpon>FormPlugins.cs</DependentUpon> </Compile> <Compile Include="Core\Other\Management\VideoPlayer.cs" /> + <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsAnalytics.cs"> + <SubType>Form</SubType> + </Compile> + <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsAnalytics.Designer.cs"> + <DependentUpon>DialogSettingsAnalytics.cs</DependentUpon> + </Compile> <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsCSS.cs"> <SubType>Form</SubType> </Compile>