-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add VPM update notification #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| using System; | ||
| using System.Collections; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using UnityEditor; | ||
| using UnityEngine; | ||
|
|
||
| namespace Net._32ba.GitTool.UpdateNotification | ||
| { | ||
| [InitializeOnLoad] | ||
| internal static class ReleaseChecker | ||
| { | ||
| private const string PackageId = "net.32ba.git-tool"; | ||
| private const string PackageDisplayName = "Git Tool"; | ||
| private const string ReleasePageUrl = "https://github.com/32ba/VRC-git-tool/releases"; | ||
| private const string LastCheckKeyPrefix = "net.32ba.git.tool.LastVersionCheck"; | ||
| private const double CheckIntervalHours = 24.0; | ||
|
|
||
| private static readonly VpmApiClient Api = new VpmApiClient(PackageId); | ||
|
|
||
| private static string LatestVersion { get; set; } | ||
| private static bool HasNewVersion { get; set; } | ||
| private static bool IsChecking { get; set; } | ||
| private static string CheckError { get; set; } | ||
|
|
||
| static ReleaseChecker() | ||
| { | ||
| EditorApplication.delayCall += () => CheckForUpdates(false); | ||
| } | ||
|
|
||
| private static void CheckForUpdates(bool forceCheck) | ||
| { | ||
| if (IsChecking) | ||
| return; | ||
|
|
||
| if (!forceCheck && !ShouldCheckForUpdates()) | ||
| return; | ||
|
|
||
| IsChecking = true; | ||
| HasNewVersion = false; | ||
| CheckError = null; | ||
|
|
||
| EditorCoroutine.Start(CheckRoutine(forceCheck)); | ||
| } | ||
|
|
||
| private static string GetCurrentVersion() | ||
| { | ||
| try | ||
| { | ||
| var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ReleaseChecker).Assembly); | ||
| if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.version)) | ||
| return packageInfo.version; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Debug.LogWarning($"[{PackageDisplayName}] Failed to read package version: {ex.Message}"); | ||
| } | ||
|
|
||
| return "0.0.0"; | ||
| } | ||
|
|
||
| private static IEnumerator CheckRoutine(bool forceCheck) | ||
| { | ||
| yield return Api.GetLatestVersionCoroutine( | ||
| latest => HandleSuccess(latest, forceCheck), | ||
| error => HandleError(error, forceCheck)); | ||
| } | ||
|
|
||
| private static void HandleSuccess(string latest, bool forceCheck) | ||
| { | ||
| IsChecking = false; | ||
|
|
||
| if (string.IsNullOrEmpty(latest)) | ||
| { | ||
| CheckError = "Empty version response"; | ||
| if (forceCheck) | ||
| EditorUtility.DisplayDialog($"{PackageDisplayName} Update Check", CheckError, "OK"); | ||
| return; | ||
| } | ||
|
|
||
| LatestVersion = latest; | ||
| var current = GetCurrentVersion(); | ||
| EditorPrefs.SetString(GetLastCheckKey(), DateTime.Now.ToBinary().ToString()); | ||
|
|
||
| if (VersionUtility.IsNewerVersion(current, latest)) | ||
| { | ||
| HasNewVersion = true; | ||
| Debug.Log($"[{PackageDisplayName}] New version available: {current} -> {latest}"); | ||
| ShowUpdateDialog(current, latest); | ||
| } | ||
| else if (forceCheck) | ||
| { | ||
| EditorUtility.DisplayDialog( | ||
| $"{PackageDisplayName} Update Check", | ||
| $"{PackageDisplayName} is up to date.\n\nCurrent: {VersionUtility.FormatVersion(current)}", | ||
| "OK"); | ||
| } | ||
| else | ||
| { | ||
| Debug.Log($"[{PackageDisplayName}] Package is up to date: {current}"); | ||
| } | ||
| } | ||
|
|
||
| private static void HandleError(string error, bool forceCheck) | ||
| { | ||
| IsChecking = false; | ||
| CheckError = error; | ||
| Debug.LogWarning($"[{PackageDisplayName}] Update check failed: {error}"); | ||
|
|
||
| if (forceCheck) | ||
| EditorUtility.DisplayDialog($"{PackageDisplayName} Update Check", $"Update check failed.\n\n{error}", "OK"); | ||
| } | ||
|
|
||
| private static void ShowUpdateDialog(string current, string latest) | ||
| { | ||
| if (EditorUtility.DisplayDialog( | ||
| $"{PackageDisplayName} Update Available", | ||
| $"A new version of {PackageDisplayName} is available.\n\n{VersionUtility.FormatVersion(current)} -> {VersionUtility.FormatVersion(latest)}", | ||
| "Open Release Page", | ||
| "Later")) | ||
| { | ||
| Application.OpenURL(ReleasePageUrl); | ||
| } | ||
| } | ||
|
|
||
| private static bool ShouldCheckForUpdates() | ||
| { | ||
| var stored = EditorPrefs.GetString(GetLastCheckKey(), ""); | ||
| if (string.IsNullOrEmpty(stored)) | ||
| return true; | ||
|
|
||
| if (long.TryParse(stored, out var binary)) | ||
| { | ||
| var last = DateTime.FromBinary(binary); | ||
| return (DateTime.Now - last).TotalHours >= CheckIntervalHours; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private static string GetLastCheckKey() | ||
| { | ||
| return $"{LastCheckKeyPrefix}.{GetProjectScopeSuffix()}"; | ||
| } | ||
|
|
||
| private static string GetProjectScopeSuffix() | ||
| { | ||
| var projectPath = Application.dataPath; | ||
| if (string.IsNullOrEmpty(projectPath)) | ||
| return "unknown"; | ||
|
|
||
| using (var sha1 = SHA1.Create()) | ||
| { | ||
| var bytes = Encoding.UTF8.GetBytes(projectPath); | ||
| var hash = sha1.ComputeHash(bytes); | ||
| return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| internal sealed class EditorCoroutine | ||
| { | ||
| private readonly IEnumerator _routine; | ||
| private IEnumerator _nested; | ||
|
|
||
| public static EditorCoroutine Start(IEnumerator routine) | ||
| { | ||
| var coroutine = new EditorCoroutine(routine); | ||
| EditorApplication.update += coroutine.Update; | ||
| return coroutine; | ||
| } | ||
|
|
||
| private EditorCoroutine(IEnumerator routine) | ||
| { | ||
| _routine = routine; | ||
| } | ||
|
|
||
| private void Update() | ||
| { | ||
| if (_nested != null) | ||
| { | ||
| if (_nested.MoveNext()) | ||
| return; | ||
| _nested = null; | ||
| } | ||
|
|
||
| if (!_routine.MoveNext()) | ||
| { | ||
| EditorApplication.update -= Update; | ||
| return; | ||
| } | ||
|
|
||
| if (_routine.Current is IEnumerator nestedRoutine) | ||
| _nested = nestedRoutine; | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| using System; | ||
| using System.Text.RegularExpressions; | ||
| using UnityEngine; | ||
|
|
||
| namespace Net._32ba.GitTool.UpdateNotification | ||
| { | ||
| internal static class VersionUtility | ||
| { | ||
| public static bool IsNewerVersion(string currentVersion, string latestVersion) | ||
| { | ||
| if (string.IsNullOrEmpty(currentVersion) || string.IsNullOrEmpty(latestVersion)) | ||
| return false; | ||
|
|
||
| try | ||
| { | ||
| return ParseVersion(latestVersion) > ParseVersion(currentVersion); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Debug.LogWarning($"[Git Tool] Failed to compare versions '{currentVersion}' and '{latestVersion}': {ex.Message}"); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| public static string FormatVersion(string version) | ||
| { | ||
| if (string.IsNullOrEmpty(version)) | ||
| return "Unknown"; | ||
|
|
||
| return version.StartsWith("v", StringComparison.OrdinalIgnoreCase) ? version : "v" + version; | ||
| } | ||
|
|
||
| private static Version ParseVersion(string versionString) | ||
| { | ||
| var clean = versionString.TrimStart('v', 'V'); | ||
|
|
||
| var match = Regex.Match(clean, @"^(\d+)\.(\d+)\.(\d+)"); | ||
| if (match.Success) | ||
| { | ||
| return new Version( | ||
| int.Parse(match.Groups[1].Value), | ||
| int.Parse(match.Groups[2].Value), | ||
| int.Parse(match.Groups[3].Value)); | ||
| } | ||
|
|
||
| match = Regex.Match(clean, @"^(\d+)\.(\d+)"); | ||
| if (match.Success) | ||
| { | ||
| return new Version( | ||
| int.Parse(match.Groups[1].Value), | ||
| int.Parse(match.Groups[2].Value), | ||
| 0); | ||
| } | ||
|
|
||
| return new Version(clean); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| using System; | ||
| using System.Collections; | ||
| using System.Text; | ||
| using UnityEngine; | ||
| using UnityEngine.Networking; | ||
|
|
||
| namespace Net._32ba.GitTool.UpdateNotification | ||
| { | ||
| internal sealed class VpmApiClient | ||
| { | ||
| private const string ApiBaseUrl = "https://vpm.32ba.net/api/packages"; | ||
| private const int RequestTimeoutSeconds = 10; | ||
| private readonly string _packageId; | ||
|
|
||
| public VpmApiClient(string packageId) | ||
| { | ||
| _packageId = packageId; | ||
| } | ||
|
|
||
| public IEnumerator GetLatestVersionCoroutine(Action<string> onComplete, Action<string> onError = null) | ||
| { | ||
| var url = $"{ApiBaseUrl}/{_packageId}/latest/version"; | ||
|
|
||
| using (var request = UnityWebRequest.Get(url)) | ||
| { | ||
| request.timeout = RequestTimeoutSeconds; | ||
| request.SetRequestHeader("User-Agent", $"32ba-UnityUpdateChecker/{Application.unityVersion}"); | ||
| yield return request.SendWebRequest(); | ||
|
|
||
| if (request.result == UnityWebRequest.Result.Success) | ||
| onComplete?.Invoke(request.downloadHandler.text.Trim()); | ||
| else | ||
| onError?.Invoke(BuildErrorMessage(request, url)); | ||
| } | ||
| } | ||
|
|
||
| private static string BuildErrorMessage(UnityWebRequest request, string url) | ||
| { | ||
| var parts = new StringBuilder(); | ||
| parts.Append("VPM API request failed"); | ||
| parts.Append($" ({request.result})"); | ||
|
|
||
| if (!string.IsNullOrEmpty(request.error)) | ||
| parts.Append($": {request.error}"); | ||
|
|
||
| if (request.responseCode > 0) | ||
| parts.Append($" [HTTP {request.responseCode}]"); | ||
|
|
||
| parts.Append($" URL={url}"); | ||
|
|
||
| var responseText = request.downloadHandler != null ? request.downloadHandler.text : null; | ||
| if (!string.IsNullOrWhiteSpace(responseText)) | ||
| { | ||
| responseText = responseText.Trim(); | ||
| if (responseText.Length > 200) | ||
| responseText = responseText.Substring(0, 200) + "..."; | ||
| parts.Append($" Response={responseText}"); | ||
| } | ||
|
|
||
| return parts.ToString(); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EditorCoroutine.Updateonly pauses for yielded values that implementIEnumerator, butGetLatestVersionCoroutineyieldsrequest.SendWebRequest()(aUnityWebRequestAsyncOperation). Because that yield is ignored,_routine.MoveNext()runs again on the next editor tick before the request completes, sorequest.resultis often stillInProgressand the code reports a failed update check even when the network call would succeed. This makes automatic/manual update checks unreliable in normal latency conditions.Useful? React with 👍 / 👎.