using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Linq; using SingularityGroup.HotReload.Editor.Localization; using UnityEditor; using UnityEngine; using System.Threading.Tasks; using System.IO; using SingularityGroup.HotReload.Newtonsoft.Json; using SingularityGroup.HotReload.EditorDependencies; namespace SingularityGroup.HotReload.Editor { internal struct HotReloadAboutTabState { public readonly bool logsFodlerExists; public readonly IReadOnlyList changelog; public readonly bool loginRequired; public readonly bool hasTrialLicense; public readonly bool hasPayedLicense; public HotReloadAboutTabState( bool logsFodlerExists, IReadOnlyList changelog, bool loginRequired, bool hasTrialLicense, bool hasPayedLicense ) { this.logsFodlerExists = logsFodlerExists; this.changelog = changelog; this.loginRequired = loginRequired; this.hasTrialLicense = hasTrialLicense; this.hasPayedLicense = hasPayedLicense; } } internal class HotReloadAboutTab : HotReloadTabBase { internal static readonly OpenURLButton seeMore = new OpenURLButton(Translations.About.ButtonSeeMore, Constants.ChangelogURL); internal static readonly OpenDialogueButton manageLicenseButton = new OpenDialogueButton(Translations.About.ButtonManageLicense, Constants.ManageLicenseURL, Translations.About.ButtonManageLicense, Translations.Dialogs.DialogManageLicenseMessage, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel); internal static readonly OpenDialogueButton manageAccountButton = new OpenDialogueButton(Translations.About.ButtonManageAccount, Constants.ManageAccountURL, Translations.About.ButtonManageAccount, Translations.Dialogs.DialogManageAccountMessage, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel); internal static readonly OpenURLButton contactButton = new OpenURLButton(Translations.About.ButtonContact, Constants.ContactURL); internal static readonly OpenURLButton discordButton = new OpenURLButton(Translations.About.ButtonJoinDiscord, Constants.DiscordInviteUrl); internal static readonly OpenDialogueButton reportIssueButton = new OpenDialogueButton(Translations.About.ButtonReportIssue, Constants.ReportIssueURL, Translations.About.ButtonReportIssue, Translations.Dialogs.DialogReportIssueMessage, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel); private Vector2 _changelogScroll; private IReadOnlyList _changelog = new List(); private bool _requestedChangelog; private int _changelogRequestAttempt; private string _changelogDir = Path.Combine(PackageConst.LibraryCachePath, "changelog.json"); public static string logsPath = Path.Combine(PackageConst.LibraryCachePath, "logs"); private static bool LatestChangelogLoaded(IReadOnlyList changelog) { return changelog.Any() && changelog[0].versionNum == PackageUpdateChecker.lastRemotePackageVersion; } private async Task FetchChangelog() { if(!_changelog.Any()) { var file = new FileInfo(_changelogDir); if (file.Exists) { await Task.Run(() => { var bytes = File.ReadAllText(_changelogDir); _changelog = JsonConvert.DeserializeObject>(bytes); }); } } if (_requestedChangelog || LatestChangelogLoaded(_changelog)) { return; } _requestedChangelog = true; try { do { var changelogRequestTimeout = ExponentialBackoff.GetTimeout(_changelogRequestAttempt); _changelog = await RequestHelper.FetchChangelog() ?? _changelog; if (LatestChangelogLoaded(_changelog)) { await Task.Run(() => { Directory.CreateDirectory(PackageConst.LibraryCachePath); File.WriteAllText(_changelogDir, JsonConvert.SerializeObject(_changelog)); }); Repaint(); return; } await Task.Delay(changelogRequestTimeout); } while (_changelogRequestAttempt++ < 1000 && !LatestChangelogLoaded(_changelog)); } catch { // ignore } finally { _requestedChangelog = false; } } public HotReloadAboutTab(HotReloadWindow window) : base(window, Translations.About.AboutTitle, "_Help", Translations.About.AboutDescription) { } string GetRelativeDate(DateTime givenDate) { const int second = 1; const int minute = 60 * second; const int hour = 60 * minute; const int day = 24 * hour; const int month = 30 * day; var ts = new TimeSpan(DateTime.UtcNow.Ticks - givenDate.Ticks); var delta = Math.Abs(ts.TotalSeconds); if (delta < 24 * hour) return Translations.About.AboutToday; if (delta < 48 * hour) return Translations.About.AboutYesterday; if (delta < 30 * day) return string.Format(Translations.About.AboutDaysAgo, ts.Days); if (delta < 12 * month) { var months = Convert.ToInt32(Math.Floor((double)ts.Days / 30)); return months <= 1 ? Translations.About.AboutOneMonthAgo : string.Format(Translations.About.AboutMonthsAgo, months); } var years = Convert.ToInt32(Math.Floor((double)ts.Days / 365)); return years <= 1 ? Translations.About.AboutOneYearAgo : string.Format(Translations.About.AboutYearsAgo, years); } void RenderVersion(ChangelogVersion version) { var tempTextString = ""; //version number EditorGUILayout.TextArea(version.versionNum, HotReloadWindowStyles.H1TitleStyle); //general info if (version.generalInfo != null) { EditorGUILayout.TextArea(version.generalInfo, HotReloadWindowStyles.H3TitleStyle); } //features if (version.features != null) { EditorGUILayout.TextArea(Translations.About.AboutFeatures, HotReloadWindowStyles.H2TitleStyle); tempTextString = ""; foreach (var feature in version.features) { tempTextString += "• " + feature + "\n"; } EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle); } //improvements if (version.improvements != null) { EditorGUILayout.TextArea(Translations.About.AboutImprovements, HotReloadWindowStyles.H2TitleStyle); tempTextString = ""; foreach (var improvement in version.improvements) { tempTextString += "• " + improvement + "\n"; } EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle); } //fixes if (version.fixes != null) { EditorGUILayout.TextArea(Translations.About.AboutFixes, HotReloadWindowStyles.H2TitleStyle); tempTextString = ""; foreach (var fix in version.fixes) { tempTextString += "• " + fix + "\n"; } EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle); } //date DateTime date; if (DateTime.TryParseExact(version.date, "dd/MM/yyyy", null, DateTimeStyles.None, out date)) { var relativeDate = GetRelativeDate(date); GUILayout.TextArea(relativeDate, HotReloadWindowStyles.H3TitleStyle); } } void RenderChangelog() { FetchChangelog().Forget(); using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) { using (new EditorGUILayout.VerticalScope()) { HotReloadPrefs.ShowChangeLog = EditorGUILayout.Foldout(HotReloadPrefs.ShowChangeLog, Translations.Miscellaneous.ChangelogTitle, true, HotReloadWindowStyles.FoldoutStyle); if (!HotReloadPrefs.ShowChangeLog) { return; } // changelog versions var maxChangeLogs = 5; var index = 0; foreach (var version in currentState.changelog) { index++; if (index > maxChangeLogs) { break; } using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.ChangelogSectionInnerBox)) { using (new EditorGUILayout.VerticalScope()) { RenderVersion(version); } } } // see more button using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.ChangelogSectionInnerBox)) { seeMore.OnGUI(); } } } } private Vector2 _aboutTabScrollPos; HotReloadAboutTabState currentState; public override void OnGUI() { // HotReloadAboutTabState ensures rendering is consistent between Layout and Repaint calls // Without it errors like this happen: // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html if (Event.current.type == EventType.Layout) { currentState = new HotReloadAboutTabState( logsFodlerExists: Directory.Exists(logsPath), changelog: _changelog, loginRequired: EditorCodePatcher.LoginNotRequired, hasTrialLicense: _window.RunTab.TrialLicense, hasPayedLicense: _window.RunTab.HasPayedLicense ); } 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)))) { _aboutTabScrollPos.x = scope.scrollPosition.x; _aboutTabScrollPos.y = scope.scrollPosition.y; using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamicSectionHelpTab)) { using (new EditorGUILayout.VerticalScope()) { GUILayout.Space(10); RenderLogButtons(); EditorGUILayout.Space(); EditorGUILayout.HelpBox(string.Format(Translations.About.AboutVersionInfo, PackageConst.Version), MessageType.Info); EditorGUILayout.Space(); RenderHelpButtons(); GUILayout.Space(15); try { RenderChangelog(); } catch { // ignore } } } } } void RenderHelpButtons() { var labelRect = GUILayoutUtility.GetLastRect(); using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUILayout.VerticalScope()) { var buttonHeight = 19; var bigButtonRect = new Rect(labelRect.x + 3, labelRect.y + 5, labelRect.width - 6, buttonHeight); OpenURLButton.RenderRaw(bigButtonRect, Translations.About.ButtonDocumentation, Constants.DocumentationURL, HotReloadWindowStyles.HelpTabButton); var firstLayerX = bigButtonRect.x; var firstLayerY = bigButtonRect.y + buttonHeight + 3; var firstLayerWidth = (int)((bigButtonRect.width / 2) - 3); var secondLayerX = firstLayerX + firstLayerWidth + 5; var secondLayerY = firstLayerY + buttonHeight + 3; var secondLayerWidth = bigButtonRect.width - firstLayerWidth - 5; using (new EditorGUILayout.HorizontalScope()) { OpenURLButton.RenderRaw(new Rect { x = firstLayerX, y = firstLayerY, width = firstLayerWidth, height = buttonHeight }, contactButton.text, contactButton.url, HotReloadWindowStyles.HelpTabButton); OpenURLButton.RenderRaw(new Rect { x = secondLayerX, y = firstLayerY, width = secondLayerWidth, height = buttonHeight }, Translations.About.ButtonUnityForum, Constants.ForumURL, HotReloadWindowStyles.HelpTabButton); } using (new EditorGUILayout.HorizontalScope()) { 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); OpenURLButton.RenderRaw(new Rect { x = secondLayerX, y = secondLayerY, width = secondLayerWidth, height = buttonHeight }, discordButton.text, discordButton.url, HotReloadWindowStyles.HelpTabButton); } } } GUILayout.Space(80); } void RenderLogButtons() { if (currentState.logsFodlerExists) { EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button(Translations.Common.ButtonOpenLogFile)) { var mostRecentFile = LogsHelper.FindRecentLog(logsPath); if (mostRecentFile == null) { Log.Info(Translations.About.LogNoLogsFound); } else { try { Process.Start($"\"{Path.Combine(logsPath, mostRecentFile)}\""); } catch (Win32Exception e) { // TODO: is this the same for chinese? if (e.Message.Contains("Application not found")) { try { Process.Start("notepad.exe", $"\"{Path.Combine(logsPath, mostRecentFile)}\""); } catch { // Fallback to opening folder with all logs Process.Start($"\"{logsPath}\""); Log.Info(Translations.About.LogFailedOpeningLogFile); } } } catch { // Fallback to opening folder with all logs Process.Start($"\"{logsPath}\""); Log.Info(Translations.About.LogFailedOpeningLogFile); } } } if (GUILayout.Button(Translations.Common.ButtonBrowseAllLogs)) { Process.Start($"\"{logsPath}\""); } EditorGUILayout.EndHorizontal(); } } } }