diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4154f88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.suo +*.user +bin +obj +packages \ No newline at end of file diff --git a/Hearth Arena Uploader.sln b/Hearth Arena Uploader.sln new file mode 100644 index 0000000..c5a573c --- /dev/null +++ b/Hearth Arena Uploader.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hearth Arena Uploader", "Hearth Arena Uploader\Hearth Arena Uploader.csproj", "{A650CB40-DE4E-4500-8FF5-1B18863FA228}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A650CB40-DE4E-4500-8FF5-1B18863FA228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A650CB40-DE4E-4500-8FF5-1B18863FA228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A650CB40-DE4E-4500-8FF5-1B18863FA228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A650CB40-DE4E-4500-8FF5-1B18863FA228}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Hearth Arena Uploader/Data/ArenaRewards.cs b/Hearth Arena Uploader/Data/ArenaRewards.cs new file mode 100644 index 0000000..1ca5bc8 --- /dev/null +++ b/Hearth Arena Uploader/Data/ArenaRewards.cs @@ -0,0 +1,58 @@ +namespace HearthArenaUploader.Data +{ + public class ArenaRewards + { + public ArenaRewards(int gold, int dust, int packs, int standardCards, int goldenCards) + { + this.gold = gold; + this.dust = dust; + this.packs = packs; + this.standardCards = standardCards; + this.goldenCards = goldenCards; + } + + private int gold; + public int Gold + { + get { return gold; } + set { gold = value; } + } + + private int dust; + + public int Dust + { + get { return dust; } + set { dust = value; } + } + + + private int packs; + + public int Packs + { + get { return packs; } + set { packs = value; } + } + + + private int standardCards; + + public int StandardCards + { + get { return standardCards; } + set { standardCards = value; } + } + + + private int goldenCards; + + public int GoldenCards + { + get { return goldenCards; } + set { goldenCards = value; } + } + + + } +} diff --git a/Hearth Arena Uploader/Data/HearthArenaClass.cs b/Hearth Arena Uploader/Data/HearthArenaClass.cs new file mode 100644 index 0000000..5ebaadc --- /dev/null +++ b/Hearth Arena Uploader/Data/HearthArenaClass.cs @@ -0,0 +1,15 @@ +namespace HearthArenaUploader.Data +{ + public enum HearthArenaClass + { + Druid = 1, + Hunter = 2, + Mage = 3, + Paladin = 4, + Priest = 5, + Rogue = 6, + Shaman = 7, + Warlock = 8, + Warrior = 9, + } +} diff --git a/Hearth Arena Uploader/Data/Result.cs b/Hearth Arena Uploader/Data/Result.cs new file mode 100644 index 0000000..2b0ece6 --- /dev/null +++ b/Hearth Arena Uploader/Data/Result.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HearthArenaUploader.Data +{ + public class Result + { + public Result(ResultDataType resultData, ResultType outcome, string errorMessage) + { + ResultData = resultData; + Outcome = outcome; + ErrorMessage = errorMessage; + } + + public ResultDataType ResultData { get; private set; } + + public ResultType Outcome { get; private set; } + + public string ErrorMessage { get; private set; } + } + + public class Result + { + public Result(ResultType outcome, string errorMessage) + { + Outcome = outcome; + ErrorMessage = errorMessage; + } + + public ResultType Outcome { get; private set; } + + public string ErrorMessage { get; private set; } + } +} diff --git a/Hearth Arena Uploader/Data/UploadResults.cs b/Hearth Arena Uploader/Data/UploadResults.cs new file mode 100644 index 0000000..fdfc309 --- /dev/null +++ b/Hearth Arena Uploader/Data/UploadResults.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HearthArenaUploader.Data +{ + public enum UploadResults + { + Success, + LoginFailedCredentialsWrong, + LoginFailedUnknownError, + SubmittingArenaRunFailedUnknownError, + ConnectionError + } +} diff --git a/Hearth Arena Uploader/Encryption.cs b/Hearth Arena Uploader/Encryption.cs new file mode 100644 index 0000000..84e97fd --- /dev/null +++ b/Hearth Arena Uploader/Encryption.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; + +namespace HearthArenaUploader +{ + public class Encryption + { + static byte[] entropy = Encoding.Unicode.GetBytes("Salt Is Not A Password"); + + public static string EncryptString(SecureString input) + { + byte[] encryptedData = ProtectedData.Protect( + Encoding.Unicode.GetBytes(ToInsecureString(input)), + entropy, + DataProtectionScope.CurrentUser); + return Convert.ToBase64String(encryptedData); + } + + public static SecureString DecryptString(string encryptedData) + { + try + { + byte[] decryptedData = ProtectedData.Unprotect( + Convert.FromBase64String(encryptedData), + entropy, + DataProtectionScope.CurrentUser); + return ToSecureString(Encoding.Unicode.GetString(decryptedData)); + } + catch + { + return new SecureString(); + } + } + + public static SecureString ToSecureString(string input) + { + SecureString secure = new SecureString(); + foreach (char c in input) + { + secure.AppendChar(c); + } + secure.MakeReadOnly(); + return secure; + } + + public static string ToInsecureString(SecureString input) + { + string returnValue = string.Empty; + IntPtr ptr = Marshal.SecureStringToBSTR(input); + try + { + returnValue = Marshal.PtrToStringBSTR(ptr); + } + finally + { + Marshal.ZeroFreeBSTR(ptr); + } + return returnValue; + } + } +} diff --git a/Hearth Arena Uploader/Hearth Arena Uploader.csproj b/Hearth Arena Uploader/Hearth Arena Uploader.csproj new file mode 100644 index 0000000..58393eb --- /dev/null +++ b/Hearth Arena Uploader/Hearth Arena Uploader.csproj @@ -0,0 +1,146 @@ + + + + + Debug + AnyCPU + {A650CB40-DE4E-4500-8FF5-1B18863FA228} + Library + Properties + HearthArenaUploader + Hearth Arena Uploader + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + true + ..\..\Hearthstone-Deck-TrackerPlugins\Hearthstone Deck Tracker\bin\HDTandPlugins\Plugins\HearthArenaUploader\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + + + + ..\..\Hearthstone-Deck-TrackerPlugins\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll + True + + + ..\..\Hearthstone-Deck-TrackerPlugins\packages\MahApps.Metro.1.1.2.0\lib\net45\MahApps.Metro.dll + False + + + + + + + + + ..\..\Hearthstone-Deck-TrackerPlugins\packages\MahApps.Metro.1.1.2.0\lib\net45\System.Windows.Interactivity.dll + True + + + + + + + + 4.0 + + + + + + + + + + + + + SettingsWindow.xaml + + + MSBuild:Compile + Designer + + + + + + + + MainWindow.xaml + Code + + + Designer + MSBuild:Compile + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + {e63a3f1c-e662-4e62-be43-af27cb9e953d} + Hearthstone Deck Tracker + False + + + + + \ No newline at end of file diff --git a/Hearth Arena Uploader/HearthArenaUploaderLogic.cs b/Hearth Arena Uploader/HearthArenaUploaderLogic.cs new file mode 100644 index 0000000..d664617 --- /dev/null +++ b/Hearth Arena Uploader/HearthArenaUploaderLogic.cs @@ -0,0 +1,312 @@ +using HearthArenaUploader.Data; +using Hearthstone_Deck_Tracker; +using Hearthstone_Deck_Tracker.Enums; +using Hearthstone_Deck_Tracker.Hearthstone; +using Hearthstone_Deck_Tracker.Stats; +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Security; +using System.Threading.Tasks; +using System.Web; +using HtmlDocument = HtmlAgilityPack.HtmlDocument; + +namespace HearthArenaUploader +{ + public class HearthArenaUploaderLogic + { + readonly string rememberMe = "on"; + readonly string submit = "Login"; + readonly string connectionError = "connection error. Try again, whene heartharena.com is available."; + string username; + SecureString password; + + Uri uriAddArena = new Uri("https://www.heartharena.com/my-arenas/add"); + + public HearthArenaUploaderLogic(string username, SecureString password) + { + this.username = username; + this.password = password; + } + + public async Task> LoginAndSubmitArenaRuns(IEnumerable runs, Action setProgress = null) + { + Result result = await LogInToHearthArena(); + CookieContainer cookieContainer = result.ResultData; + if (result.Outcome == UploadResults.Success && cookieContainer != null) + { + Logger.WriteLine(string.Format("Uploading {0} run(s)", runs.Count()), HearthArenaUploaderPlugin.LogCategory); + int i = 1; + int max = runs.Count(); + foreach (Deck run in runs) + { + Result uploadResult = await SubmitArenaRun(run, cookieContainer); + if (uploadResult.Outcome != UploadResults.Success) + { + return uploadResult; + } + if (setProgress != null) + setProgress((double)i++ / max); + } + Logger.WriteLine("Upload successful", HearthArenaUploaderPlugin.LogCategory); + return new Result(UploadResults.Success, string.Empty); + } + + return new Result(result.Outcome, result.ErrorMessage); + } + + // returns null, if login failed + private async Task> LogInToHearthArena() + { + CookieContainer cookieContainer = null; + Uri uriLogin = new Uri("https://www.heartharena.com/login"); + Uri uriLoginCheck = new Uri("https://www.heartharena.com/login_check"); + + // get login token + HttpWebRequest httpWebRequest = WebRequest.Create(uriLogin) as HttpWebRequest; + httpWebRequest.Method = "GET"; + httpWebRequest.ProtocolVersion = new Version("1.1"); + httpWebRequest.KeepAlive = true; + cookieContainer = new CookieContainer(); + httpWebRequest.CookieContainer = cookieContainer; + HttpWebResponse webResponse; + try + { + webResponse = await httpWebRequest.GetResponseAsync() as HttpWebResponse; + } + catch (Exception e) + { + string connectionErrorExtended = connectionError + Environment.NewLine + e.Message + Environment.NewLine + e.StackTrace; + Logger.WriteLine(connectionErrorExtended, HearthArenaUploaderPlugin.LogCategory); + return new Result(null, UploadResults.ConnectionError, connectionErrorExtended); + } + + string content = string.Empty; + + using (StreamReader sr = new StreamReader(webResponse.GetResponseStream())) + { + content = sr.ReadToEnd(); + } + + HtmlDocument doc = new HtmlDocument(); + doc.LoadHtml(content); + HtmlNode node = doc.DocumentNode.SelectSingleNode(@"//input[@name='_csrf_token']"); + string csrfToken = node.Attributes["value"].Value; + + string body = ""; + body = CreatePostBodyString(GetLoginBodyArgs(csrfToken)); + + // post login + HttpWebRequest httpWebRequestLogin = WebRequest.Create(uriLoginCheck) as HttpWebRequest; + httpWebRequestLogin.Method = "POST"; + httpWebRequestLogin.ProtocolVersion = new Version("1.1"); + httpWebRequestLogin.KeepAlive = true; + httpWebRequestLogin.ContentType = "application/x-www-form-urlencoded"; + httpWebRequestLogin.Referer = uriLogin.ToString(); + httpWebRequestLogin.CookieContainer = cookieContainer; + using (StreamWriter stOut = new StreamWriter(httpWebRequestLogin.GetRequestStream(), System.Text.Encoding.ASCII)) + { + stOut.Write(body); + stOut.Close(); + } + + HttpWebResponse webResponseLogin; + + try + { + webResponseLogin = await httpWebRequestLogin.GetResponseAsync() as HttpWebResponse; + } + catch (Exception e) + { + string connectionErrorExtended = connectionError + Environment.NewLine + e.Message + Environment.NewLine + e.StackTrace; + Logger.WriteLine(connectionErrorExtended, HearthArenaUploaderPlugin.LogCategory); + return new Result(null, UploadResults.ConnectionError, connectionErrorExtended); + } + + using (StreamReader sr = new StreamReader(webResponseLogin.GetResponseStream())) + { + content = sr.ReadToEnd(); + } + + bool success = webResponseLogin.ResponseUri.AbsoluteUri == @"https://www.heartharena.com/account"; + + if (!success) + { + if (webResponseLogin.ResponseUri.AbsoluteUri == uriLogin.ToString()) + { + Logger.WriteLine("Login to Hearth Arena failed: wrong credentials", HearthArenaUploaderPlugin.LogCategory); + return new Result(null, UploadResults.LoginFailedCredentialsWrong, "wrong credentials"); + } + else + { + Logger.WriteLine("Login to Hearth Arena failed: unknown error (Response url: " + webResponseLogin.ResponseUri.AbsoluteUri + ")", HearthArenaUploaderPlugin.LogCategory); + return new Result(null, UploadResults.LoginFailedUnknownError, "unknown error (Response url: " + webResponseLogin.ResponseUri.AbsoluteUri + ")."); + } + } + + Logger.WriteLine("Login to Hearth Arena successful", HearthArenaUploaderPlugin.LogCategory); + return new Result(cookieContainer, UploadResults.Success, string.Empty); + } + + private async Task> SubmitArenaRun(Deck run, CookieContainer cookieContainer) + { + // get add arena token + HttpWebRequest httpWebRequestAddArena = WebRequest.Create(uriAddArena) as HttpWebRequest; + httpWebRequestAddArena.Method = "GET"; + httpWebRequestAddArena.ProtocolVersion = new Version("1.1"); + httpWebRequestAddArena.KeepAlive = true; + httpWebRequestAddArena.CookieContainer = cookieContainer; + HttpWebResponse webResponseAddArena = null; + try + { + webResponseAddArena = await httpWebRequestAddArena.GetResponseAsync() as HttpWebResponse; + } + catch (Exception e) + { + string connectionErrorExtended = connectionError + Environment.NewLine + e.Message + Environment.NewLine + e.StackTrace; + Logger.WriteLine(connectionErrorExtended, HearthArenaUploaderPlugin.LogCategory); + return new Result(UploadResults.ConnectionError, connectionErrorExtended); + } + + string content = string.Empty; + using (StreamReader sr = new StreamReader(webResponseAddArena.GetResponseStream())) + { + content = sr.ReadToEnd(); + } + + // get token from html + HtmlDocument doc = new HtmlDocument(); + doc.LoadHtml(content); + HtmlNode node = doc.DocumentNode.SelectSingleNode(@"//input[@name='heartharena_arenabundle_arenarun[_token]']"); + string token = node.Attributes["value"].Value; + + // post add arena request + string postReq = ConvertArenaRunToRequest(run, token); + + HttpWebRequest httpWebPostAddArena = WebRequest.Create(uriAddArena) as HttpWebRequest; + httpWebPostAddArena.Method = "POST"; + httpWebPostAddArena.ProtocolVersion = new Version("1.1"); + httpWebPostAddArena.KeepAlive = true; + httpWebPostAddArena.ContentType = "application/x-www-form-urlencoded"; + httpWebPostAddArena.Referer = uriAddArena.ToString(); + httpWebPostAddArena.CookieContainer = cookieContainer; + using (StreamWriter stOut = new StreamWriter(httpWebPostAddArena.GetRequestStream(), System.Text.Encoding.ASCII)) + { + stOut.Write(postReq); + stOut.Close(); + } + + HttpWebResponse webResponsePostAddArena; + try + { + webResponsePostAddArena = await httpWebPostAddArena.GetResponseAsync() as HttpWebResponse; + } + catch (Exception e) + { + string connectionErrorExtended = connectionError + Environment.NewLine + e.Message + Environment.NewLine + e.StackTrace; + Logger.WriteLine(connectionErrorExtended, HearthArenaUploaderPlugin.LogCategory); + return new Result(UploadResults.ConnectionError, connectionErrorExtended); + } + + using (StreamReader sr = new StreamReader(webResponsePostAddArena.GetResponseStream())) + { + content = sr.ReadToEnd(); + } + + bool success = webResponsePostAddArena.ResponseUri.AbsoluteUri == @"https://www.heartharena.com/my-arenas"; + if (!success) + { + Logger.WriteLine("Submitting arena run failed" + + Environment.NewLine + webResponsePostAddArena.StatusCode + " (" + webResponsePostAddArena.ResponseUri + ")", HearthArenaUploaderPlugin.LogCategory); + } + + PluginSettings.Instance.UploadedDecks.Add(run.DeckId); + return new Result(UploadResults.Success, string.Empty); + } + + private Dictionary GetLoginBodyArgs(string csrf_token) + { + Dictionary bodyArgs = new Dictionary(); + bodyArgs["_csrf_token"] = csrf_token; + bodyArgs["_username"] = username; + bodyArgs["_password"] = Encryption.ToInsecureString(password); + bodyArgs["_remember_me"] = rememberMe; + bodyArgs["_submit"] = submit; + return bodyArgs; + } + + private string ConvertArenaRunToRequest(Deck arenaRun, string token) + { + const string prefix = "heartharena_arenabundle_arenarun"; + GameStats firstGame = arenaRun.DeckStats.Games.FirstOrDefault(); + string date = firstGame != null ? firstGame.StartTime.ToString("MM/dd/yyyy", CultureInfo.GetCultureInfo("en-US")) : DateTime.Now.ToString("MM/dd/yyyy", CultureInfo.GetCultureInfo("en-US")); + HearthArenaClass deckClass; + bool valid = Enum.TryParse(arenaRun.Class, out deckClass); + Dictionary bodyArgs = new Dictionary() + { + {prefix + AddSquareBrackets("classification"), ((int) deckClass).ToString()}, + {prefix + AddSquareBrackets("wins"), arenaRun.DeckStats.Games.Where(stats => stats.Result == GameResult.Win).Count().ToString()}, + {prefix + AddSquareBrackets("losses"), arenaRun.DeckStats.Games.Where(stats => stats.Result == GameResult.Loss).Count().ToString()}, + {prefix + AddSquareBrackets("created_at"), date}, + {prefix + AddSquareBrackets("notes"), ""}, + {prefix + AddSquareBrackets("reward") + AddSquareBrackets("gold"), arenaRun.ArenaReward.Gold.ToString()}, + {prefix + AddSquareBrackets("reward") + AddSquareBrackets("dust"), arenaRun.ArenaReward.Dust.ToString()}, + {prefix + AddSquareBrackets("reward") + AddSquareBrackets("packs"), arenaRun.ArenaReward.Packs.Count(pack => pack != ArenaRewardPacks.None).ToString()}, + {prefix + AddSquareBrackets("reward") + AddSquareBrackets("plainCards"), arenaRun.ArenaReward.Cards.Count(card => card != null && !card.Golden).ToString()}, + {prefix + AddSquareBrackets("reward") + AddSquareBrackets("goldenCards"), arenaRun.ArenaReward.Cards.Count(card => card != null && card.Golden).ToString()}, + {prefix + AddSquareBrackets("_token"), token} + + }; + + string match_prefix = prefix + AddSquareBrackets("matches"); + string match_prefix_indexed; + int matchNr = 1; + foreach (GameStats match in arenaRun.DeckStats.Games) + { + string result; + if (match.Result == GameResult.Win) + result = "win"; + else if (match.Result == GameResult.Loss) + result = "loss"; + else + continue; + match_prefix_indexed = match_prefix + AddSquareBrackets(matchNr.ToString()); + HearthArenaClass hearthArenaClass; + bool success = Enum.TryParse(match.OpponentHero, out hearthArenaClass); + bodyArgs[match_prefix_indexed + AddSquareBrackets("opponent")] = ((int)hearthArenaClass).ToString(); + bodyArgs[match_prefix_indexed + AddSquareBrackets("result")] = result; + bodyArgs[match_prefix_indexed + AddSquareBrackets("coin")] = match.Coin ? "coin" : "no-coin"; + matchNr++; + } + + return CreatePostBodyString(bodyArgs); + } + + private string AddSquareBrackets(string text) + { + return "[" + text + "]"; + } + + // create body for application/x-www-form-urlencoded + private string CreatePostBodyString(Dictionary bodyArgs) + { + string body = string.Empty; + foreach (KeyValuePair kvp in bodyArgs) + { + body += HttpUtility.UrlEncode(kvp.Key) + "=" + HttpUtility.UrlEncode(kvp.Value); + body += "&"; + } + if (body.Last() == '&') + body = body.Remove(body.Length - 1); + + // return HttpUtility.UrlEncode(body); + // return Uri.EscapeUriString(body); + return body; + } + + } +} diff --git a/Hearth Arena Uploader/HearthArenaUploaderPlugin.cs b/Hearth Arena Uploader/HearthArenaUploaderPlugin.cs new file mode 100644 index 0000000..8db2ff9 --- /dev/null +++ b/Hearth Arena Uploader/HearthArenaUploaderPlugin.cs @@ -0,0 +1,99 @@ +using HearthArenaUploader.View; +using Hearthstone_Deck_Tracker.Hearthstone; +using MahApps.Metro.Controls.Dialogs; +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.Controls; + +namespace HearthArenaUploader +{ + public class HearthArenaUploaderPlugin : Hearthstone_Deck_Tracker.Plugins.IPlugin + { + public static readonly string LogCategory = "HearthArenaUploader"; + + public void OnLoad() + { + PluginSettings.Load(); + CreateMenuItem(); + } + + private void CreateMenuItem() + { + MenuItem = new MenuItem() + { + Header = "Hearth Arena Uploader" + }; + + MenuItem.Click += (sender, args) => + { + ShowWindow(); + }; + } + + private void ShowWindow() + { + MainWindow mainWindow = new MainWindow(); + mainWindow.Show(); + } + + public void OnUnload() + { + PluginSettings.Save(); + } + + public void OnButtonPress() + { + SettingsWindow settingsWindow = new SettingsWindow(); + settingsWindow.Show(); + } + + public void OnUpdate() + { + + } + + public string Name + { + get { return "Hearth Arena Uploader"; } + } + + public string Description + { + get + { + return @"Plugin for uploading your arena runs to Hearth Arena (http://heartharena.com). +Suggestions and bug reports can be sent to https://github.com/riQQ/Hearth-Arena-Uploader."; + } + } + + public string ButtonText + { + get { return "Settings"; } + } + + public string Author + { + get { return "riQQ"; } + } + + public static readonly Version PluginVersion = new Version(0, 1, 0); + + public Version Version + { + get { return PluginVersion; } + } + + public MenuItem MenuItem + { + get; + private set; + } + + internal static PluginSettings Settings { get; set; } + } +} diff --git a/Hearth Arena Uploader/PluginSettings.cs b/Hearth Arena Uploader/PluginSettings.cs new file mode 100644 index 0000000..d6efe1e --- /dev/null +++ b/Hearth Arena Uploader/PluginSettings.cs @@ -0,0 +1,106 @@ +using Hearthstone_Deck_Tracker; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Security; +using System.Text; +using System.Windows; +using System.Xml.Serialization; + +namespace HearthArenaUploader +{ + public class PluginSettings + { + private static PluginSettings _pluginSettings; + private static readonly string configFileName = "config.xml"; + + private PluginSettings() + { + + } + + public static PluginSettings Instance + { + get + { + if (_pluginSettings == null) + { + _pluginSettings = new PluginSettings(); + } + + return _pluginSettings; + } + } + + public static void Load() + { + string configDir = Path.Combine(Config.Instance.DataDir, "HearthArenaUploader"); + bool noConfig = false; + if (!Directory.Exists(configDir)) + { + noConfig = true; + Directory.CreateDirectory(configDir); + } + + if (!noConfig) + { + try + { + string configPath = Path.Combine(configDir, configFileName); + if (File.Exists(configPath)) + { + _pluginSettings = XmlManager.Load(configPath); + } + + } + catch (Exception e) + { + MessageBox.Show( + e.Message + "\n\n" + e.InnerException + "\n\n If you don't know how to fix this, please delete " + + configDir, "Error loading config.xml"); + } + } + } + + public static void Save() + { + string configDir = Path.Combine(Config.Instance.DataDir, "HearthArenaUploader"); + XmlManager.Save(Path.Combine(configDir, configFileName), Instance); + } + + #region Settings + public string AccountName = string.Empty; + + private string password; + + [XmlIgnore] + public SecureString Password + { + get + { + return Encryption.DecryptString(password); + } + set + { + password = Encryption.EncryptString(value); + } + } + + public string EncryptedPassword + { + get + { + return password; + } + set + { + password = value; + } + } + + public HashSet UploadedDecks = new HashSet(); + #endregion + } +} diff --git a/Hearth Arena Uploader/Properties/AssemblyInfo.cs b/Hearth Arena Uploader/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4a1640f --- /dev/null +++ b/Hearth Arena Uploader/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die mit einer Assembly verknüpft sind. +[assembly: AssemblyTitle("Hearth Arena Uploader")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Hearth Arena Uploader")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar +// für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von +// COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest. +[assembly: ComVisible(false)] + +//Um mit dem Erstellen lokalisierbarer Anwendungen zu beginnen, legen Sie +//ImCodeVerwendeteKultur in der .csproj-Datei +//in einer fest. Wenn Sie in den Quelldateien beispielsweise Deutsch +//(Deutschland) verwenden, legen Sie auf \"de-DE\" fest. Heben Sie dann die Auskommentierung +//des nachstehenden NeutralResourceLanguage-Attributs auf. Aktualisieren Sie "en-US" in der nachstehenden Zeile, +//sodass es mit der UICulture-Einstellung in der Projektdatei übereinstimmt. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //Speicherort der designspezifischen Ressourcenwörterbücher + //(wird verwendet, wenn eine Ressource auf der Seite + // oder in den Anwendungsressourcen-Wörterbüchern nicht gefunden werden kann.) + ResourceDictionaryLocation.SourceAssembly //Speicherort des generischen Ressourcenwörterbuchs + //(wird verwendet, wenn eine Ressource auf der Seite, in der Anwendung oder einem + // designspezifischen Ressourcenwörterbuch nicht gefunden werden kann.) +)] + + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern +// übernehmen, indem Sie "*" eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Hearth Arena Uploader/Properties/Resources.Designer.cs b/Hearth Arena Uploader/Properties/Resources.Designer.cs new file mode 100644 index 0000000..8c9b9ae --- /dev/null +++ b/Hearth Arena Uploader/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// Dieser Code wurde von einem Tool generiert. +// Laufzeitversion:4.0.30319.18444 +// +// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn +// der Code erneut generiert wird. +// +//------------------------------------------------------------------------------ + +namespace HearthArenaUploader.Properties { + using System; + + + /// + /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. + /// + // Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert + // -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. + // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen + // mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HearthArenaUploader.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle + /// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Hearth Arena Uploader/Properties/Resources.resx b/Hearth Arena Uploader/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Hearth Arena Uploader/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Hearth Arena Uploader/Properties/Settings.Designer.cs b/Hearth Arena Uploader/Properties/Settings.Designer.cs new file mode 100644 index 0000000..557d667 --- /dev/null +++ b/Hearth Arena Uploader/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// Dieser Code wurde von einem Tool generiert. +// Laufzeitversion:4.0.30319.18444 +// +// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn +// der Code erneut generiert wird. +// +//------------------------------------------------------------------------------ + +namespace HearthArenaUploader.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Hearth Arena Uploader/Properties/Settings.settings b/Hearth Arena Uploader/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Hearth Arena Uploader/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Hearth Arena Uploader/View/CardsConverter.cs b/Hearth Arena Uploader/View/CardsConverter.cs new file mode 100644 index 0000000..8938375 --- /dev/null +++ b/Hearth Arena Uploader/View/CardsConverter.cs @@ -0,0 +1,26 @@ +using Hearthstone_Deck_Tracker.Hearthstone; +using Hearthstone_Deck_Tracker.Controls.Stats; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace HearthArenaUploader.View +{ + public class CardsConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + ArenaReward.CardReward[] rewards = value as ArenaReward.CardReward[]; + return rewards.Count(card => card != null && !card.Golden).ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} diff --git a/Hearth Arena Uploader/View/GoldenCardsConverter.cs b/Hearth Arena Uploader/View/GoldenCardsConverter.cs new file mode 100644 index 0000000..18cc39a --- /dev/null +++ b/Hearth Arena Uploader/View/GoldenCardsConverter.cs @@ -0,0 +1,26 @@ +using Hearthstone_Deck_Tracker.Hearthstone; +using Hearthstone_Deck_Tracker.Controls.Stats; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace HearthArenaUploader.View +{ + public class GoldenCardsConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + ArenaReward.CardReward[] rewards = value as ArenaReward.CardReward[]; + return rewards.Count(card => card != null && card.Golden).ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} diff --git a/Hearth Arena Uploader/View/MainWindow.xaml b/Hearth Arena Uploader/View/MainWindow.xaml new file mode 100644 index 0000000..8de0d93 --- /dev/null +++ b/Hearth Arena Uploader/View/MainWindow.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + Hide already uploaded + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hearth Arena Uploader/View/MainWindow.xaml.cs b/Hearth Arena Uploader/View/MainWindow.xaml.cs new file mode 100644 index 0000000..d4518cc --- /dev/null +++ b/Hearth Arena Uploader/View/MainWindow.xaml.cs @@ -0,0 +1,113 @@ +using HearthArenaUploader.Data; +using Hearthstone_Deck_Tracker; +using Hearthstone_Deck_Tracker.Hearthstone; +using MahApps.Metro.Controls; +using MahApps.Metro.Controls.Dialogs; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace HearthArenaUploader.View +{ + /// + /// + /// + public partial class MainWindow : MetroWindow + { + private bool hideUploaded = false; + ICollectionView collectionView; + + public MainWindow() + { + InitializeComponent(); + + hideUploaded = false; + collectionView = CollectionViewSource.GetDefaultView(DeckList.Instance.Decks); + collectionView.Filter = (deckObj) => + { + Deck tempDeck = deckObj as Deck; + HashSet uploadedDecks = PluginSettings.Instance.UploadedDecks; + return tempDeck != null && tempDeck.IsArenaDeck && (!hideUploaded || !uploadedDecks.Contains(tempDeck.DeckId)); + }; + this.dataGrid.ItemsSource = collectionView; + this.dataGrid.Items.SortDescriptions.Add(new SortDescription(this.lastPlayedColumn.SortMemberPath, ListSortDirection.Descending)); + this.lastPlayedColumn.SortDirection = ListSortDirection.Descending; + } + + private async void ButtonUploadSelectedRuns_Click(object sender, System.Windows.RoutedEventArgs e) + { + await UploadSelectedRuns(); + } + + private void ButtonOpenSettings_Click(object sender, System.Windows.RoutedEventArgs e) + { + OpenSettings(); + } + + private void HideAlreadyUploaded_Checked(object sender, System.Windows.RoutedEventArgs e) + { + HideUploaded = checkBoxHideAlreadyUploaded.IsChecked.HasValue && checkBoxHideAlreadyUploaded.IsChecked.Value; + } + + public async Task UploadSelectedRuns() + { + MetroWindow pluginWindow = Window.GetWindow(this) as MetroWindow; + + if (string.IsNullOrEmpty(PluginSettings.Instance.AccountName) || string.IsNullOrEmpty(PluginSettings.Instance.EncryptedPassword)) + { + MessageDialogResult messageDialogResult = await pluginWindow.ShowMessageAsync("Error", "Empty account name and/or password. Do you want to open the settings?", MessageDialogStyle.AffirmativeAndNegative); + if (messageDialogResult == MessageDialogResult.Affirmative) + { + OpenSettings(); + } + return; + } + HearthArenaUploaderLogic controller = new HearthArenaUploaderLogic(PluginSettings.Instance.AccountName, PluginSettings.Instance.Password); + ProgressDialogController progressDialogController = await pluginWindow.ShowProgressAsync("Upload to Hearth Arena", "Uploading arena runs to Hearth Arena"); + + Result result = await controller.LoginAndSubmitArenaRuns(SelectedDecks, progressDialogController.SetProgress); + collectionView.Refresh(); + await progressDialogController.CloseAsync(); + if (result.Outcome != UploadResults.Success) + { + await pluginWindow.ShowMessageAsync("Error", "Error uploading arena runs: " + result.ErrorMessage); + } + } + + private async void MenuItem_Click_UploadToHearthArena(object sender, RoutedEventArgs e) + { + await UploadSelectedRuns(); + } + + public List SelectedDecks + { + get + { + return this.dataGrid.SelectedItems.Cast().ToList(); + } + } + + public void OpenSettings() + { + SettingsWindow settingsWindow = new SettingsWindow(); + settingsWindow.Show(); + } + + public bool HideUploaded + { + get + { + return hideUploaded; + } + set + { + hideUploaded = value; + collectionView.Refresh(); + } + } + } +} diff --git a/Hearth Arena Uploader/View/PacksConverter.cs b/Hearth Arena Uploader/View/PacksConverter.cs new file mode 100644 index 0000000..16bb8dc --- /dev/null +++ b/Hearth Arena Uploader/View/PacksConverter.cs @@ -0,0 +1,25 @@ +using Hearthstone_Deck_Tracker.Enums; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace HearthArenaUploader.View +{ + public class PacksConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + ArenaRewardPacks[] rewards = value as ArenaRewardPacks[]; + return rewards.Count(pack => pack != ArenaRewardPacks.None).ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} diff --git a/Hearth Arena Uploader/View/SettingsWindow.xaml b/Hearth Arena Uploader/View/SettingsWindow.xaml new file mode 100644 index 0000000..fc90a6c --- /dev/null +++ b/Hearth Arena Uploader/View/SettingsWindow.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/Hearth Arena Uploader/View/SettingsWindow.xaml.cs b/Hearth Arena Uploader/View/SettingsWindow.xaml.cs new file mode 100644 index 0000000..04e196e --- /dev/null +++ b/Hearth Arena Uploader/View/SettingsWindow.xaml.cs @@ -0,0 +1,37 @@ +using MahApps.Metro.Controls; +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; + +namespace HearthArenaUploader +{ + /// + /// Interaktionslogik für SettingsWindow.xaml + /// + public partial class SettingsWindow : MetroWindow + { + public SettingsWindow() + { + InitializeComponent(); + textboxAccountName.Text = PluginSettings.Instance.AccountName ?? string.Empty; + passwordbox.Password = Encryption.ToInsecureString(PluginSettings.Instance.Password); + } + + private void Button_Click(object sender, RoutedEventArgs e) + { + PluginSettings.Instance.AccountName = textboxAccountName.Text; + PluginSettings.Instance.Password = Encryption.ToSecureString(passwordbox.Password); + PluginSettings.Save(); + } + } +}