@@ -10,13 +10,187 @@ namespace Menu
1010 static std::vector<std::unique_ptr<ITab>> s_Tabs;
1111 static bool s_Initialized = false ;
1212
13+ static std::atomic s_UpdateCheckFinished = false ;
14+ static bool s_HasUpdate = false ;
15+ static std::string s_LatestVersion = " " ;
16+ static std::string s_ReleaseUrl = " " ;
17+ static std::string s_UpdateError = " " ;
18+
19+ static std::string FetchLatestReleaseJson (std::string& outError)
20+ {
21+ std::string responseData;
22+ HINTERNET hSession = nullptr , hConnect = nullptr , hRequest = nullptr ;
23+
24+ hSession = WinHttpOpen (L" UnityInspectorStandalone/1.0" ,
25+ WINHTTP_ACCESS_TYPE_DEFAULT_PROXY ,
26+ nullptr ,
27+ nullptr , 0 );
28+ if (!hSession)
29+ {
30+ outError = " WinHttpOpen failed: " + std::to_string (GetLastError ());
31+ return " " ;
32+ }
33+
34+ WinHttpSetTimeouts (hSession, 5000 , 5000 , 5000 , 5000 );
35+
36+ hConnect = WinHttpConnect (hSession, L" api.github.com" , INTERNET_DEFAULT_HTTPS_PORT , 0 );
37+ if (!hConnect)
38+ {
39+ outError = " WinHttpConnect failed: " + std::to_string (GetLastError ());
40+ WinHttpCloseHandle (hSession);
41+ return " " ;
42+ }
43+
44+ hRequest = WinHttpOpenRequest (hConnect, L" GET" , L" /repos/PicoShot/UnityInspectorStandalone/releases/latest" ,
45+ nullptr , nullptr ,
46+ nullptr ,
47+ WINHTTP_FLAG_SECURE );
48+ if (!hRequest)
49+ {
50+ outError = " WinHttpOpenRequest failed: " + std::to_string (GetLastError ());
51+ WinHttpCloseHandle (hConnect);
52+ WinHttpCloseHandle (hSession);
53+ return " " ;
54+ }
55+
56+ BOOL bResults = WinHttpSendRequest (hRequest,
57+ nullptr , 0 ,
58+ nullptr , 0 ,
59+ 0 , 0 );
60+
61+ if (!bResults)
62+ {
63+ outError = " WinHttpSendRequest failed: " + std::to_string (GetLastError ());
64+ }
65+ else
66+ {
67+ bResults = WinHttpReceiveResponse (hRequest, nullptr );
68+ if (!bResults)
69+ {
70+ outError = " WinHttpReceiveResponse failed: " + std::to_string (GetLastError ());
71+ }
72+ }
73+
74+ if (bResults)
75+ {
76+ DWORD dwSize = 0 ;
77+ do
78+ {
79+ dwSize = 0 ;
80+ if (!WinHttpQueryDataAvailable (hRequest, &dwSize))
81+ {
82+ outError = " WinHttpQueryDataAvailable failed: " + std::to_string (GetLastError ());
83+ break ;
84+ }
85+ if (dwSize == 0 ) break ;
86+
87+ std::vector<char > tempBuffer (dwSize);
88+ DWORD dwDownloaded = 0 ;
89+ if (!WinHttpReadData (hRequest, tempBuffer.data (), dwSize, &dwDownloaded))
90+ {
91+ outError = " WinHttpReadData failed: " + std::to_string (GetLastError ());
92+ break ;
93+ }
94+ responseData.append (tempBuffer.data (), dwDownloaded);
95+ }
96+ while (dwSize > 0 );
97+ }
98+
99+ if (hRequest) WinHttpCloseHandle (hRequest);
100+ if (hConnect) WinHttpCloseHandle (hConnect);
101+ if (hSession) WinHttpCloseHandle (hSession);
102+
103+ return responseData;
104+ }
105+
106+ static std::vector<int > ParseVersion (const std::string& verStr)
107+ {
108+ std::vector<int > parts;
109+ std::string current;
110+ for (char c : verStr)
111+ {
112+ if (std::isdigit (static_cast <unsigned char >(c)))
113+ {
114+ current += c;
115+ }
116+ else if (c == ' .' || c == ' -' )
117+ {
118+ if (!current.empty ())
119+ {
120+ parts.push_back (std::stoi (current));
121+ current.clear ();
122+ }
123+ if (c == ' -' ) break ;
124+ }
125+ }
126+ if (!current.empty ())
127+ {
128+ parts.push_back (std::stoi (current));
129+ }
130+ return parts;
131+ }
132+
133+ static bool IsNewerVersion (const std::string& latest, const std::string& current)
134+ {
135+ auto latest_parts = ParseVersion (latest);
136+ auto current_parts = ParseVersion (current);
137+ for (size_t i = 0 ; i < std::max (latest_parts.size (), current_parts.size ()); ++i)
138+ {
139+ int l = (i < latest_parts.size ()) ? latest_parts[i] : 0 ;
140+ int c = (i < current_parts.size ()) ? current_parts[i] : 0 ;
141+ if (l > c) return true ;
142+ if (l < c) return false ;
143+ }
144+ return false ;
145+ }
146+
13147 void Init ()
14148 {
15149 if (s_Initialized) return ;
16150 s_Tabs.push_back (std::make_unique<DebugTab>());
17151 s_Tabs.push_back (std::make_unique<LuaConsoleTab>());
18152 s_Tabs.push_back (std::make_unique<MiscTab>());
19153 s_Initialized = true ;
154+
155+ std::thread ([]
156+ {
157+ std::string err;
158+ std::string jsonStr = FetchLatestReleaseJson (err);
159+ if (!err.empty ())
160+ {
161+ s_UpdateError = err;
162+ s_UpdateCheckFinished = true ;
163+ return ;
164+ }
165+
166+ try
167+ {
168+ if (Json json = Json::parse (jsonStr); json.contains (" tag_name" ) && json[" tag_name" ].is_string ())
169+ {
170+ std::string latestTag = json[" tag_name" ];
171+ s_LatestVersion = latestTag;
172+
173+ if (json.contains (" html_url" ) && json[" html_url" ].is_string ())
174+ {
175+ s_ReleaseUrl = json[" html_url" ];
176+ }
177+ else
178+ {
179+ s_ReleaseUrl = " https://github.com/PicoShot/UnityInspectorStandalone/releases" ;
180+ }
181+
182+ if (IsNewerVersion (latestTag, VERSION ))
183+ {
184+ s_HasUpdate = true ;
185+ }
186+ }
187+ }
188+ catch (const std::exception& e)
189+ {
190+ s_UpdateError = std::string (" JSON parse error: " ) + e.what ();
191+ }
192+ s_UpdateCheckFinished = true ;
193+ }).detach ();
20194 }
21195
22196 void Render ()
@@ -28,15 +202,41 @@ namespace Menu
28202 ImGui::SetNextWindowSize (ImVec2 (550 , 350 ), ImGuiCond_FirstUseEver);
29203 ImGui::SetNextWindowPos (ImVec2 (600 , 400 ), ImGuiCond_FirstUseEver);
30204
205+ std::string menuTitle = " UnityInspector v" VERSION ;
31206#ifdef _DEBUG
32- const auto menuTitle = " UnityInspector (Debug)" ;
33- #else
34- const auto menuTitle = " UnityInspector (Release)" ;
207+ menuTitle += " (Debug)" ;
35208#endif
36209
37- ImGui::Begin (menuTitle, &Config::state.showMenu ,
210+ ImGui::Begin (menuTitle. c_str () , &Config::state.showMenu ,
38211 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
39212
213+ if (s_UpdateCheckFinished && s_HasUpdate)
214+ {
215+ ImGui::PushStyleColor (ImGuiCol_ChildBg, ImVec4 (0 .26f , 0 .59f , 0 .98f , 0 .20f ));
216+ ImGui::PushStyleVar (ImGuiStyleVar_ChildRounding, 4 .0f );
217+ ImGui::PushStyleVar (ImGuiStyleVar_WindowPadding, ImVec2 (8 .0f , 6 .0f ));
218+
219+ if (ImGui::BeginChild (" UpdateBanner" , ImVec2 (0 , 34 ),
220+ ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding))
221+ {
222+ ImGui::AlignTextToFramePadding ();
223+ ImGui::Text (" A new version (%s) is available!" , s_LatestVersion.c_str ());
224+ ImGui::SameLine ();
225+
226+ float button_width = 80 .0f ;
227+ ImGui::SetCursorPosX (ImGui::GetWindowWidth () - button_width - 8 .0f );
228+ if (ImGui::Button (" Update" , ImVec2 (button_width, 0 )))
229+ {
230+ ShellExecuteA (nullptr , " open" , s_ReleaseUrl.c_str (), nullptr , nullptr , SW_SHOWNORMAL );
231+ }
232+ }
233+ ImGui::EndChild ();
234+
235+ ImGui::PopStyleVar (2 );
236+ ImGui::PopStyleColor ();
237+ ImGui::Spacing ();
238+ }
239+
40240 if (ImGui::BeginTabBar (" MainTabs" , ImGuiTabBarFlags_NoCloseWithMiddleMouseButton))
41241 {
42242 for (const auto & tab : s_Tabs)
0 commit comments