Add update handling and UI for latest version notifications

This commit is contained in:
Bella 2025-03-12 20:15:41 +13:00
parent 5009474e14
commit bd35b9e2be
No known key found for this signature in database
GPG Key ID: 725FECA79EF56B97
7 changed files with 308 additions and 225 deletions

View File

@ -24,6 +24,7 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
256C91642D8126E300F9736D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 256C91632D8126E300F9736D /* Alamofire */; };
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
@ -205,6 +206,7 @@
files = (
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
256C91642D8126E300F9736D /* Alamofire in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
);
@ -303,6 +305,7 @@
packageProductDependencies = (
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
256C91632D8126E300F9736D /* Alamofire */,
);
productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@ -395,6 +398,7 @@
packageReferences = (
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */,
);
preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@ -651,7 +655,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
DEVELOPMENT_TEAM = 4TD3JXVDW7;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -710,6 +714,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
@ -833,9 +839,13 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
@ -854,7 +864,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
DEVELOPMENT_TEAM = 4TD3JXVDW7;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -913,6 +923,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
@ -1036,9 +1048,13 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
@ -1235,6 +1251,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.2;
};
};
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
@ -1254,6 +1278,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
256C91632D8126E300F9736D /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = 256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
isa = XCSwiftPackageProductDependency;
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;

View File

@ -1,6 +1,15 @@
{
"originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
"originHash" : "587a0e7c5c7d612a2c16a973e66df9a6a582b963cb51df7c89fd96cb28ef4a63",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{
"identity" : "swiftsvg",
"kind" : "remoteSourceControl",

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A99C2CD6F54700029585"
BuildableName = "MeloNXTests.xctest"
BlueprintName = "MeloNXTests"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A9A62CD6F54700029585"
BuildableName = "MeloNXUITests.xctest"
BlueprintName = "MeloNXUITests"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
showGraphicsOverview = "Yes"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Debug"
revealArchiveInOrganizer = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash">
</ActionContent>
</ExecutionAction>
</PreActions>
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,37 @@
//
// LatestVersionResponse.swift
// MeloNX
//
// Created by Bella on 12/03/2025.
//
struct LatestVersionResponse: Codable {
let version_number: String
let version_number_stripped: String
let changelog: String
let download_link: String
#if DEBUG
static let example1 = LatestVersionResponse(
version_number: "1.0.0",
version_number_stripped: "100",
changelog: """
- Rewrite Display Code (SDL isn't used for display anymore)
- Add New Onboarding / Setup
- Better Performance
- Remove "SDL Window" option in settings
- Fix JIT Cache Regions
- Fix how JIT is detected in Settings
- Fix ABYX being swapped on controller.
- Settings are now a config.json file
- Fix Performance Overlay not showing when Virtual Controller is hidden
- Add displaying logs when Loading or in-game
- Fix Launching games from outside of the roms folder
- Add Waiting for JIT popup
- Fix spesific Games
- Added Back Herobrine ("You were supposed to be the hero, Bryan")
""",
download_link: "https://example.com"
)
#endif
}

View File

@ -0,0 +1,64 @@
//
// MeloNXUpdateSheet.swift
// MeloNX
//
// Created by Bella on 12/03/2025.
//
import SwiftUI
struct MeloNXUpdateSheet: View {
let updateInfo: LatestVersionResponse
@Binding var isPresented: Bool
var body: some View {
iOSNav {
VStack {
Text("Version \(updateInfo.version_number) is available. You are currently on Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown").")
VStack {
Text("Changelog:")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.headline)
ScrollView {
Text(updateInfo.changelog)
.padding()
}
.frame(maxHeight: 400)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.padding(.top, 15)
Spacer()
Button(action: {
if let url = URL(string: updateInfo.download_link) {
UIApplication.shared.open(url)
}
}) {
Text("Download Now")
.font(.title3)
.bold()
.frame(width: 300, height: 40)
}
.buttonStyle(.borderedProminent)
.frame(alignment: .bottom)
}
.padding(.horizontal)
.navigationTitle("Version \(updateInfo.version_number) Available!")
.toolbar {
Button(action: {
isPresented = false
}) {
Text("Close")
}
}
}
}
}
#Preview {
MeloNXUpdateSheet(updateInfo: LatestVersionResponse.example1, isPresented: .constant(true))
}

View File

@ -8,249 +8,82 @@
import SwiftUI
import UIKit
import CryptoKit
import Alamofire
@main
struct MeloNXApp: App {
@State var showed = false
@Environment(\.scenePhase) var scenePhase
@State var alert: UIAlertController? = nil
@State var finished = false
// Variables for the update system :)
@State var showOutOfDateSheet = false
@State var updateInfo: LatestVersionResponse? = nil
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
var body: some Scene {
WindowGroup {
ZStack {
if showed || DRM != 1 {
if finishedStorage {
ContentView()
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation {
withAnimation {
finishedStorage = newValue
}
}
}
}
VStack {
if finishedStorage {
ContentView()
} else {
Group {
VStack {
Spacer()
HStack {
Text("Loading...")
ProgressView()
}
Spacer()
Text(UIDevice.current.identifierForVendor?.uuidString ?? "")
}
}
.onAppear {
initR()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(1))
.foregroundColor(.white)
}
}
}
}
func initR() {
if DRM == 1 {
DispatchQueue.main.async { [self] in
// drmcheck()
InitializeRyujinx() { bool in
if bool {
print("Ryujinx Files Initialized Successfully")
DispatchQueue.main.async { [self] in
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation {
showed = true
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
InitializeRyujinx() { bool in
if !bool, (scenePhase != .background || scenePhase == .inactive) {
withAnimation {
showed = false
}
if !(alert?.isViewLoaded ?? false) {
alert = showDMCAAlert()
}
} else {
DispatchQueue.main.async {
alert?.dismiss(animated: true)
showed = true
}
}
withAnimation {
finishedStorage = newValue
}
}
}
} else {
showDMCAAlert()
}
}
}
}
}
func showAlert() -> UIAlertController? {
// Create the alert controller
if let mainWindow = UIApplication.shared.windows.last {
let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert)
// Add a text field to the alert
alertController.addTextField { textField in
textField.placeholder = "Enter key here"
}
// Add the "OK" action
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
// Get the text entered in the text field
if let textField = alertController.textFields?.first, let enteredText = textField.text {
print("Entered text: \(enteredText)")
UserDefaults.standard.set(enteredText, forKey: "MeloDRMID")
// drmcheck() { bool in
// if bool {
// showed = true
// } else {
// exit(0)
// }
// }
}
}
alertController.addAction(okAction)
// Add a "Cancel" action
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
// Present the alert
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
return alertController
} else {
return nil
}
}
}
func showDMCAAlert() -> UIAlertController? {
if let mainWindow = UIApplication.shared.windows.first {
let alertController = UIAlertController(title: "Unauthorized Copy Notice", message: "This app was illegally leaked. Please report the download on the MeloNX Discord. In the meantime, check out Pomelo! \n -Stossy11", preferredStyle: .alert)
DispatchQueue.main.async {
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
}
return alertController
} else {
// uhoh
return nil
}
}
/*
func drmcheck(completion: @escaping (Bool) -> Void) {
if let deviceid = UIDevice.current.identifierForVendor?.uuidString, let base64device = deviceid.data(using: .utf8)?.base64EncodedString() {
if let value = UserDefaults.standard.string(forKey: "MeloDRMID") {
if let url = URL(string: "https://mx.stossy11.com/auth/\(value)/\(base64device)") {
print(url)
// Create a URLSession
let session = URLSession.shared
// Create a data task
let task = session.dataTask(with: url) { data, response, error in
// Handle errors
if let error = error {
exit(0)
}
// Check response and data
if let response = response as? HTTPURLResponse, response.statusCode == 200 {
print("Successfully Recieved API Data")
completion(true)
} else if let response = response as? HTTPURLResponse, response.statusCode == 201 {
print("Successfully Created Auth UUID")
completion(true)
} else {
completion(false)
.onAppear {
checkLatestVersion()
}
// this seems like a weird way to show the sheet but, from my history this is the most reliable way for the content to actually show in the sheet, otherwise its blank
.sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in
if !newValue {
showOutOfDateSheet = false
updateInfo = nil
}
}
// Start the task
task.resume()
)) {
if let updateInfo = updateInfo {
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
}
}
} else {
completion(false)
}
} else {
completion(false)
}
}
*/
func InitializeRyujinx(completion: @escaping (Bool) -> Void) {
let path = "aHR0cHM6Ly9teC5zdG9zc3kxMS5jb20v"
guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else {
completion(false)
return
}
if (detectRoms(path: path) != value) {
completion(false)
}
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
configuration.urlCache = nil
let session = URLSession(configuration: configuration)
guard let url = URL(string: addFolders(path)!) else {
completion(false)
return
}
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
completion(false)
}
// sends a GET request to the MeloNXSite API and compares the version it returns to the current app version
func checkLatestVersion() {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
#if DEBUG
// no this isnt a public ip address silly viewers (i know damn well someone thought this was my real ip), this is local :PP
let url = "http://192.168.178.116:8000/api/latest_release"
#else
// dont spam this :pray:
let url = "https://melonx.org/api/latest_release"
#endif
guard let httpResponse = response as? HTTPURLResponse else {
completion(false)
return
// actually sends the request
AF.request(url).responseDecodable(of: LatestVersionResponse.self) { response in
switch response.result {
case .success(let latestVersionResponse):
let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
if Int(strippedAppVersion) ?? 0 < Int(latestAPIVersionStripped) ?? 0 {
updateInfo = latestVersionResponse
showOutOfDateSheet = true
}
case .failure(let error):
print("Error checking for new version: \(error)")
}
}
if httpResponse.statusCode == 200 {
completion(true)
} else {
completion(false)
}
return
}
task.resume()
}
func detectRoms(path string: String) -> String {
@ -259,8 +92,6 @@ func detectRoms(path string: String) -> String {
return romHash.compactMap { String(format: "%02x", $0) }.joined()
}
func addFolders(_ folderPath: String) -> String? {
let fileManager = FileManager.default
if let data = Data(base64Encoded: folderPath),
@ -271,7 +102,6 @@ func addFolders(_ folderPath: String) -> String? {
}
extension String {
func print() {
Swift.print(self)
}