├── .editorconfig
├── .github
└── workflows
│ └── pr_validation.yml
├── .gitignore
├── .gitmodules
├── Hotsapi.Uploader.Common.Test
├── Hotsapi.Uploader.Common.Test.csproj
├── ManagerTests.cs
├── MockAnalizer.cs
├── MockStorage.cs
├── MockUploader.cs
└── NoNewFilesMonitor.cs
├── Hotsapi.Uploader.Common
├── Analyzer.cs
├── DeleteFiles.cs
├── EventArgs.cs
├── Extensions.cs
├── Hotsapi.Uploader.Common.csproj
├── IAnalyzer.cs
├── IMonitor.cs
├── IReplayStorage.cs
├── IUploader.cs
├── Manager.cs
├── Monitor.cs
├── ObservableCollectionEx.cs
├── ReplayFile.cs
├── ReplayStorage.cs
├── UploadStatus.cs
├── Uploader.cs
└── packages.config
├── Hotsapi.Uploader.Windows
├── App.config
├── App.xaml
├── App.xaml.cs
├── Hotsapi.Uploader.Windows.csproj
├── Hotsapi.Uploader.nuspec
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── NLog.config
├── NLog.xsd
├── Program.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── Resources
│ ├── uploader_dark.png
│ ├── uploader_icon_dark.ico
│ ├── uploader_icon_light.ico
│ └── uploader_light.png
├── SettingsWindow.xaml
├── SettingsWindow.xaml.cs
├── Themes
│ ├── Default
│ │ └── Default.xaml
│ └── MetroDark
│ │ ├── MetroDark.Hotsapi.Implicit.xaml
│ │ ├── MetroDark.MSControls.Core.Implicit.xaml
│ │ ├── Styles.Shared.xaml
│ │ ├── Styles.WPF.xaml
│ │ └── Theme.Colors.xaml
└── UIHelpers
│ ├── FilenameConverter.cs
│ ├── FlagsConverter.cs
│ ├── GenericValueConverter.cs
│ ├── IntToVisibilityConverter.cs
│ ├── MarginSetter.cs
│ ├── UploadColorConverter.cs
│ └── UploadStatusConverter.cs
├── Hotsapi.Uploader.sln
├── LICENSE
├── README.md
└── appveyor.yml
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | [*.cs]
3 | indent_style = space
4 | indent_size = 4
5 |
6 | ;csharp_new_line_before_open_brace = accessors, anonymous_methods, anonymous_types, control_blocks, events, indexers, lambdas, object_collection
7 | csharp_new_line_before_open_brace = types, methods, properties
8 | csharp_new_line_before_else = false
9 | csharp_new_line_before_catch = true
10 | csharp_new_line_before_finally = true
11 | csharp_new_line_before_members_in_object_initializers = true
12 | csharp_new_line_before_members_in_anonymous_types = true
13 | csharp_new_line_within_query_expression_clauses = true
--------------------------------------------------------------------------------
/.github/workflows/pr_validation.yml:
--------------------------------------------------------------------------------
1 | name: PR validation
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-16.04
8 | strategy:
9 | matrix:
10 | dotnet: ['3.0.100', '3.1.100-preview1-014459' ]
11 | name: Test on ${{ matrix.dotnet }}
12 | steps:
13 | - uses: actions/checkout@v1
14 | with:
15 | submodules: true
16 | - name: Setup dotnet
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: ${{ matrix.dotnet }}
20 | - run: dotnet test Hotsapi.Uploader.Common.Test
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 |
84 | # Visual Studio profiler
85 | *.psess
86 | *.vsp
87 | *.vspx
88 | *.sap
89 |
90 | # TFS 2012 Local Workspace
91 | $tf/
92 |
93 | # Guidance Automation Toolkit
94 | *.gpState
95 |
96 | # ReSharper is a .NET coding add-in
97 | _ReSharper*/
98 | *.[Rr]e[Ss]harper
99 | *.DotSettings.user
100 |
101 | # JustCode is a .NET coding add-in
102 | .JustCode
103 |
104 | # TeamCity is a build add-in
105 | _TeamCity*
106 |
107 | # DotCover is a Code Coverage Tool
108 | *.dotCover
109 |
110 | # NCrunch
111 | _NCrunch_*
112 | .*crunch*.local.xml
113 | nCrunchTemp_*
114 |
115 | # MightyMoose
116 | *.mm.*
117 | AutoTest.Net/
118 |
119 | # Web workbench (sass)
120 | .sass-cache/
121 |
122 | # Installshield output folder
123 | [Ee]xpress/
124 |
125 | # DocProject is a documentation generator add-in
126 | DocProject/buildhelp/
127 | DocProject/Help/*.HxT
128 | DocProject/Help/*.HxC
129 | DocProject/Help/*.hhc
130 | DocProject/Help/*.hhk
131 | DocProject/Help/*.hhp
132 | DocProject/Help/Html2
133 | DocProject/Help/html
134 |
135 | # Click-Once directory
136 | publish/
137 |
138 | # Publish Web Output
139 | *.[Pp]ublish.xml
140 | *.azurePubxml
141 | # TODO: Comment the next line if you want to checkin your web deploy settings
142 | # but database connection strings (with potential passwords) will be unencrypted
143 | *.pubxml
144 | *.publishproj
145 |
146 | # NuGet Packages
147 | *.nupkg
148 | # The packages folder can be ignored because of Package Restore
149 | **/packages/*
150 | # except build/, which is used as an MSBuild target.
151 | !**/packages/build/
152 | # Uncomment if necessary however generally it will be regenerated when needed
153 | #!**/packages/repositories.config
154 | # NuGet v3's project.json files produces more ignoreable files
155 | *.nuget.props
156 | *.nuget.targets
157 |
158 | # Microsoft Azure Build Output
159 | csx/
160 | *.build.csdef
161 |
162 | # Microsoft Azure Emulator
163 | ecf/
164 | rcf/
165 |
166 | # Microsoft Azure ApplicationInsights config file
167 | ApplicationInsights.config
168 |
169 | # Windows Store app package directories and files
170 | AppPackages/
171 | BundleArtifacts/
172 | Package.StoreAssociation.xml
173 | _pkginfo.txt
174 |
175 | # Visual Studio cache files
176 | # files ending in .cache can be ignored
177 | *.[Cc]ache
178 | # but keep track of directories ending in .cache
179 | !*.[Cc]ache/
180 |
181 | # Others
182 | ClientBin/
183 | ~$*
184 | *~
185 | *.dbmdl
186 | *.dbproj.schemaview
187 | *.pfx
188 | *.publishsettings
189 | node_modules/
190 | orleans.codegen.cs
191 |
192 | # Since there are multiple workflows, uncomment next line to ignore bower_components
193 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
194 | #bower_components/
195 |
196 | # RIA/Silverlight projects
197 | Generated_Code/
198 |
199 | # Backup & report files from converting an old project file
200 | # to a newer Visual Studio version. Backup files are not needed,
201 | # because we have git ;-)
202 | _UpgradeReport_Files/
203 | Backup*/
204 | UpgradeLog*.XML
205 | UpgradeLog*.htm
206 |
207 | # SQL Server files
208 | *.mdf
209 | *.ldf
210 |
211 | # Business Intelligence projects
212 | *.rdl.data
213 | *.bim.layout
214 | *.bim_*.settings
215 |
216 | # Microsoft Fakes
217 | FakesAssemblies/
218 |
219 | # GhostDoc plugin setting file
220 | *.GhostDoc.xml
221 |
222 | # Node.js Tools for Visual Studio
223 | .ntvs_analysis.dat
224 |
225 | # Visual Studio 6 build log
226 | *.plg
227 |
228 | # Visual Studio 6 workspace options file
229 | *.opt
230 |
231 | # Visual Studio LightSwitch build output
232 | **/*.HTMLClient/GeneratedArtifacts
233 | **/*.DesktopClient/GeneratedArtifacts
234 | **/*.DesktopClient/ModelManifest.xml
235 | **/*.Server/GeneratedArtifacts
236 | **/*.Server/ModelManifest.xml
237 | _Pvt_Extensions
238 |
239 | # Paket dependency manager
240 | .paket/paket.exe
241 |
242 | # FAKE - F# Make
243 | .fake/
244 |
245 | # JetBrains Rider
246 | .idea/
247 | *.sln.iml
248 | /replay.server.battlelobby
249 | /packages
250 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "Heroes.ReplayParser"]
2 | path = Heroes.ReplayParser
3 | url = https://github.com/poma/Heroes.ReplayParser.git
4 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common.Test/Hotsapi.Uploader.Common.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common.Test/ManagerTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 |
6 | namespace Hotsapi.Uploader.Common.Test
7 | {
8 | [TestClass]
9 | public partial class ManagerTests
10 | {
11 | private Task ShortRandomDelay()
12 | {
13 | var r = new Random();
14 | var delay = r.Next(100, 200);
15 | return Task.Delay(delay);
16 | }
17 | private static IEnumerable ThreeInOrder
18 | {
19 | get {
20 | var one = new ReplayFile("one") {
21 | Created = new DateTime(2020, 1, 1, 0, 0, 1)
22 | };
23 | var two = new ReplayFile("two") {
24 | Created = new DateTime(2020, 1, 1, 0, 0, 10)
25 | };
26 | var three = new ReplayFile("three") {
27 | Created = new DateTime(2020, 1, 1, 0, 0, 20)
28 | };
29 | var initialFiles = new List() { one, two, three };
30 | return initialFiles;
31 | }
32 | }
33 |
34 | [TestMethod]
35 | [Ignore("Known intermittant failure: multiple uploads are started in parallel and don't always start in order")]
36 | public async Task InitialFilesStartInOrder()
37 | {
38 | var initialFiles = ThreeInOrder;
39 |
40 | var manager = new Manager(new MockStorage(initialFiles));
41 | var uploadTester = new MockUploader();
42 |
43 | var promise = new TaskCompletionSource();
44 | Task done = promise.Task;
45 |
46 | var uploadsSeen = 0;
47 | var l = new object();
48 | ReplayFile lastUploadStarted = null;
49 | uploadTester.SetUploadCallback(async rf => {
50 | if (lastUploadStarted != null) {
51 | try {
52 | Assert.IsTrue(rf.Created >= lastUploadStarted.Created, $"upload started out of order, {lastUploadStarted} started after {rf}");
53 | } catch (Exception e) {
54 | promise.TrySetException(e);
55 | }
56 | }
57 | lastUploadStarted = rf;
58 | await ShortRandomDelay();
59 | var isDone = false;
60 | lock (l) {
61 | uploadsSeen++;
62 | isDone = uploadsSeen >= 3;
63 | }
64 | if (isDone) {
65 | promise.TrySetResult(uploadsSeen);
66 | }
67 | });
68 |
69 | manager.Start(new NoNewFilesMonitor(), new MockAnalizer(), uploadTester);
70 | await done;
71 | }
72 |
73 |
74 |
75 | [TestMethod]
76 | [Ignore("Known intermittant failure: multiple uploads are started in parallel and don't always end in order")]
77 | public async Task InitialFilesEndInorder() {
78 | var initialFiles = ThreeInOrder;
79 |
80 | var manager = new Manager(new MockStorage(initialFiles));
81 | var uploadTester = new MockUploader();
82 | var promise = new TaskCompletionSource();
83 | Task done = promise.Task;
84 |
85 | var uploadsSeen = 0;
86 | var l = new object();
87 | ReplayFile lastUploadFinished = null;
88 | uploadTester.SetUploadCallback(async rf => {
89 | await ShortRandomDelay();
90 | if (lastUploadFinished != null) {
91 | try {
92 | Assert.IsTrue(rf.Created >= lastUploadFinished.Created, $"upload completed out of order, {lastUploadFinished} completed after {rf}");
93 | }
94 | catch (Exception e) {
95 | promise.TrySetException(e);
96 | }
97 | }
98 | lastUploadFinished = rf;
99 | var isDone = false;
100 | lock (l) {
101 | uploadsSeen++;
102 | isDone = uploadsSeen >= 3;
103 | }
104 | if (isDone) {
105 | promise.TrySetResult(uploadsSeen);
106 | }
107 | });
108 |
109 | manager.Start(new NoNewFilesMonitor(), new MockAnalizer(), uploadTester);
110 | await done;
111 | }
112 |
113 | [TestMethod]
114 | public async Task AllInitialFilesProcessed()
115 | {
116 | var initialFiles = ThreeInOrder;
117 |
118 | var manager = new Manager(new MockStorage(initialFiles));
119 | var uploadTester = new MockUploader();
120 | var done = new TaskCompletionSource();
121 |
122 | var uploadsSeen = 0;
123 | object l = new object();
124 | uploadTester.SetUploadCallback(async rf => {
125 | await ShortRandomDelay();
126 | lock (l) {
127 | uploadsSeen++;
128 | if (uploadsSeen >= 3) {
129 | done.SetResult(uploadsSeen);
130 | }
131 | }
132 | });
133 |
134 | manager.Start(new NoNewFilesMonitor(), new MockAnalizer(), uploadTester);
135 | var finished = await Task.WhenAny(Task.Delay(4000), done.Task);
136 | await finished;
137 | Assert.AreEqual(3, uploadsSeen);
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common.Test/MockAnalizer.cs:
--------------------------------------------------------------------------------
1 | using Heroes.ReplayParser;
2 |
3 | namespace Hotsapi.Uploader.Common.Test
4 | {
5 | public partial class ManagerTests
6 | {
7 | private class MockAnalizer : IAnalyzer
8 | {
9 | public int MinimumBuild { get; set; }
10 | public Replay Analyze(ReplayFile file) => new Replay();
11 | public string GetFingerprint(Replay replay) => "dummy fingerprint";
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common.Test/MockStorage.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Hotsapi.Uploader.Common.Test
4 | {
5 | public partial class ManagerTests
6 | {
7 | private class MockStorage : IReplayStorage
8 | {
9 | private IEnumerable InitialFiles { get; }
10 | public MockStorage(IEnumerable initialFiles) => InitialFiles = initialFiles;
11 | public IEnumerable Load() => InitialFiles;
12 | public void Save(IEnumerable files) { }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common.Test/MockUploader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace Hotsapi.Uploader.Common.Test
6 | {
7 | public partial class ManagerTests
8 | {
9 | private class MockUploader : IUploader
10 | {
11 | public bool UploadToHotslogs { get; set; }
12 |
13 | private Func UploadCallback = _ => Task.CompletedTask;
14 | public void SetUploadCallback(Func onUpload)
15 | {
16 | var old = UploadCallback;
17 |
18 | UploadCallback = async (ReplayFile file) => {
19 | await old(file);
20 | await onUpload(file);
21 | };
22 | }
23 |
24 | public Task CheckDuplicate(IEnumerable replays) => Task.CompletedTask;
25 | public Task GetMinimumBuild() => Task.FromResult(1);
26 | public Task Upload(ReplayFile file)
27 | {
28 | UploadCallback(file);
29 | return Task.CompletedTask;
30 | }
31 | public async Task Upload(string file)
32 | {
33 | await Task.Delay(100);
34 | return UploadStatus.Success;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common.Test/NoNewFilesMonitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Hotsapi.Uploader.Common.Test
6 | {
7 | public partial class ManagerTests
8 | {
9 | private class NoNewFilesMonitor : IMonitor
10 | {
11 | public event EventHandler> ReplayAdded;
12 |
13 | public IEnumerable ScanReplays() => Enumerable.Empty();
14 | public void Start() { }
15 | public void Stop() { }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/Analyzer.cs:
--------------------------------------------------------------------------------
1 | using Heroes.ReplayParser;
2 | using NLog;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Security.Cryptography;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace Hotsapi.Uploader.Common
11 | {
12 | public class Analyzer : IAnalyzer
13 | {
14 | public int MinimumBuild { get; set; }
15 |
16 | private static Logger _log = LogManager.GetCurrentClassLogger();
17 |
18 | ///
19 | /// Analyze replay locally before uploading
20 | ///
21 | /// Replay file
22 | public Replay Analyze(ReplayFile file)
23 | {
24 | try {
25 | var result = DataParser.ParseReplay(file.Filename, false, false, false, true);
26 | var replay = result.Item2;
27 | var parseResult = result.Item1;
28 | var status = GetPreStatus(replay, parseResult);
29 |
30 | if (status != null) {
31 | file.UploadStatus = status.Value;
32 | }
33 |
34 | if (parseResult != DataParser.ReplayParseResult.Success) {
35 | return null;
36 | }
37 |
38 | file.Fingerprint = GetFingerprint(replay);
39 | return replay;
40 | }
41 | catch (Exception e) {
42 | _log.Warn(e, $"Error analyzing file {file}");
43 | return null;
44 | }
45 | }
46 |
47 | public UploadStatus? GetPreStatus(Replay replay, DataParser.ReplayParseResult parseResult)
48 | {
49 | switch (parseResult) {
50 | case DataParser.ReplayParseResult.ComputerPlayerFound:
51 | case DataParser.ReplayParseResult.TryMeMode:
52 | return UploadStatus.AiDetected;
53 |
54 | case DataParser.ReplayParseResult.PTRRegion:
55 | return UploadStatus.PtrRegion;
56 |
57 | case DataParser.ReplayParseResult.PreAlphaWipe:
58 | return UploadStatus.TooOld;
59 | }
60 |
61 | if (parseResult != DataParser.ReplayParseResult.Success) {
62 | return null;
63 | }
64 |
65 | if (replay.GameMode == GameMode.Custom) {
66 | return UploadStatus.CustomGame;
67 | }
68 |
69 | if (replay.ReplayBuild < MinimumBuild) {
70 | return UploadStatus.TooOld;
71 | }
72 |
73 | return null;
74 | }
75 |
76 | ///
77 | /// Get unique hash of replay. Compatible with HotsLogs
78 | ///
79 | ///
80 | ///
81 | public string GetFingerprint(Replay replay)
82 | {
83 | var str = new StringBuilder();
84 | replay.Players.Select(p => p.BattleNetId).OrderBy(x => x).Map(x => str.Append(x.ToString()));
85 | str.Append(replay.RandomValue);
86 | var md5 = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(str.ToString()));
87 | var result = new Guid(md5);
88 | return result.ToString();
89 | }
90 |
91 | ///
92 | /// Swaps two bytes in a byte array
93 | ///
94 | private void SwapBytes(byte[] buf, int i, int j)
95 | {
96 | byte temp = buf[i];
97 | buf[i] = buf[j];
98 | buf[j] = temp;
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/DeleteFiles.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 |
4 | namespace Hotsapi.Uploader.Common
5 | {
6 | [Flags]
7 | public enum DeleteFiles
8 | {
9 | None = 0x00,
10 | PTR = 0x01,
11 | Ai = 0x02,
12 | Custom = 0x04,
13 | Brawl = 0x08,
14 | QuickMatch = 0x10,
15 | UnrankedDraft = 0x20,
16 | HeroLeague = 0x40,
17 | TeamLeague = 0x80,
18 | StormLeague = 0x100,
19 | }
20 | }
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/EventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Hotsapi.Uploader.Common
8 | {
9 | public class EventArgs : EventArgs
10 | {
11 | public T Data { get; private set; }
12 |
13 | public EventArgs(T input)
14 | {
15 | Data = input;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Hotsapi.Uploader.Common
8 | {
9 | public static class Extensions
10 | {
11 | ///
12 | /// Executes specified delegate on all members of the collection
13 | ///
14 | public static void Map(this IEnumerable src, Action action)
15 | {
16 | src.Select(q => { action(q); return 0; }).Count();
17 | }
18 |
19 | ///
20 | /// Does nothing. Avoids compiler warning about the lack of await
21 | ///
22 | public static void Forget(this Task task) { }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/Hotsapi.Uploader.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Debug;Release
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/IAnalyzer.cs:
--------------------------------------------------------------------------------
1 | using Heroes.ReplayParser;
2 |
3 | namespace Hotsapi.Uploader.Common
4 | {
5 | public interface IAnalyzer
6 | {
7 | int MinimumBuild { get; set; }
8 |
9 | Replay Analyze(ReplayFile file);
10 | string GetFingerprint(Replay replay);
11 | }
12 | }
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/IMonitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Hotsapi.Uploader.Common
5 | {
6 | public interface IMonitor
7 | {
8 | event EventHandler> ReplayAdded;
9 |
10 | IEnumerable ScanReplays();
11 | void Start();
12 | void Stop();
13 | }
14 | }
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/IReplayStorage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Hotsapi.Uploader.Common
6 | {
7 | public interface IReplayStorage
8 | {
9 | void Save(IEnumerable files);
10 | IEnumerable Load();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/IUploader.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 |
4 | namespace Hotsapi.Uploader.Common
5 | {
6 | public interface IUploader
7 | {
8 | bool UploadToHotslogs { get; set; }
9 | Task CheckDuplicate(IEnumerable replays);
10 | Task GetMinimumBuild();
11 | Task Upload(ReplayFile file);
12 | Task Upload(string file);
13 | }
14 | }
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/Manager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Collections.Specialized;
5 | using System.ComponentModel;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.IO;
10 | using System.Threading;
11 | using NLog;
12 | using Nito.AsyncEx;
13 | using System.Diagnostics;
14 | using Heroes.ReplayParser;
15 | using System.Collections.Concurrent;
16 |
17 | namespace Hotsapi.Uploader.Common
18 | {
19 | public class Manager : INotifyPropertyChanged
20 | {
21 | ///
22 | /// Upload thead count
23 | ///
24 | public const int MaxThreads = 4;
25 |
26 | ///
27 | /// Replay list
28 | ///
29 | public ObservableCollectionEx Files { get; private set; } = new ObservableCollectionEx();
30 |
31 | private static Logger _log = LogManager.GetCurrentClassLogger();
32 | private bool _initialized = false;
33 | private AsyncCollection processingQueue = new AsyncCollection(new ConcurrentStack());
34 | private readonly IReplayStorage _storage;
35 | private IUploader _uploader;
36 | private IAnalyzer _analyzer;
37 | private IMonitor _monitor;
38 |
39 | public event PropertyChangedEventHandler PropertyChanged;
40 |
41 | private string _status = "";
42 | ///
43 | /// Current uploader status
44 | ///
45 | public string Status
46 | {
47 | get {
48 | return _status;
49 | }
50 | }
51 |
52 | private Dictionary _aggregates = new Dictionary();
53 | ///
54 | /// List of aggregate upload stats
55 | ///
56 | public Dictionary Aggregates
57 | {
58 | get {
59 | return _aggregates;
60 | }
61 | }
62 |
63 | ///
64 | /// Whether to mark replays for upload to hotslogs
65 | ///
66 | public bool UploadToHotslogs
67 | {
68 | get {
69 | return _uploader?.UploadToHotslogs ?? false;
70 | }
71 | set {
72 | if (_uploader != null) {
73 | _uploader.UploadToHotslogs = value;
74 | }
75 | }
76 | }
77 |
78 | ///
79 | /// Which replays to delete after upload
80 | ///
81 | public DeleteFiles DeleteAfterUpload { get; set; }
82 |
83 | public Manager(IReplayStorage storage)
84 | {
85 | this._storage = storage;
86 | Files.ItemPropertyChanged += (_, __) => { RefreshStatusAndAggregates(); };
87 | Files.CollectionChanged += (_, __) => { RefreshStatusAndAggregates(); };
88 | }
89 |
90 | ///
91 | /// Start uploading and watching for new replays
92 | ///
93 | public async void Start(IMonitor monitor, IAnalyzer analyzer, IUploader uploader)
94 | {
95 | if (_initialized) {
96 | return;
97 | }
98 | _initialized = true;
99 |
100 | _uploader = uploader;
101 | _analyzer = analyzer;
102 | _monitor = monitor;
103 |
104 | var replays = ScanReplays();
105 | Files.AddRange(replays);
106 | replays.Where(x => x.UploadStatus == UploadStatus.None).Reverse().Map(x => processingQueue.Add(x));
107 |
108 | _monitor.ReplayAdded += async (_, e) => {
109 | await EnsureFileAvailable(e.Data, 3000);
110 | var replay = new ReplayFile(e.Data);
111 | Files.Insert(0, replay);
112 | processingQueue.Add(replay);
113 | };
114 | _monitor.Start();
115 |
116 | _analyzer.MinimumBuild = await _uploader.GetMinimumBuild();
117 |
118 | for (int i = 0; i < MaxThreads; i++) {
119 | Task.Run(UploadLoop).Forget();
120 | }
121 | }
122 |
123 | public void Stop()
124 | {
125 | _monitor.Stop();
126 | processingQueue.CompleteAdding();
127 | }
128 |
129 | private async Task UploadLoop()
130 | {
131 | while (await processingQueue.OutputAvailableAsync()) {
132 | try {
133 | var file = await processingQueue.TakeAsync();
134 |
135 | file.UploadStatus = UploadStatus.InProgress;
136 |
137 | // test if replay is eligible for upload (not AI, PTR, Custom, etc)
138 | var replay = _analyzer.Analyze(file);
139 | if (file.UploadStatus == UploadStatus.InProgress) {
140 | // if it is, upload it
141 | await _uploader.Upload(file);
142 | }
143 | SaveReplayList();
144 | if (ShouldDelete(file, replay)) {
145 | DeleteReplay(file);
146 | }
147 | }
148 | catch (Exception ex) {
149 | _log.Error(ex, "Error in upload loop");
150 | }
151 | }
152 | }
153 |
154 | private void RefreshStatusAndAggregates()
155 | {
156 | _status = Files.Any(x => x.UploadStatus == UploadStatus.InProgress) ? "Uploading..." : "Idle";
157 | _aggregates = Files.GroupBy(x => x.UploadStatus).ToDictionary(x => x.Key, x => x.Count());
158 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Status)));
159 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Aggregates)));
160 | }
161 |
162 | private void SaveReplayList()
163 | {
164 | try {
165 | // save only replays with fixed status. Will retry failed ones on next launch.
166 | var ignored = new[] { UploadStatus.None, UploadStatus.UploadError, UploadStatus.InProgress };
167 | _storage.Save(Files.Where(x => !ignored.Contains(x.UploadStatus)));
168 | }
169 | catch (Exception ex) {
170 | _log.Error(ex, "Error saving replay list");
171 | }
172 | }
173 |
174 | ///
175 | /// Load replay cache and merge it with folder scan results
176 | ///
177 | private List ScanReplays()
178 | {
179 | var replays = new List(_storage.Load());
180 | var lookup = new HashSet(replays);
181 | var comparer = new ReplayFile.ReplayFileComparer();
182 | replays.AddRange(_monitor.ScanReplays().Select(x => new ReplayFile(x)).Where(x => !lookup.Contains(x, comparer)));
183 | return replays.OrderByDescending(x => x.Created).ToList();
184 | }
185 |
186 | ///
187 | /// Delete replay file
188 | ///
189 | private static void DeleteReplay(ReplayFile file)
190 | {
191 | try {
192 | _log.Info($"Deleting replay {file}");
193 | file.Deleted = true;
194 | File.Delete(file.Filename);
195 | }
196 | catch (Exception ex) {
197 | _log.Error(ex, "Error deleting file");
198 | }
199 | }
200 |
201 | ///
202 | /// Ensure that HotS client finished writing replay file and it can be safely open
203 | ///
204 | /// Filename to test
205 | /// Timeout in milliseconds
206 | /// Whether to test read or write access
207 | public async Task EnsureFileAvailable(string filename, int timeout, bool testWrite = true)
208 | {
209 | var timer = new Stopwatch();
210 | timer.Start();
211 | while (timer.ElapsedMilliseconds < timeout) {
212 | try {
213 | if (testWrite) {
214 | File.OpenWrite(filename).Close();
215 | } else {
216 | File.OpenRead(filename).Close();
217 | }
218 | return;
219 | }
220 | catch (IOException) {
221 | // File is still in use
222 | await Task.Delay(100);
223 | }
224 | catch {
225 | return;
226 | }
227 | }
228 | }
229 |
230 | ///
231 | /// Decide whether a replay should be deleted according to current settings
232 | ///
233 | /// replay file metadata
234 | /// Parsed replay
235 | private bool ShouldDelete(ReplayFile file, Replay replay)
236 | {
237 | return
238 | DeleteAfterUpload.HasFlag(DeleteFiles.PTR) && file.UploadStatus == UploadStatus.PtrRegion ||
239 | DeleteAfterUpload.HasFlag(DeleteFiles.Ai) && file.UploadStatus == UploadStatus.AiDetected ||
240 | DeleteAfterUpload.HasFlag(DeleteFiles.Custom) && file.UploadStatus == UploadStatus.CustomGame ||
241 | file.UploadStatus == UploadStatus.Success && (
242 | DeleteAfterUpload.HasFlag(DeleteFiles.Brawl) && replay.GameMode == GameMode.Brawl ||
243 | DeleteAfterUpload.HasFlag(DeleteFiles.QuickMatch) && replay.GameMode == GameMode.QuickMatch ||
244 | DeleteAfterUpload.HasFlag(DeleteFiles.UnrankedDraft) && replay.GameMode == GameMode.UnrankedDraft ||
245 | DeleteAfterUpload.HasFlag(DeleteFiles.HeroLeague) && replay.GameMode == GameMode.HeroLeague ||
246 | DeleteAfterUpload.HasFlag(DeleteFiles.TeamLeague) && replay.GameMode == GameMode.TeamLeague ||
247 | DeleteAfterUpload.HasFlag(DeleteFiles.StormLeague) && replay.GameMode == GameMode.StormLeague
248 | );
249 | }
250 | }
251 | }
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/Monitor.cs:
--------------------------------------------------------------------------------
1 | using NLog;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace Hotsapi.Uploader.Common
10 | {
11 | public class Monitor : IMonitor
12 | {
13 | private static Logger _log = LogManager.GetCurrentClassLogger();
14 | protected readonly string ProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"Heroes of the Storm\Accounts");
15 | protected FileSystemWatcher _watcher;
16 |
17 | ///
18 | /// Fires when a new replay file is found
19 | ///
20 | public event EventHandler> ReplayAdded;
21 | protected virtual void OnReplayAdded(string path)
22 | {
23 | _log.Debug($"Detected new replay: {path}");
24 | ReplayAdded?.Invoke(this, new EventArgs(path));
25 | }
26 |
27 | ///
28 | /// Starts watching filesystem for new replays. When found raises event.
29 | ///
30 | public void Start()
31 | {
32 | if (_watcher == null) {
33 | _watcher = new FileSystemWatcher() {
34 | Path = ProfilePath,
35 | Filter = "*.StormReplay",
36 | IncludeSubdirectories = true
37 | };
38 | _watcher.Created += (o, e) => OnReplayAdded(e.FullPath);
39 | }
40 | _watcher.EnableRaisingEvents = true;
41 | _log.Debug($"Started watching for new replays");
42 | }
43 |
44 | ///
45 | /// Stops watching filesystem for new replays
46 | ///
47 | public void Stop()
48 | {
49 | if (_watcher != null) {
50 | _watcher.EnableRaisingEvents = false;
51 | }
52 | _log.Debug($"Stopped watching for new replays");
53 | }
54 |
55 | ///
56 | /// Finds all available replay files
57 | ///
58 | public IEnumerable ScanReplays()
59 | {
60 | return Directory.GetFiles(ProfilePath, "*.StormReplay", SearchOption.AllDirectories);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/ObservableCollectionEx.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Collections.Specialized;
5 | using System.ComponentModel;
6 | using System.Linq;
7 |
8 | namespace Hotsapi.Uploader.Common
9 | {
10 | ///
11 | /// An ObservableCollection that notifies of item property changes
12 | ///
13 | public class ObservableCollectionEx : ObservableCollection where T : INotifyPropertyChanged
14 | {
15 | public event PropertyChangedEventHandler ItemPropertyChanged;
16 | private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
17 | {
18 | ItemPropertyChanged?.Invoke(sender, e);
19 | }
20 |
21 | protected override void InsertItem(int index, T item)
22 | {
23 | base.InsertItem(index, item);
24 | item.PropertyChanged += Item_PropertyChanged;
25 | }
26 |
27 | protected override void RemoveItem(int index)
28 | {
29 | Items[index].PropertyChanged -= Item_PropertyChanged;
30 | base.RemoveItem(index);
31 | }
32 |
33 | protected override void ClearItems()
34 | {
35 | foreach (T item in Items) {
36 | item.PropertyChanged -= Item_PropertyChanged;
37 | }
38 | base.ClearItems();
39 | }
40 |
41 | protected override void SetItem(int index, T item)
42 | {
43 | T oldItem = Items[index];
44 | T newItem = item;
45 |
46 | oldItem.PropertyChanged -= Item_PropertyChanged;
47 | newItem.PropertyChanged += Item_PropertyChanged;
48 |
49 | base.SetItem(index, item);
50 | }
51 |
52 | public void AddRange(IEnumerable items)
53 | {
54 | if (!items.Any()) {
55 | return;
56 | }
57 |
58 | this.CheckReentrancy();
59 | foreach (var item in items) {
60 | this.Items.Add(item);
61 | item.PropertyChanged += Item_PropertyChanged;
62 | }
63 | this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/ReplayFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Xml.Serialization;
7 |
8 | namespace Hotsapi.Uploader.Common
9 | {
10 | [Serializable]
11 | public class ReplayFile : INotifyPropertyChanged
12 | {
13 | [XmlIgnore]
14 | public string Fingerprint { get; set; }
15 | public string Filename { get; set; }
16 | public DateTime Created { get; set; }
17 |
18 | private bool _deleted;
19 | public bool Deleted
20 | {
21 | get {
22 | return _deleted;
23 | }
24 | set {
25 | if (_deleted == value) {
26 | return;
27 | }
28 |
29 | _deleted = value;
30 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Deleted)));
31 | }
32 | }
33 |
34 | UploadStatus _uploadStatus = UploadStatus.None;
35 | public UploadStatus UploadStatus
36 | {
37 | get {
38 | return _uploadStatus;
39 | }
40 | set {
41 | if (_uploadStatus == value) {
42 | return;
43 | }
44 |
45 | _uploadStatus = value;
46 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UploadStatus)));
47 | }
48 | }
49 |
50 | public ReplayFile() { } // Required for serialization
51 |
52 | public ReplayFile(string filename)
53 | {
54 | Filename = filename;
55 | Created = File.GetCreationTime(filename);
56 | }
57 |
58 | public override string ToString()
59 | {
60 | return Filename;
61 | }
62 |
63 | public event PropertyChangedEventHandler PropertyChanged;
64 |
65 | public class ReplayFileComparer : IEqualityComparer
66 | {
67 | public bool Equals(ReplayFile x, ReplayFile y)
68 | {
69 | return x.Filename == y.Filename && x.Created == y.Created;
70 | }
71 |
72 | public int GetHashCode(ReplayFile obj)
73 | {
74 | return obj.Filename.GetHashCode() ^ obj.Created.GetHashCode();
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/ReplayStorage.cs:
--------------------------------------------------------------------------------
1 | using Hotsapi.Uploader.Common;
2 | using NLog;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Reflection;
8 | using System.Xml;
9 | using System.Xml.Serialization;
10 |
11 | namespace Hotsapi.Uploader.Common
12 | {
13 | public class ReplayStorage : IReplayStorage
14 | {
15 | private static Logger _log = LogManager.GetCurrentClassLogger();
16 | private readonly string _filename;
17 | private readonly object _lock = new object();
18 |
19 | public ReplayStorage(string filename)
20 | {
21 | _filename = filename;
22 | }
23 |
24 | public IEnumerable Load()
25 | {
26 | if (!File.Exists(_filename)) {
27 | return new ReplayFile[0];
28 | }
29 |
30 | try {
31 | using (var f = File.OpenRead(_filename)) {
32 | var serializer = new XmlSerializer(typeof(ReplayFile[]));
33 | return (ReplayFile[])serializer.Deserialize(f);
34 | }
35 | }
36 | catch (Exception ex) {
37 | _log.Error(ex, "Error loading replay upload data");
38 | return new ReplayFile[0];
39 | }
40 | }
41 |
42 | public void Save(IEnumerable files)
43 | {
44 | try {
45 | lock (_lock) {
46 | using (var stream = new MemoryStream()) {
47 | var data = files.ToArray();
48 | var serializer = new XmlSerializer(data.GetType());
49 | serializer.Serialize(stream, data);
50 | File.WriteAllBytes(_filename, stream.ToArray());
51 | }
52 | }
53 | }
54 | catch (Exception ex) {
55 | _log.Error(ex, "Error saving replay upload data");
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/UploadStatus.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Hotsapi.Uploader.Common
6 | {
7 | public enum UploadStatus
8 | {
9 | None,
10 | Success,
11 | InProgress,
12 | UploadError,
13 | Duplicate,
14 | AiDetected,
15 | CustomGame,
16 | PtrRegion,
17 | Incomplete,
18 | TooOld,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/Uploader.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 | using NLog;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Net;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace Hotsapi.Uploader.Common
12 | {
13 | public class Uploader : IUploader
14 | {
15 | private static readonly Logger _log = LogManager.GetCurrentClassLogger();
16 | #if DEBUG
17 | const string ApiEndpoint = "http://hotsapi.local/api/v1";
18 | #else
19 | const string ApiEndpoint = "https://hotsapi.net/api/v1";
20 | #endif
21 |
22 | public bool UploadToHotslogs { get; set; }
23 |
24 | ///
25 | /// New instance of replay uploader
26 | ///
27 | public Uploader()
28 | {
29 |
30 | }
31 |
32 | ///
33 | /// Upload replay
34 | ///
35 | ///
36 | public async Task Upload(ReplayFile file)
37 | {
38 | file.UploadStatus = UploadStatus.InProgress;
39 | if (file.Fingerprint != null && await CheckDuplicate(file.Fingerprint)) {
40 | _log.Debug($"File {file} marked as duplicate");
41 | file.UploadStatus = UploadStatus.Duplicate;
42 | } else {
43 | file.UploadStatus = await Upload(file.Filename);
44 | }
45 | }
46 |
47 | ///
48 | /// Upload replay
49 | ///
50 | /// Path to file
51 | /// Upload result
52 | public async Task Upload(string file)
53 | {
54 | try {
55 | string response;
56 | using (var client = new WebClient()) {
57 | var bytes = await client.UploadFileTaskAsync($"{ApiEndpoint}/upload?uploadToHotslogs={UploadToHotslogs}", file);
58 | response = Encoding.UTF8.GetString(bytes);
59 | }
60 | dynamic json = JObject.Parse(response);
61 | if ((bool)json.success) {
62 | if (Enum.TryParse((string)json.status, out UploadStatus status)) {
63 | _log.Debug($"Uploaded file '{file}': {status}");
64 | return status;
65 | } else {
66 | _log.Error($"Unknown upload status '{file}': {json.status}");
67 | return UploadStatus.UploadError;
68 | }
69 | } else {
70 | _log.Warn($"Error uploading file '{file}': {response}");
71 | return UploadStatus.UploadError;
72 | }
73 | }
74 | catch (WebException ex) {
75 | if (await CheckApiThrottling(ex.Response)) {
76 | return await Upload(file);
77 | }
78 | _log.Warn(ex, $"Error uploading file '{file}'");
79 | return UploadStatus.UploadError;
80 | }
81 | }
82 |
83 | ///
84 | /// Check replay fingerprint against database to detect duplicate
85 | ///
86 | ///
87 | private async Task CheckDuplicate(string fingerprint)
88 | {
89 | try {
90 | string response;
91 | using (var client = new WebClient()) {
92 | response = await client.DownloadStringTaskAsync($"{ApiEndpoint}/replays/fingerprints/v3/{fingerprint}?uploadToHotslogs={UploadToHotslogs}");
93 | }
94 | dynamic json = JObject.Parse(response);
95 | return (bool)json.exists;
96 | }
97 | catch (WebException ex) {
98 | if (await CheckApiThrottling(ex.Response)) {
99 | return await CheckDuplicate(fingerprint);
100 | }
101 | _log.Warn(ex, $"Error checking fingerprint '{fingerprint}'");
102 | return false;
103 | }
104 | }
105 |
106 | ///
107 | /// Mass check replay fingerprints against database to detect duplicates
108 | ///
109 | ///
110 | private async Task CheckDuplicate(IEnumerable fingerprints)
111 | {
112 | try {
113 | string response;
114 | using (var client = new WebClient()) {
115 | response = await client.UploadStringTaskAsync($"{ApiEndpoint}/replays/fingerprints?uploadToHotslogs={UploadToHotslogs}", String.Join("\n", fingerprints));
116 | }
117 | dynamic json = JObject.Parse(response);
118 | return (json.exists as JArray).Select(x => x.ToString()).ToArray();
119 | }
120 | catch (WebException ex) {
121 | if (await CheckApiThrottling(ex.Response)) {
122 | return await CheckDuplicate(fingerprints);
123 | }
124 | _log.Warn(ex, $"Error checking fingerprint array");
125 | return Array.Empty();
126 | }
127 | }
128 |
129 | ///
130 | /// Mass check replay fingerprints against database to detect duplicates
131 | ///
132 | public async Task CheckDuplicate(IEnumerable replays)
133 | {
134 | var exists = new HashSet(await CheckDuplicate(replays.Select(x => x.Fingerprint)));
135 | replays.Where(x => exists.Contains(x.Fingerprint)).Map(x => x.UploadStatus = UploadStatus.Duplicate);
136 | }
137 |
138 | ///
139 | /// Get minimum HotS client build supported by HotsApi
140 | ///
141 | public async Task GetMinimumBuild()
142 | {
143 | try {
144 | using (var client = new WebClient()) {
145 | var response = await client.DownloadStringTaskAsync($"{ApiEndpoint}/replays/min-build");
146 | if (!int.TryParse(response, out int build)) {
147 | _log.Warn($"Error parsing minimum build: {response}");
148 | return 0;
149 | }
150 | return build;
151 | }
152 | }
153 | catch (WebException ex) {
154 | if (await CheckApiThrottling(ex.Response)) {
155 | return await GetMinimumBuild();
156 | }
157 | _log.Warn(ex, $"Error getting minimum build");
158 | return 0;
159 | }
160 | }
161 |
162 | ///
163 | /// Check if Hotsapi request limit is reached and wait if it is
164 | ///
165 | /// Server response to examine
166 | private async Task CheckApiThrottling(WebResponse response)
167 | {
168 | if (response != null && (int)(response as HttpWebResponse).StatusCode == 429) {
169 | _log.Warn($"Too many requests, waiting");
170 | await Task.Delay(10000);
171 | return true;
172 | } else {
173 | return false;
174 | }
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Common/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | True
16 |
17 |
18 | True
19 |
20 |
21 | https://github.com/poma/Hotsapi.Uploader
22 |
23 |
24 | 400
25 |
26 |
27 | 400
28 |
29 |
30 | False
31 |
32 |
33 | 600
34 |
35 |
36 | 700
37 |
38 |
39 | False
40 |
41 |
42 | None
43 |
44 |
45 | MetroDark
46 |
47 |
48 | False
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/App.xaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using Hotsapi.Uploader.Common;
2 | using Microsoft.Win32;
3 | using NLog;
4 | using Squirrel;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.ComponentModel;
8 | using System.Configuration;
9 | using System.Data;
10 | using System.IO;
11 | using System.Linq;
12 | using System.Reflection;
13 | using System.Threading.Tasks;
14 | using System.Windows;
15 | using System.Windows.Data;
16 | using System.Windows.Forms;
17 | using System.Windows.Threading;
18 | using Application = System.Windows.Application;
19 | using MessageBox = System.Windows.MessageBox;
20 |
21 | namespace Hotsapi.Uploader.Windows
22 | {
23 | public partial class App : Application, INotifyPropertyChanged
24 | {
25 | #if DEBUG
26 | public const bool Debug = true;
27 | #else
28 | public const bool Debug = false;
29 | #endif
30 |
31 | #if NOSQUIRREL
32 | public const bool NoSquirrel = true;
33 | #else
34 | public const bool NoSquirrel = false;
35 | #endif
36 | // Don't want to write converters, using this quick hack instead
37 | public static bool StartWithWindowsCheckboxEnabled => !NoSquirrel;
38 |
39 | public event PropertyChangedEventHandler PropertyChanged;
40 |
41 | public NotifyIcon TrayIcon { get; private set; }
42 | public Manager Manager { get; private set; }
43 | public static Properties.Settings Settings { get { return Hotsapi.Uploader.Windows.Properties.Settings.Default; } }
44 | public static string AppExe { get { return Assembly.GetExecutingAssembly().Location; } }
45 | public static string AppDir { get { return Path.GetDirectoryName(AppExe); } }
46 | public static string AppFile { get { return Path.GetFileName(AppExe); } }
47 | public static string SettingsDir { get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Hotsapi"); } }
48 | public static UpdateManager DummyUpdateManager => new UpdateManager(@"not needed here");
49 | public bool UpdateAvailable
50 | {
51 | get {
52 | return _updateAvailable;
53 | }
54 | set {
55 | if (_updateAvailable == value) {
56 | return;
57 | }
58 | _updateAvailable = value;
59 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UpdateAvailable)));
60 | }
61 | }
62 | public static Version Version { get { return Assembly.GetExecutingAssembly().GetName().Version; } }
63 | public string VersionString
64 | {
65 | get {
66 | return $"v{Version.Major}.{Version.Minor}" + (Version.Build == 0 ? "" : $".{Version.Build}");
67 | }
68 | }
69 | public bool StartWithWindows
70 | {
71 | get {
72 | // todo: find a way to get shortcut name from UpdateManager instead of hardcoding it
73 | return File.Exists(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + @"\Hotsapi Uploader.lnk");
74 | }
75 | set {
76 | if (value) {
77 | DummyUpdateManager.CreateShortcutsForExecutable(AppFile, ShortcutLocation.Startup, false, "--autorun");
78 | } else {
79 | DummyUpdateManager.RemoveShortcutsForExecutable(AppFile, ShortcutLocation.Startup);
80 | }
81 | }
82 | }
83 |
84 | public readonly Dictionary Themes = new Dictionary {
85 | { "Default", null },
86 | { "MetroDark", "Themes/MetroDark/MetroDark.Hotsapi.Implicit.xaml" },
87 | };
88 |
89 | private static Logger _log = LogManager.GetCurrentClassLogger();
90 | private UpdateManager _updateManager;
91 | private bool _updateAvailable;
92 | private object _lock = new object();
93 | public MainWindow mainWindow;
94 |
95 |
96 | private void Application_Startup(object sender, StartupEventArgs e)
97 | {
98 | ShutdownMode = ShutdownMode.OnExplicitShutdown;
99 | SetExceptionHandlers();
100 | _log.Info($"App {VersionString} started");
101 | if (Settings.UpgradeRequired) {
102 | RestoreSettings();
103 | }
104 | SetupTrayIcon();
105 | Manager = new Manager(new ReplayStorage($@"{SettingsDir}\replays.xml"));
106 | // Enable collection modification from any thread
107 | BindingOperations.EnableCollectionSynchronization(Manager.Files, _lock);
108 |
109 | Manager.UploadToHotslogs = Settings.UploadToHotslogs;
110 | Manager.DeleteAfterUpload = Settings.DeleteAfterUpload;
111 | ApplyTheme(Settings.Theme);
112 | Settings.PropertyChanged += (o, ev) => {
113 | if (ev.PropertyName == nameof(Settings.UploadToHotslogs)) {
114 | Manager.UploadToHotslogs = Settings.UploadToHotslogs;
115 | }
116 | if (ev.PropertyName == nameof(Settings.DeleteAfterUpload)) {
117 | Manager.DeleteAfterUpload = Settings.DeleteAfterUpload;
118 | }
119 | if (ev.PropertyName == nameof(Settings.Theme)) {
120 | ApplyTheme(Settings.Theme);
121 | }
122 | };
123 |
124 | if (e.Args.Contains("--autorun") && Settings.MinimizeToTray) {
125 | TrayIcon.Visible = true;
126 | } else {
127 | mainWindow = new MainWindow();
128 | mainWindow.Show();
129 | }
130 | Manager.Start(new Monitor(), new Analyzer(), new Common.Uploader());
131 |
132 | if (!NoSquirrel) {
133 | //Check for updates on startup and then every hour
134 | CheckForUpdates();
135 | new DispatcherTimer() {
136 | Interval = TimeSpan.FromHours(1),
137 | IsEnabled = true
138 | }.Tick += (_, __) => CheckForUpdates();
139 | }
140 | }
141 |
142 | private void Application_Exit(object sender, ExitEventArgs e)
143 | {
144 | BackupSettings();
145 | TrayIcon?.Dispose();
146 | if (!NoSquirrel) {
147 | _updateManager?.Dispose();
148 | }
149 | }
150 |
151 | public void ApplyTheme(string theme)
152 | {
153 | // we will need a separate resource dictionary for themes
154 | // if we intend to store someting else in App resource dictionary
155 | Resources.MergedDictionaries.Clear();
156 | Themes.TryGetValue(theme, out string resource);
157 | if (resource != null) {
158 | Resources.MergedDictionaries.Add(new ResourceDictionary() { Source = new Uri(resource, UriKind.Relative) });
159 | } else {
160 | Resources.MergedDictionaries.Add(new ResourceDictionary() { Source = new Uri("Themes/Default/Default.xaml", UriKind.Relative) });
161 | }
162 | }
163 |
164 | public void Activate()
165 | {
166 | if (mainWindow != null) {
167 | if (mainWindow.WindowState == WindowState.Minimized) {
168 | mainWindow.WindowState = WindowState.Normal;
169 | }
170 | mainWindow.Activate();
171 | } else {
172 | mainWindow = new MainWindow();
173 | mainWindow.Show();
174 | mainWindow.WindowState = WindowState.Normal;
175 | TrayIcon.Visible = false;
176 | }
177 | }
178 |
179 | private void SetupTrayIcon()
180 | {
181 | TrayIcon = new NotifyIcon {
182 | Icon = System.Drawing.Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location),
183 | Visible = false
184 | };
185 | TrayIcon.Click += (o, e) => {
186 | mainWindow = new MainWindow();
187 | mainWindow.Show();
188 | TrayIcon.Visible = false;
189 | };
190 | }
191 |
192 | private async void CheckForUpdates()
193 | {
194 | if (Debug || !Settings.AutoUpdate) {
195 | return;
196 | }
197 | try {
198 | if (_updateManager == null) {
199 | _updateManager = await UpdateManager.GitHubUpdateManager(Settings.UpdateRepository, prerelease: Settings.AllowPreReleases);
200 | }
201 | var release = await _updateManager.UpdateApp();
202 | if (release != null) {
203 | _log.Info($"Updating app to version {release.Version}");
204 | UpdateAvailable = true;
205 | BackupSettings();
206 | }
207 | }
208 | catch (Exception e) {
209 | _log.Warn(e, "Error checking for updates");
210 | }
211 | }
212 |
213 | ///
214 | /// Make a backup of our settings.
215 | /// Used to persist settings across updates.
216 | ///
217 | public static void BackupSettings()
218 | {
219 | Settings.Save();
220 | string settingsFile = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
221 | string destination = $@"{SettingsDir}\last.config";
222 | File.Copy(settingsFile, destination, true);
223 | }
224 |
225 | ///
226 | /// Restore our settings backup if any.
227 | /// Used to persist settings across updates and upgrade settings format.
228 | ///
229 | public static void RestoreSettings()
230 | {
231 | string destFile = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
232 | string sourceFile = $@"{SettingsDir}\last.config";
233 |
234 | if (File.Exists(sourceFile)) {
235 | try {
236 | Directory.CreateDirectory(Path.GetDirectoryName(destFile)); // Create directory if needed
237 | File.Copy(sourceFile, destFile, true);
238 | Settings.Reload();
239 | Settings.Upgrade();
240 |
241 | if (string.IsNullOrEmpty(Settings.ApplicationVersion)) { // < v1.7
242 | /* Apparently even reading "start with windows" setting from the previous version triggers antivirus software
243 | * Commenting this out
244 | *
245 | * Now "start with windows" setting will be lost and switched to "off" when upgrading from earlier versions
246 | *
247 | * var reg = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion").OpenSubKey(@"Run");
248 | if (reg.OpenSubKey(@"Run").GetValue("Hotsapi") != null) {
249 | reg.OpenSubKey(@"Run", true).DeleteValue("Hotsapi", false);
250 |
251 | DummyUpdateManager.CreateShortcutsForExecutable(App.AppFile, ShortcutLocation.Startup, false, "--autorun");
252 | }
253 | */
254 | } else {
255 | var previous = Version.Parse(Settings.ApplicationVersion);
256 |
257 | // custom upgrade code
258 | }
259 | }
260 | catch (Exception e) {
261 | _log.Error(e, "Error upgrading settings");
262 | }
263 | }
264 |
265 | Settings.ApplicationVersion = Version.ToString();
266 | Settings.UpgradeRequired = false;
267 | Settings.Save();
268 | }
269 |
270 | ///
271 | /// Log all unhandled exceptions
272 | ///
273 | private void SetExceptionHandlers()
274 | {
275 | DispatcherUnhandledException += (o, e) => LogAndDisplay(e.Exception, "dispatcher");
276 | TaskScheduler.UnobservedTaskException += (o, e) => LogAndDisplay(e.Exception, "task");
277 | AppDomain.CurrentDomain.UnhandledException += (o, e) => LogAndDisplay(e.ExceptionObject as Exception, "domain");
278 | }
279 |
280 | private void LogAndDisplay(Exception e, string type)
281 | {
282 | _log.Error(e, $"Unhandled {type} exception");
283 | try {
284 | MessageBox.Show(e.ToString(), $"Unhandled {type} exception");
285 | }
286 | catch { /* probably not gui thread */ }
287 | }
288 | }
289 | }
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Hotsapi.Uploader.Windows.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {F774F86B-410F-410D-9719-ADF892D315D5}
8 | WinExe
9 | Hotsapi.Uploader.Windows
10 | Hotsapi.Uploader
11 | v4.6.2
12 | 512
13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
14 | 4
15 | true
16 |
17 |
18 |
19 | AnyCPU
20 | true
21 | full
22 | false
23 | bin\Debug\
24 | TRACE;DEBUG;NOSQUIRREL
25 | prompt
26 | 4
27 |
28 |
29 | AnyCPU
30 | pdbonly
31 | true
32 | bin\Release\
33 | TRACE;NOSQUIRREL
34 | prompt
35 | 4
36 |
37 |
38 | Resources\uploader_icon_light.ico
39 |
40 |
41 | bin\Zip\
42 | TRACE;NOSQUIRREL
43 | true
44 | pdbonly
45 | AnyCPU
46 | prompt
47 | MinimumRecommendedRules.ruleset
48 | true
49 |
50 |
51 | bin\Installer\
52 | TRACE
53 | true
54 | pdbonly
55 | AnyCPU
56 | prompt
57 | MinimumRecommendedRules.ruleset
58 | true
59 |
60 |
61 |
62 |
63 |
64 | 1.5.0.235
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 4.0
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | MSBuild:Compile
98 | Designer
99 |
100 |
101 |
102 | SettingsWindow.xaml
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | MSBuild:Compile
113 | Designer
114 |
115 |
116 | App.xaml
117 | Code
118 |
119 |
120 | MainWindow.xaml
121 | Code
122 |
123 |
124 | Designer
125 | MSBuild:Compile
126 |
127 |
128 | Designer
129 | MSBuild:Compile
130 |
131 |
132 | Designer
133 | MSBuild:Compile
134 |
135 |
136 | MSBuild:Compile
137 | Designer
138 |
139 |
140 | MSBuild:Compile
141 | Designer
142 |
143 |
144 | MSBuild:Compile
145 | Designer
146 |
147 |
148 | MSBuild:Compile
149 | Designer
150 |
151 |
152 |
153 |
154 | Code
155 |
156 |
157 | True
158 | True
159 | Resources.resx
160 |
161 |
162 | True
163 | Settings.settings
164 | True
165 |
166 |
167 |
168 |
169 |
170 |
171 | ResXFileCodeGenerator
172 | Resources.Designer.cs
173 |
174 |
175 | Always
176 | Designer
177 |
178 |
179 | Designer
180 |
181 |
182 | Designer
183 |
184 |
185 | PublicSettingsSingleFileGenerator
186 | Settings.Designer.cs
187 |
188 |
189 |
190 |
191 | Designer
192 |
193 |
194 |
195 |
196 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}
197 | Hotsapi.Uploader.Common
198 |
199 |
200 |
201 |
202 |
203 |
204 | False
205 |
206 |
207 | False
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Hotsapi.Uploader.nuspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hotsapi.Uploader
5 | 0.0.0.0
6 | Hotsapi.Uploader
7 | Poma
8 | Uploads Heroes of the Storm replays
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ❗ An update is downloaded and will be installed when you restart the uploader. Restart now.
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using Squirrel;
2 | using System;
3 | using System.Diagnostics;
4 | using System.Reflection;
5 | using System.Windows;
6 |
7 | namespace Hotsapi.Uploader.Windows
8 | {
9 | public partial class MainWindow : Window
10 | {
11 | public App App { get { return Application.Current as App; } }
12 | private bool ShutdownOnClose = true;
13 |
14 | public MainWindow()
15 | {
16 | InitializeComponent();
17 | }
18 |
19 | private void Window_StateChanged(object sender, EventArgs e)
20 | {
21 | if (App.Settings.MinimizeToTray && WindowState == WindowState.Minimized) {
22 | App.TrayIcon.Visible = true;
23 | ShutdownOnClose = false;
24 | Close();
25 | }
26 | }
27 |
28 | private void Window_Closed(object sender, EventArgs e)
29 | {
30 | App.mainWindow = null;
31 | if (ShutdownOnClose) {
32 | App.Shutdown();
33 | }
34 | }
35 |
36 | private void Logo_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
37 | {
38 | Process.Start("https://hotsapi.net");
39 | }
40 |
41 | private void ShowLog_Click(object sender, RoutedEventArgs e)
42 | {
43 | Process.Start("explorer.exe", $@"{App.SettingsDir}\logs");
44 | }
45 |
46 | private void Settings_Click(object sender, RoutedEventArgs e)
47 | {
48 | new SettingsWindow() { Owner = this, DataContext = this }.ShowDialog();
49 | }
50 |
51 | private async void Restart_Click(object sender, RoutedEventArgs e)
52 | {
53 | // Actually this should never happen when squirrel is disabled
54 | if (!App.NoSquirrel) {
55 | await UpdateManager.RestartAppWhenExited();
56 | }
57 | App.Shutdown();
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/NLog.config:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualBasic.ApplicationServices;
2 | using Squirrel;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace Hotsapi.Uploader.Windows
11 | {
12 | internal static class Program
13 | {
14 | [STAThread]
15 | public static void Main(string[] args)
16 | {
17 | if (!App.NoSquirrel) {
18 | // Note, in most of these scenarios, the app exits after this method completes!
19 | SquirrelAwareApp.HandleEvents(
20 | onInitialInstall: v => App.DummyUpdateManager.CreateShortcutForThisExe(),
21 | onAppUpdate: v => App.DummyUpdateManager.CreateShortcutForThisExe(),
22 | onAppUninstall: v => {
23 | App.DummyUpdateManager.RemoveShortcutForThisExe();
24 | if (Directory.Exists(App.SettingsDir)) {
25 | Directory.Delete(App.SettingsDir, true);
26 | }
27 | });
28 | }
29 |
30 | if (!Directory.Exists(App.SettingsDir)) {
31 | Directory.CreateDirectory(App.SettingsDir);
32 | }
33 |
34 | // Move files from old locations
35 | if (File.Exists($@"{App.AppDir}\..\replays.xml") && !File.Exists($@"{App.SettingsDir}\replays.xml")) {
36 | File.Move($@"{App.AppDir}\..\replays.xml", $@"{App.SettingsDir}\replays.xml");
37 | }
38 | if (File.Exists($@"{App.AppDir}\..\last.config") && !File.Exists($@"{App.SettingsDir}\last.config")) {
39 | File.Move($@"{App.AppDir}\..\last.config", $@"{App.SettingsDir}\last.config");
40 | }
41 |
42 | SingleInstanceManager manager = new SingleInstanceManager();
43 | manager.Run(args);
44 | }
45 | }
46 |
47 | public class SingleInstanceManager : WindowsFormsApplicationBase
48 | {
49 | private App _application;
50 |
51 | public SingleInstanceManager()
52 | {
53 | IsSingleInstance = true;
54 | ShutdownStyle = ShutdownMode.AfterAllFormsClose;
55 | }
56 |
57 | protected override bool OnStartup(StartupEventArgs eventArgs)
58 | {
59 | _application = new App();
60 | _application.InitializeComponent();
61 | _application.Run();
62 | return false;
63 | }
64 |
65 | protected override void OnStartupNextInstance(StartupNextInstanceEventArgs eventArgs)
66 | {
67 | base.OnStartupNextInstance(eventArgs);
68 | _application.Activate();
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("Hotsapi.Uploader.Windows")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("Hotsapi Uploader")]
15 | [assembly: AssemblyCopyright("Copyright © 2017")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 | [assembly: AssemblyMetadata("SquirrelAwareVersion", "1")]
44 |
45 | // Version information for an assembly consists of the following four values:
46 | //
47 | // Major Version
48 | // Minor Version
49 | // Build Number
50 | // Revision
51 | //
52 | // You can specify all the values or you can default the Build and Revision Numbers
53 | // by using the '*' as shown below:
54 | // [assembly: AssemblyVersion("1.0.*")]
55 | [assembly: AssemblyVersion("1.0.0")]
56 | [assembly: AssemblyFileVersion("1.0.0")]
57 | [assembly: AssemblyInformationalVersion("1.0.0")]
58 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Hotsapi.Uploader.Windows.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Hotsapi.Uploader.Windows.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Hotsapi.Uploader.Windows.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")]
16 | public sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 |
26 | [global::System.Configuration.UserScopedSettingAttribute()]
27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
28 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
29 | public bool UpgradeRequired {
30 | get {
31 | return ((bool)(this["UpgradeRequired"]));
32 | }
33 | set {
34 | this["UpgradeRequired"] = value;
35 | }
36 | }
37 |
38 | [global::System.Configuration.UserScopedSettingAttribute()]
39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
40 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
41 | public bool AutoUpdate {
42 | get {
43 | return ((bool)(this["AutoUpdate"]));
44 | }
45 | set {
46 | this["AutoUpdate"] = value;
47 | }
48 | }
49 |
50 | [global::System.Configuration.UserScopedSettingAttribute()]
51 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
52 | [global::System.Configuration.DefaultSettingValueAttribute("https://github.com/poma/Hotsapi.Uploader")]
53 | public string UpdateRepository {
54 | get {
55 | return ((string)(this["UpdateRepository"]));
56 | }
57 | set {
58 | this["UpdateRepository"] = value;
59 | }
60 | }
61 |
62 | [global::System.Configuration.UserScopedSettingAttribute()]
63 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
64 | [global::System.Configuration.DefaultSettingValueAttribute("400")]
65 | public int WindowTop {
66 | get {
67 | return ((int)(this["WindowTop"]));
68 | }
69 | set {
70 | this["WindowTop"] = value;
71 | }
72 | }
73 |
74 | [global::System.Configuration.UserScopedSettingAttribute()]
75 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
76 | [global::System.Configuration.DefaultSettingValueAttribute("400")]
77 | public int WindowLeft {
78 | get {
79 | return ((int)(this["WindowLeft"]));
80 | }
81 | set {
82 | this["WindowLeft"] = value;
83 | }
84 | }
85 |
86 | [global::System.Configuration.UserScopedSettingAttribute()]
87 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
88 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
89 | public bool MinimizeToTray {
90 | get {
91 | return ((bool)(this["MinimizeToTray"]));
92 | }
93 | set {
94 | this["MinimizeToTray"] = value;
95 | }
96 | }
97 |
98 | [global::System.Configuration.UserScopedSettingAttribute()]
99 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
100 | [global::System.Configuration.DefaultSettingValueAttribute("600")]
101 | public int WindowHeight {
102 | get {
103 | return ((int)(this["WindowHeight"]));
104 | }
105 | set {
106 | this["WindowHeight"] = value;
107 | }
108 | }
109 |
110 | [global::System.Configuration.UserScopedSettingAttribute()]
111 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
112 | [global::System.Configuration.DefaultSettingValueAttribute("700")]
113 | public int WindowWidth {
114 | get {
115 | return ((int)(this["WindowWidth"]));
116 | }
117 | set {
118 | this["WindowWidth"] = value;
119 | }
120 | }
121 |
122 | [global::System.Configuration.UserScopedSettingAttribute()]
123 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
124 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
125 | public bool UploadToHotslogs {
126 | get {
127 | return ((bool)(this["UploadToHotslogs"]));
128 | }
129 | set {
130 | this["UploadToHotslogs"] = value;
131 | }
132 | }
133 |
134 | [global::System.Configuration.UserScopedSettingAttribute()]
135 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
136 | [global::System.Configuration.DefaultSettingValueAttribute("None")]
137 | public global::Hotsapi.Uploader.Common.DeleteFiles DeleteAfterUpload {
138 | get {
139 | return ((global::Hotsapi.Uploader.Common.DeleteFiles)(this["DeleteAfterUpload"]));
140 | }
141 | set {
142 | this["DeleteAfterUpload"] = value;
143 | }
144 | }
145 |
146 | [global::System.Configuration.UserScopedSettingAttribute()]
147 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
148 | [global::System.Configuration.DefaultSettingValueAttribute("MetroDark")]
149 | public string Theme {
150 | get {
151 | return ((string)(this["Theme"]));
152 | }
153 | set {
154 | this["Theme"] = value;
155 | }
156 | }
157 |
158 | [global::System.Configuration.UserScopedSettingAttribute()]
159 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
160 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
161 | public bool AllowPreReleases {
162 | get {
163 | return ((bool)(this["AllowPreReleases"]));
164 | }
165 | set {
166 | this["AllowPreReleases"] = value;
167 | }
168 | }
169 |
170 | [global::System.Configuration.UserScopedSettingAttribute()]
171 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
172 | [global::System.Configuration.DefaultSettingValueAttribute("")]
173 | public string ApplicationVersion {
174 | get {
175 | return ((string)(this["ApplicationVersion"]));
176 | }
177 | set {
178 | this["ApplicationVersion"] = value;
179 | }
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | True
7 |
8 |
9 | True
10 |
11 |
12 | https://github.com/poma/Hotsapi.Uploader
13 |
14 |
15 | 400
16 |
17 |
18 | 400
19 |
20 |
21 | False
22 |
23 |
24 | 600
25 |
26 |
27 | 700
28 |
29 |
30 | False
31 |
32 |
33 | None
34 |
35 |
36 | MetroDark
37 |
38 |
39 | False
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Resources/uploader_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotsapi/Hotsapi.Uploader/2d853776022538641428c0ed97c75e8b4a770b15/Hotsapi.Uploader.Windows/Resources/uploader_dark.png
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Resources/uploader_icon_dark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotsapi/Hotsapi.Uploader/2d853776022538641428c0ed97c75e8b4a770b15/Hotsapi.Uploader.Windows/Resources/uploader_icon_dark.ico
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Resources/uploader_icon_light.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotsapi/Hotsapi.Uploader/2d853776022538641428c0ed97c75e8b4a770b15/Hotsapi.Uploader.Windows/Resources/uploader_icon_light.ico
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Resources/uploader_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hotsapi/Hotsapi.Uploader/2d853776022538641428c0ed97c75e8b4a770b15/Hotsapi.Uploader.Windows/Resources/uploader_light.png
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/SettingsWindow.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/SettingsWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 | using System.Windows.Data;
9 | using System.Windows.Documents;
10 | using System.Windows.Input;
11 | using System.Windows.Media;
12 | using System.Windows.Media.Imaging;
13 | using System.Windows.Shapes;
14 |
15 | namespace Hotsapi.Uploader.Windows
16 | {
17 | ///
18 | /// Interaction logic for SettingsWindow.xaml
19 | ///
20 | public partial class SettingsWindow : Window
21 | {
22 | public SettingsWindow()
23 | {
24 | InitializeComponent();
25 | if (App.Settings.AllowPreReleases) {
26 | PreReleasePanel.Visibility = Visibility.Visible;
27 | }
28 | }
29 |
30 | private void Window_KeyDown(object sender, KeyEventArgs e)
31 | {
32 | if (e.Key == Key.Z && Keyboard.Modifiers == ModifierKeys.Control) {
33 | PreReleasePanel.Visibility = Visibility.Visible;
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Themes/Default/Default.xaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Themes/MetroDark/MetroDark.Hotsapi.Implicit.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Themes/MetroDark/Styles.WPF.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
56 |
57 |
58 |
80 |
81 |
82 |
124 |
125 |
126 |
168 |
169 |
170 |
306 |
307 |
308 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 | Visible
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 | Visible
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 | Visible
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 | Visible
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
632 |
633 |
634 |
740 |
741 |
742 |
827 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/Themes/MetroDark/Theme.Colors.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | #FF282828
13 | #FFFFFFFF
14 | #FFBABABA
15 | #FF858585
16 | #FF747474
17 | #FF565656
18 | #FF454545
19 | #FF333333
20 | #FF292929
21 | #FF181818
22 |
23 |
24 | #E5FFFFFF
25 |
26 | #BFFFFFFF
27 |
28 | #99FFFFFF
29 |
30 | #72FFFFFF
31 |
32 | #4CFFFFFF
33 |
34 | #26FFFFFF
35 |
36 | #00FFFFFF
37 |
38 |
39 |
40 | #E5000000
41 |
42 | #BF000000
43 |
44 | #99000000
45 |
46 | #72000000
47 |
48 | #4C000000
49 |
50 | #26000000
51 |
52 | #00000000
53 |
54 | #66E2E2E2
55 |
56 |
57 | #FF4f52b0
58 | #FF6468de
59 | #FF8185f0
60 | #FFb1b3f0
61 | #266468de
62 |
63 |
64 | #FFD0284C
65 | #FFF55E7F
66 | #FFFFCAD5
67 |
68 |
69 | #FF006481
70 | #FF8A9B0F
71 | #FF3E4700
72 | #FFF14D0F
73 | #FF8D2E00
74 | #FF81106B
75 | #FF410135
76 | #FFFCA910
77 | #FF8D4902
78 | #FF037A54
79 | #FF003F2A
80 | #FF154D85
81 | #FF02284D
82 | #FF543511
83 | #FF211303
84 | #FFBB8E2E
85 | #FF393225
86 | #FF58458B
87 | #FF211347
88 | #7FB9B9B9
89 | #33565656
90 | #7F3F3F3F
91 | #FF686868
92 | #8000AADE
93 | #CC3F3F3F
94 |
95 |
96 |
97 | #FF0092BE
98 | #FF00AADE
99 | #FF2BB9E5
100 | #FF55C8EB
101 | #FF80D7F2
102 |
103 |
104 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/FilenameConverter.cs:
--------------------------------------------------------------------------------
1 | using Hotsapi.Uploader.Common;
2 | using System;
3 | using System.Linq;
4 | using System.IO;
5 | using System.Text.RegularExpressions;
6 | using System.Windows.Media;
7 |
8 | namespace Hotsapi.Uploader.Windows.UIHelpers
9 | {
10 | public class FilenameConverter : GenericValueConverter
11 | {
12 | protected override string Convert(string value)
13 | {
14 | return Path.GetFileName(value);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/FlagsConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Windows;
4 |
5 | namespace Hotsapi.Uploader.Windows.UIHelpers
6 | {
7 | public class FlagsConverter : GenericValueConverter
8 | {
9 | protected override bool Convert(Enum value, Enum parameter)
10 | {
11 | return value.HasFlag(parameter);
12 | }
13 |
14 | protected override Enum ConvertBack(bool value, Enum parameter)
15 | {
16 | // I was unable to find how to get source binding value, so let's use a dirty hack
17 | var val = App.Settings.DeleteAfterUpload;
18 |
19 | if (value) {
20 | val |= (Common.DeleteFiles)parameter;
21 | } else {
22 | val &= ~(Common.DeleteFiles)parameter;
23 | }
24 | return val;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/GenericValueConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Globalization;
6 | using System.Windows.Data;
7 | using System.Windows.Markup;
8 | using System.Windows;
9 |
10 | namespace Hotsapi.Uploader.Windows.UIHelpers
11 | {
12 | public abstract class GenericValueConverter : IValueConverter
13 | {
14 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
15 | {
16 | //if (value.GetType() != typeof(V)) throw new ArgumentException(GetType().Name + ".Convert: value type not " + typeof(V).Name);
17 | //if (targetType != typeof(T)) throw new ArgumentException(GetType().Name + ".Convert: target type not " + typeof(T).Name);
18 | //if (parameter != null) throw new ArgumentException(GetType().Name + ".Convert: binding contains unexpected parameter");
19 | return Convert((V)value, (P)parameter);
20 | }
21 |
22 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
23 | {
24 | //if (value.GetType() != typeof(T)) throw new ArgumentException(GetType().Name + ".ConvertBack: value type not " + typeof(T).Name);
25 | //if (targetType != typeof(V)) throw new ArgumentException(GetType().Name + ".ConvertBack: target type not " + typeof(V).Name);
26 | //if (parameter != null) throw new ArgumentException(GetType().Name + ".Convert: binding contains unexpected parameter");
27 | return ConvertBack((T)value, (P)parameter);
28 | }
29 |
30 | protected virtual T Convert(V value, P parameter)
31 | {
32 | throw new NotImplementedException(GetType().Name + "Convert not implemented");
33 | }
34 | protected virtual V ConvertBack(T value, P parameter)
35 | {
36 | throw new NotImplementedException(GetType().Name + "ConvertBack not implemented");
37 | }
38 | }
39 |
40 | public abstract class GenericValueConverter : IValueConverter
41 | {
42 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
43 | {
44 | //if (value.GetType() != typeof(V)) throw new ArgumentException(GetType().Name + ".Convert: value type not " + typeof(V).Name);
45 | //if (targetType != typeof(T)) throw new ArgumentException(GetType().Name + ".Convert: target type not " + typeof(T).Name);
46 | //if (parameter != null) throw new ArgumentException(GetType().Name + ".Convert: binding contains unexpected parameter");
47 | return Convert((V)value);
48 | }
49 |
50 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
51 | {
52 | //if (value.GetType() != typeof(T)) throw new ArgumentException(GetType().Name + ".ConvertBack: value type not " + typeof(T).Name);
53 | //if (targetType != typeof(V)) throw new ArgumentException(GetType().Name + ".ConvertBack: target type not " + typeof(V).Name);
54 | //if (parameter != null) throw new ArgumentException(GetType().Name + ".Convert: binding contains unexpected parameter");
55 | return ConvertBack((T)value);
56 | }
57 |
58 | protected virtual T Convert(V value)
59 | {
60 | throw new NotImplementedException(GetType().Name + "Convert not implemented");
61 | }
62 | protected virtual V ConvertBack(T value)
63 | {
64 | throw new NotImplementedException(GetType().Name + "ConvertBack not implemented");
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/IntToVisibilityConverter.cs:
--------------------------------------------------------------------------------
1 | using Hotsapi.Uploader.Common;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Windows;
6 |
7 | namespace Hotsapi.Uploader.Windows.UIHelpers
8 | {
9 | public class IntToVisibilityConverter : GenericValueConverter, Visibility, UploadStatus>
10 | {
11 | protected override Visibility Convert(Dictionary value, UploadStatus parameter)
12 | {
13 | return value.ContainsKey(parameter) ? Visibility.Visible : Visibility.Collapsed;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/MarginSetter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 |
9 | namespace Hotsapi.Uploader.Windows.UIHelpers
10 | {
11 | internal class MarginSetter
12 | {
13 | public static Thickness GetMargin(DependencyObject obj)
14 | {
15 | return (Thickness)obj.GetValue(MarginProperty);
16 | }
17 |
18 | public static void SetMargin(DependencyObject obj, Thickness value)
19 | {
20 | obj.SetValue(MarginProperty, value);
21 | }
22 |
23 | public static readonly DependencyProperty MarginProperty =
24 | DependencyProperty.RegisterAttached("Margin", typeof(Thickness), typeof(MarginSetter), new UIPropertyMetadata(new Thickness(), CreateThicknesForChildren));
25 |
26 | public static void CreateThicknesForChildren(object sender, DependencyPropertyChangedEventArgs e)
27 | {
28 | var panel = sender as Panel;
29 | if (panel == null) return;
30 | panel.Loaded += (q, w) => CreateThicknesForChildrenInner(panel);
31 | CreateThicknesForChildrenInner(panel);
32 | }
33 |
34 | private static void CreateThicknesForChildrenInner(Panel panel)
35 | {
36 | var zero = new Thickness(0);
37 | foreach (var child in panel.Children) {
38 | var fe = child as FrameworkElement;
39 | if (fe == null) continue;
40 | if (fe.Margin == zero)
41 | fe.Margin = MarginSetter.GetMargin(panel);
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/UploadColorConverter.cs:
--------------------------------------------------------------------------------
1 | using Hotsapi.Uploader.Common;
2 | using System;
3 | using System.Linq;
4 | using System.Windows.Media;
5 |
6 | namespace Hotsapi.Uploader.Windows.UIHelpers
7 | {
8 | public class UploadColorConverter : GenericValueConverter
9 | {
10 | protected override Brush Convert(UploadStatus value)
11 | {
12 | switch (value) {
13 | case UploadStatus.Success:
14 | return GetBrush("StatusUploadSuccessBrush");
15 |
16 | case UploadStatus.InProgress:
17 | return GetBrush("StatusUploadInProgressBrush");
18 |
19 | case UploadStatus.Duplicate:
20 | case UploadStatus.AiDetected:
21 | case UploadStatus.CustomGame:
22 | case UploadStatus.PtrRegion:
23 | case UploadStatus.TooOld:
24 | return GetBrush("StatusUploadNeutralBrush");
25 |
26 | case UploadStatus.None:
27 | case UploadStatus.UploadError:
28 | case UploadStatus.Incomplete:
29 | default:
30 | return GetBrush("StatusUploadFailedBrush");
31 | }
32 | }
33 |
34 | private Brush GetBrush(string key)
35 | {
36 | return App.Current.Resources[key] as Brush;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.Windows/UIHelpers/UploadStatusConverter.cs:
--------------------------------------------------------------------------------
1 | using Hotsapi.Uploader.Common;
2 | using System;
3 | using System.Linq;
4 | using System.Text.RegularExpressions;
5 | using System.Windows;
6 |
7 | namespace Hotsapi.Uploader.Windows.UIHelpers
8 | {
9 | public class UploadStatusConverter : GenericValueConverter
10 | {
11 | protected override string Convert(UploadStatus value)
12 | {
13 | if (value == UploadStatus.None) {
14 | return "";
15 | }
16 | // Convert "EnumItems" to "Enum items"
17 | return Regex.Replace(value.ToString(), "([a-z])([A-Z])", m => $"{m.Groups[1].Value} {m.Groups[2].Value.ToLower()}");
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Hotsapi.Uploader.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29519.87
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hotsapi.Uploader.Common", "Hotsapi.Uploader.Common\Hotsapi.Uploader.Common.csproj", "{DC695BCC-4403-4B20-B4F5-EB80683E5967}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hotsapi.Uploader.Windows", "Hotsapi.Uploader.Windows\Hotsapi.Uploader.Windows.csproj", "{F774F86B-410F-410D-9719-ADF892D315D5}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MpqTool.netstandard", "Heroes.ReplayParser\MpqTool\MpqTool.netstandard.csproj", "{401F4637-5826-4DDA-A666-A3B4D1994330}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Heroes.ReplayParser.netstandard", "Heroes.ReplayParser\Heroes.ReplayParser\Heroes.ReplayParser.netstandard.csproj", "{7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hotsapi.Uploader.Common.Test", "Hotsapi.Uploader.Common.Test\Hotsapi.Uploader.Common.Test.csproj", "{F0AF2897-857E-462E-86E0-2812624E2F78}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Installer|Any CPU = Installer|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | Zip|Any CPU = Zip|Any CPU
22 | EndGlobalSection
23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
24 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Installer|Any CPU.ActiveCfg = Release|Any CPU
27 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Installer|Any CPU.Build.0 = Release|Any CPU
28 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Zip|Any CPU.ActiveCfg = Release|Any CPU
31 | {DC695BCC-4403-4B20-B4F5-EB80683E5967}.Zip|Any CPU.Build.0 = Release|Any CPU
32 | {F774F86B-410F-410D-9719-ADF892D315D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {F774F86B-410F-410D-9719-ADF892D315D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {F774F86B-410F-410D-9719-ADF892D315D5}.Installer|Any CPU.ActiveCfg = Installer|Any CPU
35 | {F774F86B-410F-410D-9719-ADF892D315D5}.Installer|Any CPU.Build.0 = Installer|Any CPU
36 | {F774F86B-410F-410D-9719-ADF892D315D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {F774F86B-410F-410D-9719-ADF892D315D5}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {F774F86B-410F-410D-9719-ADF892D315D5}.Zip|Any CPU.ActiveCfg = Zip|Any CPU
39 | {F774F86B-410F-410D-9719-ADF892D315D5}.Zip|Any CPU.Build.0 = Zip|Any CPU
40 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Installer|Any CPU.ActiveCfg = Release|Any CPU
43 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Installer|Any CPU.Build.0 = Release|Any CPU
44 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Zip|Any CPU.ActiveCfg = Release|Any CPU
47 | {401F4637-5826-4DDA-A666-A3B4D1994330}.Zip|Any CPU.Build.0 = Release|Any CPU
48 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Installer|Any CPU.ActiveCfg = Release|Any CPU
51 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Installer|Any CPU.Build.0 = Release|Any CPU
52 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Zip|Any CPU.ActiveCfg = Release|Any CPU
55 | {7CEC9C7E-3DC6-4C72-8DFF-FE07D086B3A4}.Zip|Any CPU.Build.0 = Release|Any CPU
56 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Installer|Any CPU.ActiveCfg = Debug|Any CPU
59 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Installer|Any CPU.Build.0 = Debug|Any CPU
60 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
61 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Release|Any CPU.Build.0 = Release|Any CPU
62 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Zip|Any CPU.ActiveCfg = Debug|Any CPU
63 | {F0AF2897-857E-462E-86E0-2812624E2F78}.Zip|Any CPU.Build.0 = Debug|Any CPU
64 | EndGlobalSection
65 | GlobalSection(SolutionProperties) = preSolution
66 | HideSolutionNode = FALSE
67 | EndGlobalSection
68 | GlobalSection(ExtensibilityGlobals) = postSolution
69 | SolutionGuid = {24555457-A9E2-4C94-B0E4-1BC34F4C042B}
70 | EndGlobalSection
71 | EndGlobal
72 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Roman Semenov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hotsapi.Uploader [](https://ci.appveyor.com/project/poma/hotsapi-uploader/branch/master) [](https://discord.gg/cADfdFP)
2 |
3 | Uploads Heroes of the Storm replays to [hotsapi.net](https://hotsapi.net) ([repo link](https://github.com/poma/hotsapi))
4 |
5 | 
6 |
7 | # Installation
8 |
9 | * Requires .NET Framework 4.6.2 or higher
10 | * [__Download__](https://github.com/Poma/Hotsapi.Uploader/releases/latest) **"HotsApiUploaderSetup.exe"** from [Releases](https://github.com/Poma/Hotsapi.Uploader/releases/latest) page (you don't need to download other files listed there) and run it
11 |
12 | *Note:* sometimes the installer is mistakenly marked as a virus by some AV vendors heuristics because they don't like things that install something on your PC in general. If you don't trust the installer you can download a portable "HotsApi.zip" and use it instead. In that case you are losing auto updates, start with windows, and shortcuts. Also you'll need to make sure that [.NET 4.6.2](https://www.microsoft.com/en-us/download/details.aspx?id=53344) is installed on your machine.
13 |
14 | # Contributing
15 |
16 | Coding conventions are as usual for C# except braces, those are in egyptian style ([OTBS](https://en.wikipedia.org/wiki/Indent_style#1TBS)). For repos included as submodules their coding style is used.
17 |
18 | All logic is contained in `Hotsapi.Uploader.Common` to make UI project as thin as possible. `Hotsapi.Uploader.Windows` is responsible for only OS-specific tasks such as auto update, tray icon, autorun, file locations.
19 |
20 | For the current to do list look in the [Project](https://github.com/poma/Hotsapi.Uploader/projects/1) page
21 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | core_version: 2.1.0
3 | GitHubToken:
4 | secure: Hq962a6/5Qpa9d37AJuVplk7sYc4AYRn9b5dg4eLi1jXBkdvbj2zRsG+8r+4nNE3
5 | image: Visual Studio 2019
6 | version: '$(core_version)+{branch}.{build}'
7 | install:
8 | - cmd: git submodule update --init --recursive
9 | - cmd: nuget.exe restore
10 | - cmd: IF "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" (%UserProfile%\.nuget\packages\squirrel.windows\1.7.8\tools\SyncReleases.exe --url=https://github.com/poma/Hotsapi.Uploader --token=%GitHubToken%)
11 |
12 | assembly_info:
13 | patch: true
14 | file: Hotsapi.Uploader.Windows\Properties\AssemblyInfo.cs
15 | assembly_version: '$(core_version)'
16 | assembly_file_version: '$(core_version)'
17 | assembly_informational_version: '{version}'
18 |
19 | build_script:
20 | - msbuild Hotsapi.Uploader.sln -verbosity:minimal /property:Configuration=Zip
21 | - msbuild Hotsapi.Uploader.sln -verbosity:minimal /property:Configuration=Installer
22 |
23 | artifacts:
24 | - path: Releases\HotsApiUploaderSetup.exe
25 | - path: Releases\HotsApi.zip
26 | - path: Releases\RELEASES
27 | - path: Releases\Hotsapi.Uploader-$(core_version)-full.nupkg
28 | - path: Releases\Hotsapi.Uploader-$(core_version)-delta.nupkg
29 |
30 | deploy:
31 | - provider: GitHub
32 | auth_token:
33 | secure: Hq962a6/5Qpa9d37AJuVplk7sYc4AYRn9b5dg4eLi1jXBkdvbj2zRsG+8r+4nNE3
34 | repository: poma/Hotsapi.Uploader
35 | artifact: '/.*/'
36 | draft: true
37 | on:
38 | branch: /^v\d+\.\d+\.\d+/
39 | appveyor_repo_tag: true
40 |
41 | cache:
42 | - packages -> **\packages.config
43 | - '%USERPROFILE%\.nuget\packages -> **\*.csproj'
44 | - Releases
45 |
46 | notifications:
47 | - provider: Webhook
48 | url: https://webhooks.gitter.im/e/59b5d893e9a21d517d5e
49 | method: POST
50 | on_build_success: true
51 | on_build_failure: true
52 | on_build_status_changed: true
--------------------------------------------------------------------------------