diff --git a/PlayCover/AppInstaller/Installer.swift b/PlayCover/AppInstaller/Installer.swift index dc042731a..92d1709c1 100644 --- a/PlayCover/AppInstaller/Installer.swift +++ b/PlayCover/AppInstaller/Installer.swift @@ -40,6 +40,7 @@ class Installer { // If (the option key is held or the install playtools popup settings is true) and its not an export, // then show the installer dialog let installPlayTools: Bool + let applicationType = InstallPreferences.shared.defaultAppType if (Installer.isOptionKeyHeld || InstallPreferences.shared.showInstallPopup) && !export { installPlayTools = installPlayToolsPopup() @@ -81,6 +82,8 @@ class Installer { try PlayTools.installInIPA(app.executable) } + app.info.applicationCategoryType = applicationType + if !export { // -rwxr-xr-x try app.executable.setBinaryPosixPermissions(0o755) diff --git a/PlayCover/Model/AppInfo.swift b/PlayCover/Model/AppInfo.swift index d95ba2b86..4bf60131d 100644 --- a/PlayCover/Model/AppInfo.swift +++ b/PlayCover/Model/AppInfo.swift @@ -5,6 +5,35 @@ import Foundation +enum LSApplicationCategoryType: String, CaseIterable { + case business = "public.app-category.business" + case developerTools = "public.app-category.developer-tools" + case education = "public.app-category.education" + case entertainment = "public.app-category.entertainment" + case finance = "public.app-category.finance" + case games = "public.app-category.games" + case graphicsDesign = "public.app-category.graphics-design" + case healthcareFitness = "public.app-category.healthcare-fitness" + case lifestyle = "public.app-category.lifestyle" + case medical = "public.app-category.medical" + case music = "public.app-category.music" + case news = "public.app-category.news" + case photography = "public.app-category.photography" + case productivity = "public.app-category.productivity" + case reference = "public.app-category.reference" + case socialNetworking = "public.app-category.social-networking" + case sports = "public.app-category.sports" + case travel = "public.app-category.travel" + case utilities = "public.app-category.utilities" + case video = "public.app-category.video" + case weather = "public.app-category.weather" + case none = "public.app-category.none" // Note: This is not in an official category type + + var localizedName: String { + NSLocalizedString(rawValue, comment: "") + } +} + public class AppInfo { public let url: URL fileprivate var rawStorage: NSMutableDictionary @@ -122,6 +151,26 @@ public class AppInfo { } } + var applicationCategoryType: LSApplicationCategoryType { + get { + LSApplicationCategoryType( + rawValue: self[string: "LSApplicationCategoryType"] ?? "" + ) ?? LSApplicationCategoryType.none + } + set { + if newValue == .none { + rawStorage.removeObject(forKey: "LSApplicationCategoryType") + } else { + self[string: "LSApplicationCategoryType"] = newValue.rawValue + } + do { + try write() + } catch { + Log.shared.error(error) + } + } + } + var minimumOSVersion: String { get { self[string: "MinimumOSVersion"] ?? "" @@ -186,6 +235,31 @@ public class AppInfo { return "AppIcon" } + var lsEnvironment: [String: String] { + get { + if self[dictionary: "LSEnvironment"] == nil { + self[dictionary: "LSEnvironment"] = NSMutableDictionary(dictionary: [String: String]()) + } + + return self[dictionary: "LSEnvironment"] as? [String: String] ?? [:] + } + set { + if self[dictionary: "LSEnvironment"] == nil { + self[dictionary: "LSEnvironment"] = NSMutableDictionary(dictionary: [String: String]()) + } + + if let key = newValue.first?.key, let value = newValue.first?.value { + self[dictionary: "LSEnvironment"]?[key] = value + + do { + try write() + } catch { + Log.shared.error(error) + } + } + } + } + func assert(minimumVersion: Double) { if let double = Double(minimumOSVersion) { if double > 11.0 { diff --git a/PlayCover/Model/AppSettings.swift b/PlayCover/Model/AppSettings.swift index 267b4234f..81abb6e37 100644 --- a/PlayCover/Model/AppSettings.swift +++ b/PlayCover/Model/AppSettings.swift @@ -8,6 +8,8 @@ import Foundation import UniformTypeIdentifiers struct AppSettingsData: Codable { + var bundleIdentifier: String = "" + var keymapping = true var sensitivity: Float = 50 @@ -25,7 +27,15 @@ struct AppSettingsData: Codable { var playChain = true var playChainDebugging = false var inverseScreenValues = false - var metalHUD = false + var metalHUD = false { + didSet { + do { + try Shell.setMetalHUD(bundleIdentifier, enabled: metalHUD) + } catch { + Log.shared.error(error) + } + } + } var windowFixMethod = 0 var injectIntrospection = false var rootWorkDir = true @@ -39,6 +49,7 @@ struct AppSettingsData: Codable { // handle old 2.x settings where PlayChain did not exist yet init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + bundleIdentifier = try container.decodeIfPresent(String.self, forKey: .bundleIdentifier) ?? "" keymapping = try container.decodeIfPresent(Bool.self, forKey: .keymapping) ?? true sensitivity = try container.decodeIfPresent(Float.self, forKey: .sensitivity) ?? 50 disableTimeout = try container.decodeIfPresent(Bool.self, forKey: .disableTimeout) ?? false @@ -103,6 +114,8 @@ class AppSettings { if !decode() { encode() } + + settings.bundleIdentifier = info.bundleIdentifier } public func sync() { diff --git a/PlayCover/Model/PlayApp.swift b/PlayCover/Model/PlayApp.swift index 192eafbee..d3964af63 100644 --- a/PlayCover/Model/PlayApp.swift +++ b/PlayCover/Model/PlayApp.swift @@ -66,12 +66,6 @@ class PlayApp: BaseApp { func runAppExec() { let config = NSWorkspace.OpenConfiguration() - if settings.settings.metalHUD { - config.environment = ["MTL_HUD_ENABLED": "1"] - } else { - config.environment = ["MTL_HUD_ENABLED": "0"] - } - if settings.settings.injectIntrospection { config.environment["DYLD_LIBRARY_PATH"] = "/usr/lib/system/introspection" } @@ -208,6 +202,33 @@ class PlayApp: BaseApp { } } + func introspection(set: Bool? = nil) -> Bool { + if info.lsEnvironment["DYLD_LIBRARY_PATH"] == nil { + info.lsEnvironment["DYLD_LIBRARY_PATH"] = "" + } + + if let set = set { + if set { + info.lsEnvironment["DYLD_LIBRARY_PATH"]? += "/usr/lib/system/introspection:" + } else { + info.lsEnvironment["DYLD_LIBRARY_PATH"] = info.lsEnvironment["DYLD_LIBRARY_PATH"]? + .replacingOccurrences(of: "/usr/lib/system/introspection:", with: "") + } + + do { + try Shell.signApp(executable) + } catch { + Log.shared.error(error) + } + } + + guard let introspection = info.lsEnvironment["DYLD_LIBRARY_PATH"] else { + return false + } + + return introspection.contains("/usr/lib/system/introspection") + } + func hasAlias() -> Bool { return FileManager.default.fileExists(atPath: aliasURL.path) } diff --git a/PlayCover/Utils/Shell.swift b/PlayCover/Utils/Shell.swift index eca076d60..c16c8b98e 100644 --- a/PlayCover/Utils/Shell.swift +++ b/PlayCover/Utils/Shell.swift @@ -86,6 +86,11 @@ class Shell: ObservableObject { "--deep", "--preserve-metadata=entitlements") } + static func setMetalHUD(_ bundleID: String, enabled: Bool) throws { + try run("/usr/bin/defaults", "write", bundleID, + "MetalForceHudEnabled", "-bool", String(enabled)) + } + static func lldb(_ url: URL, withTerminalWindow: Bool = false) throws { Task(priority: .utility) { if withTerminalWindow { diff --git a/PlayCover/Views/AppSettingsView.swift b/PlayCover/Views/AppSettingsView.swift index 44b7bb592..a844bf7fa 100644 --- a/PlayCover/Views/AppSettingsView.swift +++ b/PlayCover/Views/AppSettingsView.swift @@ -81,7 +81,9 @@ struct AppSettingsView: View { } .disabled(!(hasPlayTools ?? true)) BypassesView(settings: $viewModel.settings, - hasPlayTools: $hasPlayTools) + hasPlayTools: $hasPlayTools, + hasIntrospection: viewModel.app.introspection(), + app: viewModel.app) .tabItem { Text("settings.tab.bypasses") } @@ -90,7 +92,8 @@ struct AppSettingsView: View { closeView: $closeView, hasPlayTools: $hasPlayTools, hasAlias: $hasAlias, - app: viewModel.app) + app: viewModel.app, + applicationCategoryType: viewModel.app.info.applicationCategoryType) .tabItem { Text("settings.tab.misc") } @@ -441,6 +444,10 @@ struct BypassesView: View { @Binding var settings: AppSettings @Binding var hasPlayTools: Bool? + @State var hasIntrospection: Bool + + var app: PlayApp + var body: some View { ScrollView { VStack { @@ -461,7 +468,7 @@ struct BypassesView: View { } Spacer() HStack { - Toggle("settings.toggle.introspection", isOn: $settings.settings.injectIntrospection) + Toggle("settings.toggle.introspection", isOn: $hasIntrospection) .help("settings.toggle.introspection.help") Spacer() } @@ -483,6 +490,9 @@ struct BypassesView: View { } .padding() } + .onChange(of: hasIntrospection) {_ in + _ = app.introspection(set: hasIntrospection) + } } } @@ -496,9 +506,34 @@ struct MiscView: View { var app: PlayApp + @State var applicationCategoryType: LSApplicationCategoryType + var body: some View { ScrollView { VStack { + HStack { + Text("settings.applicationCategoryType") + Spacer() + Picker("", selection: $applicationCategoryType) { + ForEach(LSApplicationCategoryType.allCases, id: \.rawValue) { value in + Text(value.localizedName) + .tag(value) + } + } + .frame(width: 225) + .onChange(of: applicationCategoryType) { _ in + app.info.applicationCategoryType = applicationCategoryType + Task(priority: .userInitiated) { + do { + try Shell.signApp(app.executable) + } catch { + Log.shared.error(error) + } + } + } + } + Spacer() + .frame(height: 20) HStack { Toggle("settings.toggle.discord", isOn: $settings.settings.discordActivity.enable) Spacer() @@ -594,6 +629,8 @@ struct MiscView: View { } } } + Spacer() + .frame(height: 20) // swiftlint:disable:next todo // TODO: Test and remove before 3.0 release HStack { @@ -642,6 +679,11 @@ struct InfoView: View { Spacer() Text("\(info.bundleVersion)") } + HStack { + Text("settings.applicationCategoryType") + Text(":") + Spacer() + Text("\(info.applicationCategoryType.rawValue)") + } HStack { Text("settings.info.executableName") Spacer() diff --git a/PlayCover/Views/MainView.swift b/PlayCover/Views/MainView.swift index 9d4bc8f5a..471cfb65f 100644 --- a/PlayCover/Views/MainView.swift +++ b/PlayCover/Views/MainView.swift @@ -89,7 +89,7 @@ struct MainView: View { self.selectedView = URLObserved.type == .source ? 2 : 1 } .toolbar { - ToolbarItem(placement: .navigation) { + ToolbarItem { // Sits on the left by default Button(action: toggleSidebar, label: { Image(systemName: "sidebar.leading") }) diff --git a/PlayCover/Views/Settings/InstallSettings.swift b/PlayCover/Views/Settings/InstallSettings.swift index d64833529..5abbe8e69 100644 --- a/PlayCover/Views/Settings/InstallSettings.swift +++ b/PlayCover/Views/Settings/InstallSettings.swift @@ -12,6 +12,8 @@ class InstallPreferences: NSObject, ObservableObject { @objc @AppStorage("AlwaysInstallPlayTools") var alwaysInstallPlayTools = true + @AppStorage("DefaultAppType") var defaultAppType: LSApplicationCategoryType = .none + @AppStorage("ShowInstallPopup") var showInstallPopup = false } @@ -21,20 +23,36 @@ struct InstallSettings: View { @ObservedObject var installPreferences = InstallPreferences.shared var body: some View { - Form { + VStack(alignment: .leading) { + HStack { + Text("settings.applicationCategoryType") + Spacer() + Picker("", selection: installPreferences.$defaultAppType) { + ForEach(LSApplicationCategoryType.allCases, id: \.rawValue) { value in + Text(value.localizedName) + .tag(value) + } + } + .frame(width: 225) + } + Spacer() + .frame(height: 20) Toggle("preferences.toggle.showInstallPopup", isOn: $installPreferences.showInstallPopup) GroupBox { - HStack { - VStack(alignment: .leading) { - Toggle("preferences.toggle.alwaysInstallPlayTools", - isOn: $installPreferences.alwaysInstallPlayTools) + VStack { + HStack { + VStack(alignment: .leading) { + Toggle("preferences.toggle.alwaysInstallPlayTools", + isOn: $installPreferences.alwaysInstallPlayTools) + } + Spacer() } Spacer() + .frame(height: 20) } }.disabled(installPreferences.showInstallPopup) - Spacer() } .padding(20) - .frame(width: 350, height: 100, alignment: .center) + .frame(width: 400, height: 200) } } diff --git a/PlayCover/en.lproj/Localizable.strings b/PlayCover/en.lproj/Localizable.strings index c52fa1a8b..1edb0dfba 100644 --- a/PlayCover/en.lproj/Localizable.strings +++ b/PlayCover/en.lproj/Localizable.strings @@ -192,6 +192,7 @@ "settings.toggle.introspection" = "Insert Introspection libraries"; "settings.toggle.introspection.help" = "Add the system introspection libraries to the app. Known to fix issues with some apps not starting correctly"; +"settings.applicationCategoryType" = "Application Type"; "settings.removePlayTools" = "Remove PlayTools"; "settings.addToLaunchpad" = "Add to Launchpad"; "settings.removeFromLaunchpad" = "Remove from Launchpad";