Seregon/ShadPKG

A tool for deriving PKG packet encryption keys for ps4 written in c++

C++/47.3 KB/No license
gui/views/SettingsView.cpp
ShadPKG / gui / views / SettingsView.cpp
1// SPDX-FileCopyrightText: Copyright 2025 shadPKG
2// SPDX-License-Identifier: GPL-2.0-or-later
3 
4#include "../include/views/SettingsView.h"
5#include "../include/GuiLogSink.h"
6#include "../include/StyleManager.h"
7#include "common/assert.h"
8#include "common/version.h"
9 
10// STL includes for stb_image
11#include <cassert>
12#include <cstdio>
13#include <cstdlib> // malloc, free, etc
14#include <cstring>
15 
16#ifndef IM_ASSERT
17#define IM_ASSERT(_EXPR) ASSERT(_EXPR)
18#endif
19#include "../include/IconsFontAwesome6.h"
20#include "imgui.h"
21 
22// Define stb_image implementation
23#define STB_IMAGE_IMPLEMENTATION
24#define STBI_ASSERT(x) (void)(0)
25#include "../include/stb_image.h"
26 
27#if defined(__APPLE__)
28#include <OpenGL/gl3.h>
29#else
30#include <GL/gl.h>
31#endif
32 
33namespace ShadPKG::GUI {
34 
35// Global texture state for Avatar
36static GLuint g_AvatarTexture = 0;
37static int g_AvatarWidth = 0;
38static int g_AvatarHeight = 0;
39static bool g_AvatarLoaded = false;
40 
41// Helper to load texture
42static bool LoadTextureFromFile(const char *filename, GLuint *out_texture,
43 int *out_width, int *out_height) {
44 int image_width = 0;
45 int image_height = 0;
46 unsigned char *image_data =
47 stbi_load(filename, &image_width, &image_height, NULL, 4);
48 if (image_data == NULL)
49 return false;
50 
51 GLuint image_texture;
52 glGenTextures(1, &image_texture);
53 glBindTexture(GL_TEXTURE_2D, image_texture);
54 
55 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
56 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
57 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
58 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
59 
60#if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
61 glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
62#endif
63 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0, GL_RGBA,
64 GL_UNSIGNED_BYTE, image_data);
65 stbi_image_free(image_data);
66 
67 *out_texture = image_texture;
68 *out_width = image_width;
69 *out_height = image_height;
70 
71 return true;
72}
73 
74// ╔═══════════════════════════════════════════════════════════════════════════╗
75// ║ SettingsView::Draw - Main render function ║
76// ╚═══════════════════════════════════════════════════════════════════════════╝
77void SettingsView::Draw(GUIContext &ctx) {
78 ctx.CheckForUpdates();
79 
80 static bool contributorsTriggered = false;
81 if (!contributorsTriggered) {
82 ctx.FetchContributors();
83 contributorsTriggered = true;
84 }
85 
86 // Load Avatar on first draw
87 if (!g_AvatarLoaded) {
88 // Try loading from assets folder
89 if (LoadTextureFromFile("assets/logo.jpeg", &g_AvatarTexture,
90 &g_AvatarWidth, &g_AvatarHeight)) {
91 g_AvatarLoaded = true;
92 } else if (LoadTextureFromFile("../assets/logo.jpeg", &g_AvatarTexture,
93 &g_AvatarWidth, &g_AvatarHeight)) {
94 g_AvatarLoaded = true;
95 } else {
96 // If execution dir is different, try looking relative to common paths
97 // Just mark as loaded to stop trying if it fails
98 g_AvatarLoaded = true;
99 g_AvatarTexture = 0;
100 }
101 }
102 
103 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 10));
104 
105 DrawGeneralSection(ctx);
106 
107 ImGui::Spacing();
108 ImGui::Separator();
109 ImGui::Spacing();
110 
111 DrawExtractionSection();
112 
113 ImGui::Spacing();
114 ImGui::Separator();
115 ImGui::Spacing();
116 
117 DrawAppearanceSection();
118 
119 ImGui::Spacing();
120 ImGui::Separator();
121 ImGui::Spacing();
122 
123 DrawCreditsSection();
124 
125 ImGui::Spacing();
126 ImGui::Separator();
127 ImGui::Spacing();
128 
129 DrawContributorsSection(ctx);
130 
131 ImGui::Spacing();
132 ImGui::Separator();
133 ImGui::Spacing();
134 
135 DrawAboutSection();
136 
137 ImGui::PopStyleVar();
138}
139 
140// ┌─────────────────────────────────────────────────────────────────────────┐
141// │ General Settings │
142// └─────────────────────────────────────────────────────────────────────────┘
143void SettingsView::DrawGeneralSection(GUIContext &ctx) {
144 // Update Notification Banner
145 const auto &status = ctx.GetUpdateStatus();
146 if (status.hasUpdate) {
147 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.2f, 0.4f, 0.2f, 1.0f));
148 if (ImGui::BeginChild("UpdateBanner", ImVec2(0, 45), true)) {
149 ImGui::SameLine();
150 ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f),
151 ICON_FA_DOWNLOAD " New Version Available: %s",
152 status.latestVersion.c_str());
153 
154 ImGui::SameLine();
155 ImGui::Dummy(ImVec2(20, 0));
156 ImGui::SameLine();
157 
158 if (ImGui::Button("Download")) {
159 system(("open " + status.releaseUrl).c_str());
160 }
161 }
162 ImGui::EndChild();
163 ImGui::PopStyleColor();
164 ImGui::Spacing();
165 }
166 
167 ImGui::TextColored(Colors::Primary, ICON_FA_GEAR " GENERAL");
168 ImGui::Spacing();
169 
170 ImGui::Checkbox("Auto-detect RIF files", &settings_.autoDetectRif);
171 if (ImGui::IsItemHovered()) {
172 ImGui::SetTooltip("Automatically search for matching RIF files in the same "
173 "directory as the PKG");
174 }
175 
176 ImGui::SliderInt("Max Log Entries", &settings_.maxLogEntries, 100, 5000);
177 if (ImGui::IsItemHovered()) {
178 ImGui::SetTooltip("Maximum number of log entries to keep in memory");
179 }
180 
181 GuiLogSink::Instance().SetMaxEntries(
182 static_cast<size_t>(settings_.maxLogEntries));
183}
184 
185// ┌─────────────────────────────────────────────────────────────────────────┐
186// │ Extraction Settings │
187// └─────────────────────────────────────────────────────────────────────────┘
188void SettingsView::DrawExtractionSection() {
189 ImGui::TextColored(Colors::Primary, ICON_FA_DOWNLOAD " EXTRACTION");
190 ImGui::Spacing();
191 
192 ImGui::Checkbox("Create TitleID subfolder",
193 &settings_.createTitleIdSubfolder);
194 ImGui::Checkbox("Show hex values in Inspector",
195 &settings_.showHexInInspector);
196}
197 
198// ┌─────────────────────────────────────────────────────────────────────────┐
199// │ Appearance Settings │
200// └─────────────────────────────────────────────────────────────────────────┘
201void SettingsView::DrawAppearanceSection() {
202 ImGui::TextColored(Colors::Primary, ICON_FA_IMAGE " APPEARANCE");
203 ImGui::Spacing();
204 
205 ImGui::BeginDisabled(true);
206 ImGui::Checkbox("Dark Theme (Cyber-Dark)", &settings_.darkTheme);
207 ImGui::EndDisabled();
208 ImGui::TextColored(Colors::TextDim, "Only dark theme is currently supported");
209}
210 
211// ┌─────────────────────────────────────────────────────────────────────────┐
212// │ About Section │
213// └─────────────────────────────────────────────────────────────────────────┘
214void SettingsView::DrawAboutSection() {
215 ImGui::TextColored(Colors::Primary, ICON_FA_CIRCLE_INFO " ABOUT");
216 ImGui::Spacing();
217 
218 ImGui::PushStyleColor(ImGuiCol_Text, Colors::Primary);
219 ImFont *monoFont = ShadPKG::GUI::GetMonospaceFont();
220 if (monoFont) {
221 ImGui::PushFont(monoFont);
222 }
223 ImGui::TextUnformatted(R"(
224 
225 __ ______ __ ________
226 _____/ /_ ____ _____/ / __ \/ //_/ ____/
227 / ___/ __ \/ __ `/ __ / /_/ / ,< / / __
228 (__ ) / / / /_/ / /_/ / ____/ /| / /_/ /
229/____/_/ /_/\__,_/\__,_/_/ /_/ |_\____/
230 
231 )");
232 if (monoFont) {
233 ImGui::PopFont();
234 }
235 ImGui::PopStyleColor();
236 
237 ImGui::Spacing();
238 ImGui::Text("Version: %s", Common::VERSION);
239 ImGui::TextColored(Colors::TextDim, "PKG Extractor and Inspector");
240 
241 ImGui::Spacing();
242 ImGui::Text("GitHub:");
243 ImGui::SameLine();
244 ImGui::TextColored(Colors::Primary, "https://github.com/seregonwar/shadPKG");
245 
246 ImGui::Spacing();
247 ImGui::TextColored(Colors::TextDim, "Licensed under LGPL-2.1-or-later");
248 
249 ImGui::Spacing();
250 ImGui::Spacing();
251 if (ImGui::Button("Clear Logs")) {
252 GuiLogSink::Instance().Clear();
253 }
254}
255 
256// ┌─────────────────────────────────────────────────────────────────────────┐
257// │ Credits Section │
258// └─────────────────────────────────────────────────────────────────────────┘
259static void DrawAvatarPlaceholder(float size) {
260 ImDrawList *draw_list = ImGui::GetWindowDrawList();
261 ImVec2 p = ImGui::GetCursorScreenPos();
262 float radius = size * 0.5f;
263 ImVec2 center = ImVec2(p.x + radius, p.y + radius);
264 
265 draw_list->AddCircleFilled(center, radius, IM_COL32(0, 173, 181, 255));
266 
267 const char *initials = "SW";
268 ImVec2 text_size = ImGui::CalcTextSize(initials);
269 ImVec2 text_pos =
270 ImVec2(center.x - text_size.x * 0.5f, center.y - text_size.y * 0.5f);
271 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), initials);
272 draw_list->AddCircle(center, radius, IM_COL32(255, 255, 255, 100), 0, 2.0f);
273 ImGui::Dummy(ImVec2(size, size));
274}
275 
276void SettingsView::DrawCreditsSection() {
277 ImGui::TextColored(Colors::Primary, ICON_FA_USER " CREDITS");
278 ImGui::Spacing();
279 
280 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.12f, 1.0f));
281 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(15, 15));
282 
283 if (ImGui::BeginChild("ProfileCard", ImVec2(0, 110), true,
284 ImGuiWindowFlags_NoScrollbar)) {
285 // Avatar
286 if (g_AvatarTexture != 0) {
287 // Draw real avatar
288 ImGui::Image((ImTextureID)(uintptr_t)g_AvatarTexture, ImVec2(64, 64));
289 } else {
290 // Fallback
291 DrawAvatarPlaceholder(64.0f);
292 }
293 
294 ImGui::SameLine();
295 ImGui::Dummy(ImVec2(10, 0));
296 ImGui::SameLine();
297 
298 // Info
299 ImGui::BeginGroup();
300 {
301 ImGui::PushFont(ShadPKG::GUI::GetDefaultFont());
302 ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "seregonwar");
303 ImGui::PopFont();
304 
305 ImGui::TextColored(Colors::TextDim, "Lead Developer");
306 ImGui::Spacing();
307 
308 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 4));
309 
310 if (ImGui::Button(ICON_FA_TERMINAL " GitHub")) {
311 system("open https://github.com/seregonwar");
312 }
313 if (ImGui::IsItemHovered())
314 ImGui::SetTooltip("Visit GitHub Profile");
315 
316 ImGui::SameLine();
317 
318 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.16f, 0.66f, 0.89f, 1.0f));
319 if (ImGui::Button(ICON_FA_MUG_HOT " Support")) {
320 system("open https://ko-fi.com/seregon");
321 }
322 ImGui::PopStyleColor();
323 ImGui::SameLine();
324 
325 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
326 if (ImGui::Button(ICON_FA_HEART " Sponsor")) {
327 system("open https://github.com/sponsors/seregonwar");
328 }
329 ImGui::PopStyleColor();
330 
331 ImGui::PopStyleVar();
332 }
333 ImGui::EndGroup();
334 }
335 ImGui::EndChild();
336 ImGui::PopStyleVar();
337 ImGui::PopStyleColor();
338}
339 
340// ┌─────────────────────────────────────────────────────────────────────────┐
341// │ Contributors Section │
342// └─────────────────────────────────────────────────────────────────────────┘
343void SettingsView::DrawContributorsSection(GUIContext &ctx) {
344 ImGui::TextColored(Colors::Primary,
345 ICON_FA_USER " CONTRIBUTORS & COMMUNITY");
346 ImGui::Spacing();
347 
348 // Background frame for contributors
349 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.12f, 0.5f));
350 ImGui::BeginChild("ContributorsList", ImVec2(0, 80), true);
351 
352 const auto &contributors = ctx.GetContributors();
353 
354 if (contributors.empty()) {
355 ImGui::TextColored(Colors::TextDim, "Loading community from GitHub...");
356 } else {
357 // Simple flowing layout
358 ImGuiStyle &style = ImGui::GetStyle();
359 float window_visible_x2 =
360 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
361 
362 for (size_t i = 0; i < contributors.size(); i++) {
363 const auto &c = contributors[i];
364 ImGui::PushID((int)i); // Fixes ID conflict error
365 
366 // Draw button
367 std::string label = c.name.empty() ? "Contributor" : c.name;
368 if (ImGui::Button(label.c_str())) {
369 system(("open " + c.url).c_str());
370 }
371 if (ImGui::IsItemHovered())
372 ImGui::SetTooltip("View Profile: %s", c.url.c_str());
373 
374 // Layout wrapping
375 float last_button_x2 = ImGui::GetItemRectMax().x;
376 float next_button_x2 =
377 last_button_x2 + style.ItemSpacing.x + 100.0f; // estimation
378 if (i + 1 < contributors.size() && next_button_x2 < window_visible_x2)
379 ImGui::SameLine();
380 
381 ImGui::PopID();
382 }
383 }
384 
385 ImGui::EndChild();
386 ImGui::PopStyleColor();
387}
388 
389} // namespace ShadPKG::GUI
390