mirror of
https://git.743378673.xyz/MeloNX/MeloNX.git
synced 2025-06-27 19:06:23 +02:00
UINavigationBarItemPaletteView added
This commit is contained in:
parent
bff023563b
commit
1f243ef21b
6 changed files with 234 additions and 107 deletions
|
@ -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 */;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)!
|
||||
}
|
||||
}
|
|
@ -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<Self> {
|
||||
let vc = UIHostingController<Self>(rootView: self)
|
||||
if #available(iOS 16.4, *) {
|
||||
vc.safeAreaRegions = []
|
||||
}
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
var asView: some View {
|
||||
GenericControllerView(viewController: self)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue