diff --git a/ZuneModdingHelper/App.xaml.cs b/ZuneModdingHelper/App.xaml.cs index fcc6665..cea21ad 100644 --- a/ZuneModdingHelper/App.xaml.cs +++ b/ZuneModdingHelper/App.xaml.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows; +using System.Windows; using Microsoft.AppCenter; using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Crashes; @@ -13,9 +12,8 @@ namespace ZuneModdingHelper { public const string Title = "Zune Modding Helper"; - public static readonly Version VersionNum = new(2021, 10, 23, 0); - public const string VersionStatus = "alpha"; - public static readonly string Version = VersionNum.ToString() + (VersionStatus != string.Empty ? "-" + VersionStatus : string.Empty); + public static readonly ReleaseVersion Version = new(2021, 11, 14, 0, Phase.Alpha); + public static readonly string VersionStr = Version.ToString(); public const string DonateLink = "https://www.paypal.me/YoshiAsk"; @@ -40,17 +38,5 @@ namespace ZuneModdingHelper UseShellExecute = true }); } - - public static bool CheckIfNewerVersion(string otherStr) - { - int idxSplit = otherStr.IndexOf('-'); - Version otherNum = new(otherStr[..idxSplit]); - string otherStatus = otherStr[(idxSplit + 1)..]; - - // TODO: This assumes that the VersionStatus is "alpha" - bool isNotAlpha = otherStatus != VersionStatus; - bool isNewer = otherNum > VersionNum; - return isNotAlpha || isNewer; - } } } diff --git a/ZuneModdingHelper/MainWindow.xaml b/ZuneModdingHelper/MainWindow.xaml index 82045ab..32c1780 100644 --- a/ZuneModdingHelper/MainWindow.xaml +++ b/ZuneModdingHelper/MainWindow.xaml @@ -34,7 +34,7 @@ - + diff --git a/ZuneModdingHelper/MainWindow.xaml.cs b/ZuneModdingHelper/MainWindow.xaml.cs index 406671a..6c4268d 100644 --- a/ZuneModdingHelper/MainWindow.xaml.cs +++ b/ZuneModdingHelper/MainWindow.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.AppCenter.Analytics; using Microsoft.WindowsAPICodePack.Dialogs; using Newtonsoft.Json.Linq; using OwlCore.AbstractUI.Models; +using Syroot.Windows.IO; using System; using System.Collections.Generic; using System.IO; @@ -166,10 +167,11 @@ namespace ZuneModdingHelper { // Get releases list from GitHub List releases = await "https://api.github.com/repos/ZuneDev/ZuneModdingHelper/releases" - .WithHeader("User-Agent", App.Title.Replace(" ", "") + "/" + App.Version) + .WithHeader("User-Agent", App.Title.Replace(" ", "") + "/" + App.VersionStr) .GetJsonAsync>(); JObject latest = releases[0]; - if (!App.CheckIfNewerVersion(latest["tag_name"].Value())) + string latestVerStr = latest["tag_name"].Value(); + if (!ReleaseVersion.TryParse(latestVerStr, out var latestVer) || App.Version >= latestVer) { // Already up-to-date @@ -192,7 +194,7 @@ namespace ZuneModdingHelper NegativeButtonText = "Later" }; await checkDialog.CloseAsync(); - var promptResult = await this.ShowMessageAsync("Update available", $"Relase {latest["name"]} is available. Would you like to download it now?", + var promptResult = await this.ShowMessageAsync("Update available", $"Release {latest["name"]} is available. Would you like to download it now?", MessageDialogStyle.AffirmativeAndNegative, promptSettings); bool acceptedUpdate = promptResult == MessageDialogResult.Affirmative; @@ -221,7 +223,8 @@ namespace ZuneModdingHelper // Ask user to save file Microsoft.Win32.SaveFileDialog saveFileDialog = new() { - FileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads", assetName) + FileName = assetName, + InitialDirectory = new KnownFolder(KnownFolderType.Downloads).Path }; bool dialogResult = saveFileDialog.ShowDialog() ?? false; await progDialog.CloseAsync(); @@ -240,6 +243,9 @@ namespace ZuneModdingHelper catch { App.OpenInBrowser("https://github.com/ZuneDev/ZuneModdingHelper/releases"); + } + finally + { if (checkDialog.IsOpen) await checkDialog.CloseAsync(); } @@ -266,7 +272,7 @@ namespace ZuneModdingHelper { // TODO: Fallback to pre-Vista dialog this.ShowMessageAsync("Error", - "Please use File Explorer to locate an copy the path.", + "Please use File Explorer to locate and copy the path.", settings: defaultMetroDialogSettings); } } diff --git a/ZuneModdingHelper/ReleaseVersion.cs b/ZuneModdingHelper/ReleaseVersion.cs new file mode 100644 index 0000000..26dd726 --- /dev/null +++ b/ZuneModdingHelper/ReleaseVersion.cs @@ -0,0 +1,484 @@ +using CommunityToolkit.Diagnostics; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace ZuneModdingHelper +{ + public sealed class ReleaseVersion : ICloneable, IComparable, IComparable, IEquatable + { + /// + /// Gets the value of the major component of the version number for the current object. + /// + public int Major { get; set; } + + /// + /// Gets the value of the minor component of the version number for the current object. + /// + public int Minor { get; set; } + + /// + /// Gets the value of the build component of the version number for the current object. + /// + public int Build { get; set; } + + /// + /// Gets the value of the revision component of the version number for the current object. + /// + public int Revision { get; set; } + + private Phase _Phase = Phase.Unknown; + /// + /// Gets the value of the release phase component of the version number for the current object. + /// + public Phase Phase + { + set => _Phase = value; + get + { + if (_Phase == Phase.Unknown) + { +#if DEBUG + _Phase = Phase.Debug; +#else + _Phase = Phase.Production; +#endif + } + return _Phase; + } + } + + /// + /// Initializes a new instance of the class. + /// + public ReleaseVersion() + { + } + + /// + /// Initializes a new instance of the class with the specified major, + /// minor, build, revision, and release phase. + /// + /// + /// The major version number. + /// + /// + /// The minor version number. + /// + /// + /// The build version number. + /// + /// + /// The revision version number. + /// + /// + /// The release phase. + /// + /// + /// , , , or is less than zero. + /// + public ReleaseVersion(int major, int minor, int build = 0, int revision = 0, Phase phase = Phase.Unknown) + { + Guard.IsGreaterThanOrEqualTo(major, 0, nameof(major)); + Guard.IsGreaterThanOrEqualTo(minor, 0, nameof(minor)); + Guard.IsGreaterThanOrEqualTo(build, 0, nameof(build)); + Guard.IsGreaterThanOrEqualTo(revision, 0, nameof(revision)); + + Major = major; + Minor = minor; + Build = build; + Revision = revision; + Phase = phase; + } + + /// + /// Returns a new object whose value is the same as the current + /// object. + /// + /// + /// A new whose values are a copy of the current object. + /// + public object Clone() + { + return new ReleaseVersion(Major, Minor, Build, Revision, Phase); + } + + /// + /// Compares the current object to a specified object and returns + /// an indication of their relative values. + /// + /// + /// An object to compare, or null. + /// + /// + /// A signed integer that indicates the relative values of the two objects, as shown + /// in the following table. + /// Return value – Meaning + /// Less than zero – The current object is a version before . + /// Zero – The current object is the same version as . + /// Greater than zero – The current object is a version subsequent + /// to , or is null. + /// + /// + /// is not of type . + /// + public int CompareTo(object? version) + { + if (version == null) + { + return 1; + } + + if (version is ReleaseVersion v) + { + return CompareTo(v); + } + + throw new ArgumentException(nameof(version) + " must be of type " + nameof(ReleaseVersion)); + } + + /// + /// Compares the current object to a specified object + /// and returns an indication of their relative values. + /// + /// + /// A object to compare to the current object, or null. + /// + /// + /// A signed integer that indicates the relative values of the two objects, as shown + /// in the following table. + /// Return value – Meaning + /// Less than zero – The current object is a version before . + /// Zero – The current object is the same version as . + /// Greater than zero – The current object is a version subsequent + /// to , or is null. + /// + public int CompareTo(ReleaseVersion? value) + { + return + ReferenceEquals(value, this) ? 0 : + value is null ? 1 : + Major != value.Major ? (Major > value.Major ? 1 : -1) : + Minor != value.Minor ? (Minor > value.Minor ? 1 : -1) : + Build != value.Build ? (Build > value.Build ? 1 : -1) : + Revision != value.Revision ? (Revision > value.Revision ? 1 : -1) : + Phase != value.Phase? ((byte)Phase > (byte)value.Phase ? 1 : -1) : + 0; + } + + /// + /// Returns a value indicating whether the current object is equal + /// to a specified object. + /// + /// + /// An object to compare with the current object, or null. + /// + /// + /// true if the current object and + /// are both objects, + /// and every component of the current object matches the corresponding + /// component of ; otherwise, false. + /// + public override bool Equals(object? obj) + { + return Equals(obj as ReleaseVersion); + } + + /// + /// Returns a value indicating whether the current object and a specified + /// object represent the same value. + /// + /// + /// A object to compare to the current object, or null. + /// + /// + /// true if every component of the current object matches the corresponding + /// component of the parameter; otherwise, false. + /// + public bool Equals(ReleaseVersion? obj) + { + return ReferenceEquals(obj, this) || + (obj is not null && + Major == obj.Major && + Minor == obj.Minor && + Build == obj.Build && + Revision == obj.Revision && + Phase == obj.Phase); + } + + /// + /// Returns a hash code for the current object. + /// + /// + /// A 32-bit signed integer hash code. + /// + public override int GetHashCode() + { + return HashCode.Combine(Major, Minor, Build, Revision, Phase); + } + + /// + /// Determines whether two specified objects are equal. + /// + /// + /// The first object. + /// + /// + /// The second object. + /// + /// + /// true if v1 equals v2; otherwise, false. + /// + // Force inline as the true/false ternary takes it above ALWAYS_INLINE size even though the asm ends up smaller + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ReleaseVersion? v1, ReleaseVersion? v2) + { + // Test "right" first to allow branch elimination when inlined for null checks (== null) + // so it can become a simple test + if (v2 is null) + { + // return true/false not the test result https://github.com/dotnet/runtime/issues/4207 + return (v1 is null) ? true : false; + } + + // Quick reference equality test prior to calling the virtual Equality + return ReferenceEquals(v2, v1) ? true : v2.Equals(v1); + } + + /// + /// Determines whether the first specified object is greater than + /// the second specified object. + /// + /// + /// The first object. + /// + /// + /// The second object. + /// + /// + /// true if v1 is greater than v2; otherwise, false. + /// + public static bool operator >(ReleaseVersion? v1, ReleaseVersion? v2) + => v2 < v1; + + /// + /// Determines whether the first specified object is greater than + /// or equal to the second specified object. + /// + /// + /// The first object. + /// + /// + /// The second object. + /// + /// + /// true if v1 is greater than or equal to v2; otherwise, false. + /// + public static bool operator >=(ReleaseVersion? v1, ReleaseVersion? v2) + => v2 <= v1; + + /// + /// Determines whether two specified objects are not equal. + /// + /// + /// The first object. + /// + /// + /// The second object. + /// + /// + /// true if v1 does not equal v2; otherwise, false. + /// + public static bool operator !=(ReleaseVersion? v1, ReleaseVersion? v2) + => !(v1 == v2); + + /// + /// Determines whether the first specified object is less than the + /// second specified object. + /// + /// + /// The first object. + /// + /// + /// The second object. + /// + /// + /// true if v1 is less than v2; otherwise, false. + /// + public static bool operator <(ReleaseVersion? v1, ReleaseVersion? v2) + { + if (v1 is null) + { + return !(v2 is null); + } + + return v1.CompareTo(v2) < 0; + } + + /// + /// Determines whether the first specified object is less than or + /// equal to the second object. + /// + /// + /// The first object. + /// + /// + /// The second object. + /// + /// + /// true if v1 is less than or equal to v2; otherwise, false. + /// + public static bool operator <=(ReleaseVersion? v1, ReleaseVersion? v2) + { + if (v1 is null) + { + return true; + } + + return v1.CompareTo(v2) <= 0; + } + + /// + /// Converts the string representation of a version number to an equivalent + /// object. + /// + /// + /// A string containing the major, minor, build, and revision numbers, where each + /// number is delimited with a period character ('.'), followed by a dash character + /// ('-') and the release phase. + /// + /// + /// An object that is equivalent to the version number specified in the input parameter. + /// + public static ReleaseVersion Parse(string input) + { + ReleaseVersion version = new(); + string versionPart = input; + int phaseIdx = input.IndexOf('-'); + if (phaseIdx != -1) + { + versionPart = input[..phaseIdx]; + + string phaseComp = input[(phaseIdx + 1)..]; + if (phaseComp.ToLowerInvariant() == "rc") + version.Phase = Phase.ReleaseCandidate; + else + version.Phase = Enum.Parse(phaseComp, true); + } + + string[] components = versionPart.Split('.'); + Guard.IsGreaterThanOrEqualTo(components.Length, 2, nameof(components)); + + if (components.Length >= 2) + { + version.Major = int.Parse(components[0]); + version.Minor = int.Parse(components[1]); + + + if (components.Length >= 3) + { + version.Build = int.Parse(components[2]); + + + if (components.Length >= 4) + { + version.Revision = int.Parse(components[3]); + } + } + } + + return version; + } + + /// + /// Converts the value of the current object to its equivalent + /// representation. + /// + /// + /// The string representation of the values of the major, minor, build, and + /// revision components of the current object, as depicted in the + /// following format. Each component is separated by a period character ('.'), + /// and the release phase is appended to the end after a dash character ('-'). Square + /// brackets ('[' and ']') indicate a component that will not appear in the return + /// value if the component is not defined: major.minor[.build[.revision]][-phase] For example, + /// if you create a object using the constructor Version(1, 1), the + /// returned string is "1.1". If you create a object using the constructor + /// Version(1, 3, 4, 2, Phase.Beta), the returned string is "1.3.4.2-beta". + /// + public override string ToString() => ToString(true); + + /// + /// + /// When set to true, the string will be formatted as major.minor.build.revision-phase, + /// even if an optional component is not present. + /// + public string ToString(bool includeAllComponents) + { + const char numDelim = '.'; + const char phaseDelim = '-'; + string output = Major.ToString() + numDelim + Minor.ToString(); + + if (Build > 0 || includeAllComponents) + { + output += numDelim + Build.ToString(); + + if (Revision > 0 || includeAllComponents) + { + output += numDelim + Revision.ToString(); + } + } + + if ((Phase != Phase.Unknown && Phase != Phase.Production) || includeAllComponents) + { + output += phaseDelim + (Phase == Phase.ReleaseCandidate ? "rc" : Phase.ToString().ToLowerInvariant()); + } + + return output; + } + + /// + /// Tries to convert the string representation of a version number to an equivalent + /// object, and returns a value that indicates whether the conversion + /// succeeded. + /// + /// + /// A string that contains a version number to convert. + /// + /// + /// When this method returns, contains the equivalent of the number + /// that is contained in input, if the conversion succeeded. If input is null, , + /// or if the conversion fails, result is null when the method returns. + /// + /// + /// true if the input parameter was converted successfully; otherwise, false. + /// + public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out ReleaseVersion? result) + { + try + { + Guard.IsNotNull(input, nameof(input)); + result = Parse(input); + return true; + } + catch + { + result = null; + return false; + } + } + } + + public enum Phase : byte + { + Unknown, + Debug, + Alpha, + Beta, + ReleaseCandidate, + Production + } +}