diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index ef8ce7fa6..be6341527 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -28,16 +28,10 @@ 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; + D1C0A55D2DBFAAD3005AB251 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = D1C0A55C2DBFAAD3005AB251 /* SwiftUIIntrospect */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4E80A9852CD6F54500029585 /* Project object */; - proxyType = 1; - remoteGlobalIDString = BD43C6212D1B248D003BBC42; - remoteInfo = com.Stossy11.MeloNX.RyujinxAg; - }; 4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4E80A9852CD6F54500029585 /* Project object */; @@ -198,6 +192,7 @@ buildActionMask = 2147483647; files = ( CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, + D1C0A55D2DBFAAD3005AB251 /* SwiftUIIntrospect in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */, ); @@ -287,7 +282,6 @@ buildRules = ( ); dependencies = ( - 4E2953AC2D803BC9000497CD /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 4E80A98F2CD6F54500029585 /* MeloNX */, @@ -295,6 +289,7 @@ name = MeloNX; packageProductDependencies = ( 4EA5AE812D16807500AD0B9F /* SwiftSVG */, + D1C0A55C2DBFAAD3005AB251 /* SwiftUIIntrospect */, ); productName = MeloNX; productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */; @@ -386,6 +381,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, + D1C0A55B2DBFAAD3005AB251 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, ); preferredProjectObjectVersion = 56; productRefGroup = 4E80A98E2CD6F54500029585 /* Products */; @@ -473,12 +469,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */; - targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */; - }; 4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4E80A98C2CD6F54500029585 /* MeloNX */; @@ -1421,6 +1411,14 @@ kind = branch; }; }; + D1C0A55B2DBFAAD3005AB251 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/swiftui-introspect.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1429,6 +1427,11 @@ package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; productName = SwiftSVG; }; + D1C0A55C2DBFAAD3005AB251 /* SwiftUIIntrospect */ = { + isa = XCSwiftPackageProductDependency; + package = D1C0A55B2DBFAAD3005AB251 /* XCRemoteSwiftPackageReference "swiftui-introspect" */; + productName = SwiftUIIntrospect; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4E80A9852CD6F54500029585 /* Project object */; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5f080c7a7..f992285be 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805", + "originHash" : "9fd9e5cf42fe0cb11d840e36abe7fbfb590073df6eb786652581b3f6b11d599f", "pins" : [ { "identity" : "swiftsvg", @@ -9,6 +9,15 @@ "branch" : "master", "revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d" } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/swiftui-introspect.git", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } } ], "version" : 3 diff --git a/src/MeloNX/MeloNX/App/Views/Extensions/NavigationItemPalette.swift b/src/MeloNX/MeloNX/App/Views/Extensions/NavigationItemPalette.swift new file mode 100644 index 000000000..46ca16ec9 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Views/Extensions/NavigationItemPalette.swift @@ -0,0 +1,83 @@ +// +// NavigationItemPalette.swift +// iTorrent +// +// Created by Daniil Vinogradov on 14.11.2024. +// + +import UIKit +import SwiftUI +import SwiftUIIntrospect + +public extension View { + func navitaionItemBottomPalette(@ViewBuilder body: () -> (some View)) -> some View { + modifier(NavitaionItemBottomPaletteContent(content: body().asController)) + } +} + +struct NavitaionItemBottomPaletteContent: ViewModifier { + let content: UIViewController + + func body(content: Content) -> some View { + content + .introspect(.viewController, on: .iOS(.v14, .v15, .v16, .v17, .v18), customize: { viewController in + let view = self.content.view! + view.backgroundColor = .clear + let size = view.systemLayoutSizeFitting(.init(width: viewController.view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow) + viewController.navigationItem.setBottomPalette(view, height: size.height) + }) + } +} + +extension UINavigationItem { + func setBottomPalette(_ contentView: UIView?, height: CGFloat = 44) { + /// "_setBottomPalette:" + let selector = NSSelectorFromBase64String("X3NldEJvdHRvbVBhbGV0dGU6") + guard responds(to: selector) else { return } + perform(selector, with: Self.makeNavigationItemPalette(with: contentView, height: height)) + } + + private static func makeNavigationItemPalette(with contentView: UIView?, height: CGFloat) -> UIView? { + guard let contentView else { return nil } + contentView.translatesAutoresizingMaskIntoConstraints = false + + let contentViewHolder = UIView(frame: .init(x: 0, y: 0, width: 0, height: height)) + contentViewHolder.autoresizingMask = [.flexibleHeight] + contentViewHolder.addSubview(contentView) + NSLayoutConstraint.activate([ + contentViewHolder.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + contentViewHolder.topAnchor.constraint(equalTo: contentView.topAnchor), + contentView.trailingAnchor.constraint(equalTo: contentViewHolder.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: contentViewHolder.bottomAnchor), + ]) + + /// "_UINavigationBarPalette" + guard let paletteClass = NSClassFromBase64String("X1VJTmF2aWdhdGlvbkJhclBhbGV0dGU=") as? UIView.Type + else { return nil } + + /// "alloc" + /// "initWithContentView:" + guard let palette = paletteClass.perform(NSSelectorFromBase64String("YWxsb2M=")) + .takeUnretainedValue() + .perform(NSSelectorFromBase64String("aW5pdFdpdGhDb250ZW50Vmlldzo="), with: contentViewHolder) + .takeUnretainedValue() as? UIView + else { return nil } + + palette.preservesSuperviewLayoutMargins = true + return palette + } +} + +func NSSelectorFromBase64String(_ base64String: String) -> Selector { + NSSelectorFromString(String(base64: base64String)) +} + +func NSClassFromBase64String(_ aBase64ClassName: String) -> AnyClass? { + NSClassFromString(String(base64: aBase64ClassName)) +} + +extension String { + init(base64: String) { + self.init(data: Data(base64Encoded: base64)!, encoding: .utf8)! + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Extensions/UIKitSwiftUIInarop.swift b/src/MeloNX/MeloNX/App/Views/Extensions/UIKitSwiftUIInarop.swift new file mode 100644 index 000000000..4d5624cf8 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Views/Extensions/UIKitSwiftUIInarop.swift @@ -0,0 +1,36 @@ +// +// UIKitSwiftUIInarop.swift +// iTorrent +// +// Created by Daniil Vinogradov on 01/11/2023. +// + +import SwiftUI + +private struct GenericControllerView: UIViewControllerRepresentable { + let viewController: UIViewController + typealias UIViewControllerType = UIViewController + + func makeUIViewController(context: Context) -> UIViewController { + viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { /* Ignore */ } +} + +extension View { + @MainActor + var asController: UIHostingController { + let vc = UIHostingController(rootView: self) + if #available(iOS 16.4, *) { + vc.safeAreaRegions = [] + } + return vc + } +} + +extension UIViewController { + var asView: some View { + GenericControllerView(viewController: self) + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift index 029b1ca63..df4f688f4 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift @@ -61,28 +61,39 @@ struct GameLibraryView: View { var body: some View { iOSNav { - ZStack { - // Background color - Color(UIColor.systemBackground) - .ignoresSafeArea() - - VStack(spacing: 0) { - // Header with stats - if !Ryujinx.shared.games.isEmpty { - GameLibraryHeader( - totalGames: Ryujinx.shared.games.count, - recentGames: realRecentGames.count, - firmwareVersion: firmwareversion - ) - } - - // Game list - if Ryujinx.shared.games.isEmpty { - EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile) - } else { - gameListView - .animation(.easeInOut(duration: 0.3), value: searchText) - } + VStack(spacing: 0) { + // Game list + if Ryujinx.shared.games.isEmpty { + EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile) + } else { + gameListView + .animation(.easeInOut(duration: 0.3), value: searchText) + } + } + .navitaionItemBottomPalette { + // Header with stats + if !Ryujinx.shared.games.isEmpty { + GameLibraryHeader( + totalGames: Ryujinx.shared.games.count, + recentGames: realRecentGames.count, + firmwareVersion: firmwareversion + ) + .overlay(Group { + if ryujinx.jitenabled { + VStack { + HStack { + Spacer() + Circle() + .frame(width: 12, height: 12) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundColor(Color.green) + .padding() + } + Spacer() + } + } + }) } } .navigationTitle("Game Library") @@ -147,27 +158,11 @@ struct GameLibraryView: View { } } } - .overlay(Group { - if ryujinx.jitenabled { - VStack { - HStack { - Spacer() - Circle() - .frame(width: 12, height: 12) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .foregroundColor(Color.green) - .padding() - } - Spacer() - } - } - }) .onChange(of: startemu) { game in guard let game else { return } addToRecentGames(game) } - // .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers") + .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers") .onChange(of: searchText) { _ in isSearching = !searchText.isEmpty } @@ -290,7 +285,7 @@ struct GameLibraryView: View { gameRequirements: $gameRequirements, gameInfo: $gameInfo ) - .padding(.horizontal, 3) + .padding(.horizontal) .padding(.vertical, 8) } } @@ -596,7 +591,7 @@ struct GameLibraryHeader: View { // Stats cards StatCard( icon: "gamecontroller.fill", - title: "Total Games", + title: "Games", value: "\(totalGames)", color: .blue ) @@ -616,8 +611,7 @@ struct GameLibraryHeader: View { ) } .padding(.horizontal) - .padding(.top, 8) - .padding(.bottom, 4) + .padding(.bottom, 8) } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift index 9e1c7303b..565b99a60 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift @@ -113,58 +113,48 @@ struct SettingsView: View { var body: some View { iOSNav { - ZStack { - // Background color - Color(UIColor.systemBackground) - .ignoresSafeArea() - - VStack(spacing: 0) { - // Category selector - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(SettingsCategory.allCases, id: \.id) { category in - CategoryButton( - title: category.rawValue, - icon: category.icon, - isSelected: selectedCategory == category - ) { - selectedCategory = category - } - } - } + ScrollView { + VStack(spacing: 24) { + // Device Info Card + deviceInfoCard .padding(.horizontal) - .padding(.vertical, 8) + .padding(.top) + + switch selectedCategory { + case .graphics: + graphicsSettings + case .input: + inputSettings + case .system: + systemSettings + case .advanced: + advancedSettings + case .misc: + miscSettings } - - Divider() - - // Settings content - ScrollView { - VStack(spacing: 24) { - // Device Info Card - deviceInfoCard - .padding(.horizontal) - .padding(.top) - - switch selectedCategory { - case .graphics: - graphicsSettings - case .input: - inputSettings - case .system: - systemSettings - case .advanced: - advancedSettings - case .misc: - miscSettings - } - - Spacer(minLength: 50) - } - .padding(.bottom) - } - .scrollDismissesKeyboardIfAvailable() } + .padding(.bottom) + } + .scrollDismissesKeyboardIfAvailable() + .navitaionItemBottomPalette { + // Category selector + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(SettingsCategory.allCases, id: \.id) { category in + CategoryButton( + title: category.rawValue, + icon: category.icon, + isSelected: selectedCategory == category + ) { + selectedCategory = category + UIImpactFeedbackGenerator(style: .soft).impactOccurred() + } + } + } + .padding(.horizontal) + .padding(.bottom, 8) + } + .defaultScrollAnchorIsAvailable(.center) } .navigationTitle("Settings") .navigationBarTitleDisplayMode(.large) @@ -1193,3 +1183,15 @@ extension View { } } +// this code is used to enable the keyboard to be dismissed when scrolling if available on iOS 16+ +extension View { + @ViewBuilder + func defaultScrollAnchorIsAvailable(_ anchor: UnitPoint?) -> some View { + if #available(iOS 17.0, *) { + self.defaultScrollAnchor(anchor) + } else { + self + } + } +} +