forked from MeloNX/MeloNX
Add Intent to Launch Game and change how DRM works
This commit is contained in:
parent
4f3e49a90c
commit
007cb026a4
Binary file not shown.
@ -58,6 +58,7 @@ class Ryujinx {
|
||||
@Published var metalLayer: CAMetalLayer? = nil
|
||||
@Published var firmwareversion = "0"
|
||||
@Published var emulationUIView = UIView()
|
||||
@Published var games: [Game] = []
|
||||
|
||||
var shouldMetal: Bool {
|
||||
metalLayer == nil
|
||||
@ -65,7 +66,9 @@ class Ryujinx {
|
||||
|
||||
static let shared = Ryujinx()
|
||||
|
||||
private init() {}
|
||||
private init() {
|
||||
self.games = loadGames()
|
||||
}
|
||||
|
||||
public struct Configuration : Codable, Equatable {
|
||||
var gamepath: String
|
||||
@ -203,6 +206,53 @@ class Ryujinx {
|
||||
}
|
||||
}
|
||||
|
||||
func loadGames() -> [Game] {
|
||||
let fileManager = FileManager.default
|
||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return [] }
|
||||
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
if (!fileManager.fileExists(atPath: romsDirectory.path)) {
|
||||
do {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
print("Failed to create roms directory: \(error)")
|
||||
}
|
||||
}
|
||||
var games: [Game] = []
|
||||
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
for fileURLCandidate in files {
|
||||
if fileURLCandidate.pathExtension == "zip" {
|
||||
continue
|
||||
}
|
||||
|
||||
do {
|
||||
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
|
||||
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
|
||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||
|
||||
|
||||
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
|
||||
|
||||
games.append(game)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
return games
|
||||
} catch {
|
||||
print("Error loading games from roms folder: \(error)")
|
||||
return games
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func buildCommandLineArgs(from config: Configuration) -> [String] {
|
||||
var args: [String] = []
|
||||
|
||||
|
54
src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
Normal file
54
src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// LaunchGameIntentDef.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 10/02/2025.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Intents
|
||||
import AppIntents
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct LaunchGameIntentDef: AppIntent {
|
||||
|
||||
static let title: LocalizedStringResource = "Launch Game"
|
||||
|
||||
static var description = IntentDescription("Launches the Selected Game.")
|
||||
|
||||
@Parameter(title: "Game", optionsProvider: GameOptionsProvider())
|
||||
var gameName: String
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Launch \(\.$gameName)")
|
||||
}
|
||||
|
||||
static var openAppWhenRun: Bool = true
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
||||
let ryujinx = Ryujinx.shared.games
|
||||
|
||||
let urlString = "melonx://game?\(ryujinx.contains(where: { $0.titleName.localizedCaseInsensitiveContains(gameName) }) ? "name" : "id")=\(gameName)"
|
||||
print(urlString)
|
||||
if let url = URL(string: urlString) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct GameOptionsProvider: DynamicOptionsProvider {
|
||||
func results() async throws -> [String] {
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
|
||||
let dynamicGames = Ryujinx.shared.games
|
||||
|
||||
return dynamicGames.map { $0.titleName }
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public struct Game: Identifiable, Equatable {
|
||||
public struct Game: Identifiable, Equatable, Hashable {
|
||||
public var id = UUID()
|
||||
|
||||
var containerFolder: URL
|
||||
|
@ -118,6 +118,16 @@ struct ContentView: View {
|
||||
|
||||
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
|
||||
}
|
||||
.onOpenURL() { url in
|
||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
components.host == "game" {
|
||||
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
|
||||
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
||||
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -52,6 +52,14 @@ struct GameInfoSheet: View {
|
||||
.bold()
|
||||
|
||||
Text("**Version:** \(game.version)")
|
||||
Text("**Title ID:** \(game.titleId)")
|
||||
.contextMenu {
|
||||
Button {
|
||||
UIPasteboard.general.string = game.titleId
|
||||
} label: {
|
||||
Text("Copy Title ID")
|
||||
}
|
||||
}
|
||||
Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
||||
Text("**File Type:** .\(getFileType(game.fileURL))")
|
||||
Text("**Game URL:** \(trimGameURL(game.fileURL))")
|
||||
|
@ -16,7 +16,6 @@ extension UTType {
|
||||
struct GameLibraryView: View {
|
||||
@Binding var startemu: Game?
|
||||
// @State var importDLCs = false
|
||||
@State private var games: [Game] = []
|
||||
@State private var searchText = ""
|
||||
@State private var isSearching = false
|
||||
@AppStorage("recentGames") private var recentGamesData: Data = Data()
|
||||
@ -29,13 +28,18 @@ struct GameLibraryView: View {
|
||||
@State var isSelectingGameFile = false
|
||||
@State var isViewingGameInfo: Bool = false
|
||||
@State var gameInfo: Game?
|
||||
|
||||
var games: Binding<[Game]> {
|
||||
Binding(
|
||||
get: { Ryujinx.shared.games },
|
||||
set: { Ryujinx.shared.games = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
var filteredGames: [Game] {
|
||||
if searchText.isEmpty {
|
||||
return games
|
||||
return Ryujinx.shared.games
|
||||
}
|
||||
return games.filter {
|
||||
return Ryujinx.shared.games.filter {
|
||||
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.developer.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
@ -52,7 +56,7 @@ struct GameLibraryView: View {
|
||||
.padding(.top, 12)
|
||||
}
|
||||
|
||||
if games.isEmpty {
|
||||
if Ryujinx.shared.games.isEmpty {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "gamecontroller.fill")
|
||||
.font(.system(size: 64))
|
||||
@ -95,7 +99,7 @@ struct GameLibraryView: View {
|
||||
|
||||
LazyVStack(spacing: 2) {
|
||||
ForEach(filteredGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu, games: $games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
|
||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
|
||||
.onTapGesture {
|
||||
addToRecentGames(game)
|
||||
}
|
||||
@ -105,7 +109,7 @@ struct GameLibraryView: View {
|
||||
} else {
|
||||
LazyVStack(spacing: 2) {
|
||||
ForEach(filteredGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu, games: $games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
|
||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
|
||||
.onTapGesture {
|
||||
addToRecentGames(game)
|
||||
}
|
||||
@ -115,7 +119,6 @@ struct GameLibraryView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadGames()
|
||||
loadRecentGames()
|
||||
|
||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||
@ -262,7 +265,7 @@ struct GameLibraryView: View {
|
||||
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try fileManager.copyItem(at: url, to: destinationURL)
|
||||
|
||||
loadGames()
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
} catch {
|
||||
print("Error copying game file: \(error)")
|
||||
}
|
||||
@ -318,55 +321,14 @@ struct GameLibraryView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - loads games from roms
|
||||
func loadGames() {
|
||||
let fileManager = FileManager.default
|
||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
// Check if "roms" folder exists; if not, create it
|
||||
if (!fileManager.fileExists(atPath: romsDirectory.path)) {
|
||||
do {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
print("Failed to create roms directory: \(error)")
|
||||
}
|
||||
}
|
||||
games = []
|
||||
// Load games only from "roms" folder
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
files.forEach { fileURLCandidate in
|
||||
do {
|
||||
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
|
||||
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
|
||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||
|
||||
|
||||
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
|
||||
|
||||
games.append(game)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Error loading games from roms folder: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete Game Function
|
||||
func deleteGame(game: Game) {
|
||||
let fileManager = FileManager.default
|
||||
do {
|
||||
try fileManager.removeItem(at: game.fileURL)
|
||||
games.removeAll { $0.id == game.id }
|
||||
loadGames()
|
||||
Ryujinx.shared.games.removeAll { $0.id == game.id }
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
} catch {
|
||||
print("Error deleting game: \(error)")
|
||||
}
|
||||
|
@ -2,8 +2,29 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.stossy11.MeloNX</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>melonx</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>melonx</string>
|
||||
</array>
|
||||
<key>MeloID</key>
|
||||
<string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>LaunchGameIntent</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
|
@ -15,7 +15,7 @@ import CryptoKit
|
||||
struct MeloNXApp: App {
|
||||
|
||||
@State var showed = false
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
@ -61,7 +61,7 @@ struct MeloNXApp: App {
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
InitializeRyujinx() { bool in
|
||||
if !bool {
|
||||
if !bool, (scenePhase != .background || scenePhase == .inactive) {
|
||||
withAnimation {
|
||||
showed = false
|
||||
}
|
||||
@ -119,7 +119,7 @@ struct MeloNXApp: App {
|
||||
// Present the alert
|
||||
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||
} else {
|
||||
exit(0)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ func showDMCAAlert() {
|
||||
|
||||
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||
} else {
|
||||
exit(0)
|
||||
// uhoh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user