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 metalLayer: CAMetalLayer? = nil
|
||||||
@Published var firmwareversion = "0"
|
@Published var firmwareversion = "0"
|
||||||
@Published var emulationUIView = UIView()
|
@Published var emulationUIView = UIView()
|
||||||
|
@Published var games: [Game] = []
|
||||||
|
|
||||||
var shouldMetal: Bool {
|
var shouldMetal: Bool {
|
||||||
metalLayer == nil
|
metalLayer == nil
|
||||||
@ -65,7 +66,9 @@ class Ryujinx {
|
|||||||
|
|
||||||
static let shared = Ryujinx()
|
static let shared = Ryujinx()
|
||||||
|
|
||||||
private init() {}
|
private init() {
|
||||||
|
self.games = loadGames()
|
||||||
|
}
|
||||||
|
|
||||||
public struct Configuration : Codable, Equatable {
|
public struct Configuration : Codable, Equatable {
|
||||||
var gamepath: String
|
var gamepath: String
|
||||||
@ -202,6 +205,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] {
|
private func buildCommandLineArgs(from config: Configuration) -> [String] {
|
||||||
var args: [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 SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
public struct Game: Identifiable, Equatable {
|
public struct Game: Identifiable, Equatable, Hashable {
|
||||||
public var id = UUID()
|
public var id = UUID()
|
||||||
|
|
||||||
var containerFolder: URL
|
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.
|
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()
|
.bold()
|
||||||
|
|
||||||
Text("**Version:** \(game.version)")
|
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("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
||||||
Text("**File Type:** .\(getFileType(game.fileURL))")
|
Text("**File Type:** .\(getFileType(game.fileURL))")
|
||||||
Text("**Game URL:** \(trimGameURL(game.fileURL))")
|
Text("**Game URL:** \(trimGameURL(game.fileURL))")
|
||||||
|
@ -16,7 +16,6 @@ extension UTType {
|
|||||||
struct GameLibraryView: View {
|
struct GameLibraryView: View {
|
||||||
@Binding var startemu: Game?
|
@Binding var startemu: Game?
|
||||||
// @State var importDLCs = false
|
// @State var importDLCs = false
|
||||||
@State private var games: [Game] = []
|
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@State private var isSearching = false
|
@State private var isSearching = false
|
||||||
@AppStorage("recentGames") private var recentGamesData: Data = Data()
|
@AppStorage("recentGames") private var recentGamesData: Data = Data()
|
||||||
@ -29,13 +28,18 @@ struct GameLibraryView: View {
|
|||||||
@State var isSelectingGameFile = false
|
@State var isSelectingGameFile = false
|
||||||
@State var isViewingGameInfo: Bool = false
|
@State var isViewingGameInfo: Bool = false
|
||||||
@State var gameInfo: Game?
|
@State var gameInfo: Game?
|
||||||
|
var games: Binding<[Game]> {
|
||||||
|
Binding(
|
||||||
|
get: { Ryujinx.shared.games },
|
||||||
|
set: { Ryujinx.shared.games = $0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var filteredGames: [Game] {
|
var filteredGames: [Game] {
|
||||||
if searchText.isEmpty {
|
if searchText.isEmpty {
|
||||||
return games
|
return Ryujinx.shared.games
|
||||||
}
|
}
|
||||||
return games.filter {
|
return Ryujinx.shared.games.filter {
|
||||||
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
||||||
$0.developer.localizedCaseInsensitiveContains(searchText)
|
$0.developer.localizedCaseInsensitiveContains(searchText)
|
||||||
}
|
}
|
||||||
@ -52,7 +56,7 @@ struct GameLibraryView: View {
|
|||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
if games.isEmpty {
|
if Ryujinx.shared.games.isEmpty {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Image(systemName: "gamecontroller.fill")
|
Image(systemName: "gamecontroller.fill")
|
||||||
.font(.system(size: 64))
|
.font(.system(size: 64))
|
||||||
@ -95,7 +99,7 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
LazyVStack(spacing: 2) {
|
LazyVStack(spacing: 2) {
|
||||||
ForEach(filteredGames) { game in
|
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 {
|
.onTapGesture {
|
||||||
addToRecentGames(game)
|
addToRecentGames(game)
|
||||||
}
|
}
|
||||||
@ -105,7 +109,7 @@ struct GameLibraryView: View {
|
|||||||
} else {
|
} else {
|
||||||
LazyVStack(spacing: 2) {
|
LazyVStack(spacing: 2) {
|
||||||
ForEach(filteredGames) { game in
|
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 {
|
.onTapGesture {
|
||||||
addToRecentGames(game)
|
addToRecentGames(game)
|
||||||
}
|
}
|
||||||
@ -115,7 +119,6 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadGames()
|
|
||||||
loadRecentGames()
|
loadRecentGames()
|
||||||
|
|
||||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
@ -262,7 +265,7 @@ struct GameLibraryView: View {
|
|||||||
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
|
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
try fileManager.copyItem(at: url, to: destinationURL)
|
try fileManager.copyItem(at: url, to: destinationURL)
|
||||||
|
|
||||||
loadGames()
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
} catch {
|
} catch {
|
||||||
print("Error copying game file: \(error)")
|
print("Error copying game file: \(error)")
|
||||||
}
|
}
|
||||||
@ -317,56 +320,15 @@ struct GameLibraryView: View {
|
|||||||
recentGames = []
|
recentGames = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// MARK: - Delete Game Function
|
||||||
func deleteGame(game: Game) {
|
func deleteGame(game: Game) {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
do {
|
do {
|
||||||
try fileManager.removeItem(at: game.fileURL)
|
try fileManager.removeItem(at: game.fileURL)
|
||||||
games.removeAll { $0.id == game.id }
|
Ryujinx.shared.games.removeAll { $0.id == game.id }
|
||||||
loadGames()
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
} catch {
|
} catch {
|
||||||
print("Error deleting game: \(error)")
|
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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<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>
|
<key>MeloID</key>
|
||||||
<string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string>
|
<string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>LaunchGameIntent</string>
|
||||||
|
</array>
|
||||||
<key>UIFileSharingEnabled</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UTExportedTypeDeclarations</key>
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
@ -15,7 +15,7 @@ import CryptoKit
|
|||||||
struct MeloNXApp: App {
|
struct MeloNXApp: App {
|
||||||
|
|
||||||
@State var showed = false
|
@State var showed = false
|
||||||
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
@ -61,7 +61,7 @@ struct MeloNXApp: App {
|
|||||||
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||||
InitializeRyujinx() { bool in
|
InitializeRyujinx() { bool in
|
||||||
if !bool {
|
if !bool, (scenePhase != .background || scenePhase == .inactive) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
showed = false
|
showed = false
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ struct MeloNXApp: App {
|
|||||||
// Present the alert
|
// Present the alert
|
||||||
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||||
} else {
|
} else {
|
||||||
exit(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ func showDMCAAlert() {
|
|||||||
|
|
||||||
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||||
} else {
|
} else {
|
||||||
exit(0)
|
// uhoh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user