HotReloadAboutTab.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.Linq;
  7. using UnityEditor;
  8. using UnityEngine;
  9. using System.Threading.Tasks;
  10. using System.IO;
  11. using SingularityGroup.HotReload.Newtonsoft.Json;
  12. using SingularityGroup.HotReload.EditorDependencies;
  13. namespace SingularityGroup.HotReload.Editor {
  14. internal struct HotReloadAboutTabState {
  15. public readonly bool logsFodlerExists;
  16. public readonly IReadOnlyList<ChangelogVersion> changelog;
  17. public readonly bool loginRequired;
  18. public readonly bool hasTrialLicense;
  19. public readonly bool hasPayedLicense;
  20. public HotReloadAboutTabState(
  21. bool logsFodlerExists,
  22. IReadOnlyList<ChangelogVersion> changelog,
  23. bool loginRequired,
  24. bool hasTrialLicense,
  25. bool hasPayedLicense
  26. ) {
  27. this.logsFodlerExists = logsFodlerExists;
  28. this.changelog = changelog;
  29. this.loginRequired = loginRequired;
  30. this.hasTrialLicense = hasTrialLicense;
  31. this.hasPayedLicense = hasPayedLicense;
  32. }
  33. }
  34. internal class HotReloadAboutTab : HotReloadTabBase {
  35. internal static readonly OpenURLButton seeMore = new OpenURLButton("See More", Constants.ChangelogURL);
  36. internal static readonly OpenDialogueButton manageLicenseButton = new OpenDialogueButton("Manage License", Constants.ManageLicenseURL, "Manage License", "Upgrade/downgrade/edit your subscription and edit payment info.", "Open in browser", "Cancel");
  37. internal static readonly OpenDialogueButton manageAccountButton = new OpenDialogueButton("Manage Account", Constants.ManageAccountURL, "Manage Account", "Login with company code 'naughtycult'. Use the email you signed up with. Your initial password was sent to you by email.", "Open in browser", "Cancel");
  38. internal static readonly OpenURLButton contactButton = new OpenURLButton("Contact", Constants.ContactURL);
  39. internal static readonly OpenURLButton discordButton = new OpenURLButton("Join Discord", Constants.DiscordInviteUrl);
  40. internal static readonly OpenDialogueButton reportIssueButton = new OpenDialogueButton("Report issue", Constants.ReportIssueURL, "Report issue", "Report issue in our public issue tracker. Requires gitlab.com account (if you don't have one and are not willing to make it, please contact us by other means such as our website).", "Open in browser", "Cancel");
  41. private Vector2 _changelogScroll;
  42. private IReadOnlyList<ChangelogVersion> _changelog = new List<ChangelogVersion>();
  43. private bool _requestedChangelog;
  44. private int _changelogRequestAttempt;
  45. private string _changelogDir = Path.Combine(PackageConst.LibraryCachePath, "changelog.json");
  46. public static string logsPath = Path.Combine(PackageConst.LibraryCachePath, "logs");
  47. private static bool LatestChangelogLoaded(IReadOnlyList<ChangelogVersion> changelog) {
  48. return changelog.Any() && changelog[0].versionNum == PackageUpdateChecker.lastRemotePackageVersion;
  49. }
  50. private async Task FetchChangelog() {
  51. if(!_changelog.Any()) {
  52. var file = new FileInfo(_changelogDir);
  53. if (file.Exists) {
  54. await Task.Run(() => {
  55. var bytes = File.ReadAllText(_changelogDir);
  56. _changelog = JsonConvert.DeserializeObject<List<ChangelogVersion>>(bytes);
  57. });
  58. }
  59. }
  60. if (_requestedChangelog || LatestChangelogLoaded(_changelog)) {
  61. return;
  62. }
  63. _requestedChangelog = true;
  64. try {
  65. do {
  66. var changelogRequestTimeout = ExponentialBackoff.GetTimeout(_changelogRequestAttempt);
  67. _changelog = await RequestHelper.FetchChangelog() ?? _changelog;
  68. if (LatestChangelogLoaded(_changelog)) {
  69. await Task.Run(() => {
  70. Directory.CreateDirectory(PackageConst.LibraryCachePath);
  71. File.WriteAllText(_changelogDir, JsonConvert.SerializeObject(_changelog));
  72. });
  73. Repaint();
  74. return;
  75. }
  76. await Task.Delay(changelogRequestTimeout);
  77. } while (_changelogRequestAttempt++ < 1000 && !LatestChangelogLoaded(_changelog));
  78. } catch {
  79. // ignore
  80. } finally {
  81. _requestedChangelog = false;
  82. }
  83. }
  84. public HotReloadAboutTab(HotReloadWindow window) : base(window, "Help", "_Help", "Info and support for Hot Reload for Unity.") { }
  85. string GetRelativeDate(DateTime givenDate) {
  86. const int second = 1;
  87. const int minute = 60 * second;
  88. const int hour = 60 * minute;
  89. const int day = 24 * hour;
  90. const int month = 30 * day;
  91. var ts = new TimeSpan(DateTime.UtcNow.Ticks - givenDate.Ticks);
  92. var delta = Math.Abs(ts.TotalSeconds);
  93. if (delta < 24 * hour)
  94. return "Today";
  95. if (delta < 48 * hour)
  96. return "Yesterday";
  97. if (delta < 30 * day)
  98. return ts.Days + " days ago";
  99. if (delta < 12 * month) {
  100. var months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  101. return months <= 1 ? "one month ago" : months + " months ago";
  102. }
  103. var years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
  104. return years <= 1 ? "one year ago" : years + " years ago";
  105. }
  106. void RenderVersion(ChangelogVersion version) {
  107. var tempTextString = "";
  108. //version number
  109. EditorGUILayout.TextArea(version.versionNum, HotReloadWindowStyles.H1TitleStyle);
  110. //general info
  111. if (version.generalInfo != null) {
  112. EditorGUILayout.TextArea(version.generalInfo, HotReloadWindowStyles.H3TitleStyle);
  113. }
  114. //features
  115. if (version.features != null) {
  116. EditorGUILayout.TextArea("Features:", HotReloadWindowStyles.H2TitleStyle);
  117. tempTextString = "";
  118. foreach (var feature in version.features) {
  119. tempTextString += "• " + feature + "\n";
  120. }
  121. EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle);
  122. }
  123. //improvements
  124. if (version.improvements != null) {
  125. EditorGUILayout.TextArea("Improvements:", HotReloadWindowStyles.H2TitleStyle);
  126. tempTextString = "";
  127. foreach (var improvement in version.improvements) {
  128. tempTextString += "• " + improvement + "\n";
  129. }
  130. EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle);
  131. }
  132. //fixes
  133. if (version.fixes != null) {
  134. EditorGUILayout.TextArea("Fixes:", HotReloadWindowStyles.H2TitleStyle);
  135. tempTextString = "";
  136. foreach (var fix in version.fixes) {
  137. tempTextString += "• " + fix + "\n";
  138. }
  139. EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle);
  140. }
  141. //date
  142. DateTime date;
  143. if (DateTime.TryParseExact(version.date, "dd/MM/yyyy", null, DateTimeStyles.None, out date)) {
  144. var relativeDate = GetRelativeDate(date);
  145. GUILayout.TextArea(relativeDate, HotReloadWindowStyles.H3TitleStyle);
  146. }
  147. }
  148. void RenderChangelog() {
  149. FetchChangelog().Forget();
  150. using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
  151. using (new EditorGUILayout.VerticalScope()) {
  152. HotReloadPrefs.ShowChangeLog = EditorGUILayout.Foldout(HotReloadPrefs.ShowChangeLog, "Changelog", true, HotReloadWindowStyles.FoldoutStyle);
  153. if (!HotReloadPrefs.ShowChangeLog) {
  154. return;
  155. }
  156. // changelog versions
  157. var maxChangeLogs = 5;
  158. var index = 0;
  159. foreach (var version in currentState.changelog) {
  160. index++;
  161. if (index > maxChangeLogs) {
  162. break;
  163. }
  164. using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.ChangelogSectionInnerBox)) {
  165. using (new EditorGUILayout.VerticalScope()) {
  166. RenderVersion(version);
  167. }
  168. }
  169. }
  170. // see more button
  171. using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.ChangelogSectionInnerBox)) {
  172. seeMore.OnGUI();
  173. }
  174. }
  175. }
  176. }
  177. private Vector2 _aboutTabScrollPos;
  178. HotReloadAboutTabState currentState;
  179. public override void OnGUI() {
  180. // HotReloadAboutTabState ensures rendering is consistent between Layout and Repaint calls
  181. // Without it errors like this happen:
  182. // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint
  183. // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html
  184. if (Event.current.type == EventType.Layout) {
  185. currentState = new HotReloadAboutTabState(
  186. logsFodlerExists: Directory.Exists(logsPath),
  187. changelog: _changelog,
  188. loginRequired: EditorCodePatcher.LoginNotRequired,
  189. hasTrialLicense: _window.RunTab.TrialLicense,
  190. hasPayedLicense: _window.RunTab.HasPayedLicense
  191. );
  192. }
  193. using (var scope = new EditorGUILayout.ScrollViewScope(_aboutTabScrollPos, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(Math.Max(HotReloadWindowStyles.windowScreenHeight, 800)), GUILayout.MaxWidth(Math.Max(HotReloadWindowStyles.windowScreenWidth, 800)))) {
  194. _aboutTabScrollPos.x = scope.scrollPosition.x;
  195. _aboutTabScrollPos.y = scope.scrollPosition.y;
  196. using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamicSectionHelpTab)) {
  197. using (new EditorGUILayout.VerticalScope()) {
  198. GUILayout.Space(10);
  199. RenderLogButtons();
  200. EditorGUILayout.Space();
  201. EditorGUILayout.HelpBox($" Hot Reload version {PackageConst.Version}. ", MessageType.Info);
  202. EditorGUILayout.Space();
  203. RenderHelpButtons();
  204. GUILayout.Space(15);
  205. try {
  206. RenderChangelog();
  207. } catch {
  208. // ignore
  209. }
  210. }
  211. }
  212. }
  213. }
  214. void RenderHelpButtons() {
  215. var labelRect = GUILayoutUtility.GetLastRect();
  216. using (new EditorGUILayout.HorizontalScope()) {
  217. using (new EditorGUILayout.VerticalScope()) {
  218. var buttonHeight = 19;
  219. var bigButtonRect = new Rect(labelRect.x + 3, labelRect.y + 5, labelRect.width - 6, buttonHeight);
  220. OpenURLButton.RenderRaw(bigButtonRect, "Documentation", Constants.DocumentationURL, HotReloadWindowStyles.HelpTabButton);
  221. var firstLayerX = bigButtonRect.x;
  222. var firstLayerY = bigButtonRect.y + buttonHeight + 3;
  223. var firstLayerWidth = (int)((bigButtonRect.width / 2) - 3);
  224. var secondLayerX = firstLayerX + firstLayerWidth + 5;
  225. var secondLayerY = firstLayerY + buttonHeight + 3;
  226. var secondLayerWidth = bigButtonRect.width - firstLayerWidth - 5;
  227. using (new EditorGUILayout.HorizontalScope()) {
  228. OpenURLButton.RenderRaw(new Rect { x = firstLayerX, y = firstLayerY, width = firstLayerWidth, height = buttonHeight }, contactButton.text, contactButton.url, HotReloadWindowStyles.HelpTabButton);
  229. OpenURLButton.RenderRaw(new Rect { x = secondLayerX, y = firstLayerY, width = secondLayerWidth, height = buttonHeight }, "Unity Forum", Constants.ForumURL, HotReloadWindowStyles.HelpTabButton);
  230. }
  231. using (new EditorGUILayout.HorizontalScope()) {
  232. OpenDialogueButton.RenderRaw(rect: new Rect { x = firstLayerX, y = secondLayerY, width = firstLayerWidth, height = buttonHeight }, text: reportIssueButton.text, url: reportIssueButton.url, title: reportIssueButton.title, message: reportIssueButton.message, ok: reportIssueButton.ok, cancel: reportIssueButton.cancel, style: HotReloadWindowStyles.HelpTabButton);
  233. OpenURLButton.RenderRaw(new Rect { x = secondLayerX, y = secondLayerY, width = secondLayerWidth, height = buttonHeight }, discordButton.text, discordButton.url, HotReloadWindowStyles.HelpTabButton);
  234. }
  235. }
  236. }
  237. GUILayout.Space(80);
  238. }
  239. void RenderLogButtons() {
  240. if (currentState.logsFodlerExists) {
  241. EditorGUILayout.Space();
  242. EditorGUILayout.BeginHorizontal();
  243. GUILayout.FlexibleSpace();
  244. if (GUILayout.Button("Open Log File")) {
  245. var mostRecentFile = LogsHelper.FindRecentLog(logsPath);
  246. if (mostRecentFile == null) {
  247. Log.Info("No logs found");
  248. } else {
  249. try {
  250. Process.Start($"\"{Path.Combine(logsPath, mostRecentFile)}\"");
  251. } catch (Win32Exception e) {
  252. if (e.Message.Contains("Application not found")) {
  253. try {
  254. Process.Start("notepad.exe", $"\"{Path.Combine(logsPath, mostRecentFile)}\"");
  255. } catch {
  256. // Fallback to opening folder with all logs
  257. Process.Start($"\"{logsPath}\"");
  258. Log.Info("Failed opening log file.");
  259. }
  260. }
  261. } catch {
  262. // Fallback to opening folder with all logs
  263. Process.Start($"\"{logsPath}\"");
  264. Log.Info("Failed opening log file.");
  265. }
  266. }
  267. }
  268. if (GUILayout.Button("Browse all logs")) {
  269. Process.Start($"\"{logsPath}\"");
  270. }
  271. EditorGUILayout.EndHorizontal();
  272. }
  273. }
  274. }
  275. }