diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 1209ebdb6..03497f111 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; }; 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; + 7CD054B52E253D2F00287A89 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */; }; CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; /* End PBXBuildFile section */ @@ -196,6 +197,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7CD054B52E253D2F00287A89 /* SwiftUIIntrospect in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, @@ -295,6 +297,7 @@ packageProductDependencies = ( 4EA5AE812D16807500AD0B9F /* SwiftSVG */, 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */, + 7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */, ); productName = MeloNX; productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */; @@ -387,6 +390,7 @@ packageReferences = ( 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */, + 7CD054B32E253D2F00287A89 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, ); preferredProjectObjectVersion = 56; productRefGroup = 4E80A98E2CD6F54500029585 /* Products */; @@ -642,7 +646,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 95J8WZ4TN8; + DEVELOPMENT_TEAM = D59DHVRS87; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; ENABLE_PREVIEWS = YES; ENABLE_TESTABILITY = NO; @@ -782,6 +786,9 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -1041,9 +1048,15 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = "$(VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; + PRODUCT_BUNDLE_IDENTIFIER = com.xitrix.MeloNX; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1066,7 +1079,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 95J8WZ4TN8; + DEVELOPMENT_TEAM = D59DHVRS87; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; ENABLE_PREVIEWS = YES; ENABLE_TESTABILITY = YES; @@ -1206,6 +1219,9 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -1465,9 +1481,15 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = "$(VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; + PRODUCT_BUNDLE_IDENTIFIER = com.xitrix.MeloNX; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1682,6 +1704,14 @@ kind = branch; }; }; + 7CD054B32E253D2F00287A89 /* 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 */ @@ -1695,6 +1725,11 @@ package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; productName = SwiftSVG; }; + 7CD054B42E253D2F00287A89 /* SwiftUIIntrospect */ = { + isa = XCSwiftPackageProductDependency; + package = 7CD054B32E253D2F00287A89 /* 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 cb3a468df..41572f58c 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" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390", + "originHash" : "ae170c69ea1ccacb2f3982b1a91b689a09bc4dcc59ed970f1df977bf7c7aed6f", "pins" : [ { "identity" : "cocoaasyncsocket", @@ -18,6 +18,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..ce3c3b6da --- /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 +@_spi(Advanced) import SwiftUIIntrospect + +public extension View { + func navigationItemBottomPalette(@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...), 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/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift index c6cbb3880..040eab209 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift @@ -96,9 +96,15 @@ struct GameInfoSheet: View { .navigationTitle(game.titleName) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Dismiss") { - presentationMode.wrappedValue.dismiss() + ToolbarItem(placement: .topBarTrailing) { + if #available(iOS 26, *) { + Button(role: .close) { + presentationMode.wrappedValue.dismiss() + } + } else { + Button("Dismiss") { + presentationMode.wrappedValue.dismiss() + } } } } 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 f8911a32b..14d55b1cc 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift @@ -78,28 +78,38 @@ 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) - } + Group { + // Game list + if Ryujinx.shared.games.isEmpty { + EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile) + } else { + gameListView + .animation(.easeInOut(duration: 0.3), value: searchText) + } + } + .navigationItemBottomPalette { + 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") @@ -164,27 +174,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 } @@ -654,7 +648,7 @@ struct GameLibraryHeader: View { // Stats cards StatCard( icon: "gamecontroller.fill", - title: "Total Games", + title: "Games", value: "\(totalGames)", color: .blue ) @@ -674,8 +668,7 @@ struct GameLibraryHeader: View { ) } .padding(.horizontal) - .padding(.top, 8) - .padding(.bottom, 4) + .padding(.bottom, 8) } } @@ -684,7 +677,9 @@ struct StatCard: View { let title: String let value: String let color: Color - + + @Namespace var statCardIdNamespace + var body: some View { VStack(alignment: .leading, spacing: 4) { HStack { @@ -700,11 +695,24 @@ struct StatCard: View { } .frame(maxWidth: .infinity, alignment: .leading) .padding(10) - .background(color.opacity(0.1)) - .cornerRadius(10) + .apply { + if #available(iOS 26, *) { + $0 + .glassEffect(.regular.tint(color.opacity(0.05)), in: RoundedRectangle(cornerRadius: 16)) + .glassEffectID("StatCardID", in: statCardIdNamespace) + } else { + $0 + .background(color.opacity(0.1)) + .cornerRadius(10) + } + } } } +extension View { + func apply(@ViewBuilder _ block: (Self) -> V) -> V { block(self) } +} + // MARK: - Game Card View struct GameCardView: View { let game: Game @@ -881,7 +889,9 @@ struct GameListRow: View { } } } - + + Spacer() + if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) { Image(systemName: "gearshape.circle") .resizable() @@ -889,8 +899,7 @@ struct GameListRow: View { .foregroundStyle(.blue) .frame(width: 20, height: 20) } - - Spacer() + VStack(alignment: .leading) { // Compatibility badges diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift index c53c8a99a..c094dbbf9 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift @@ -168,50 +168,44 @@ struct PerGameSettingsView: View { var body: some View { iOSNav { - ZStack { - Color(UIColor.systemBackground) - .ignoresSafeArea() - - VStack(spacing: 0) { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(PerSettingsCategory.allCases, id: \.id) { category in - CategoryButton( - title: category.rawValue, - icon: category.icon, - isSelected: selectedCategory == category - ) { - selectedCategory = category - } - } - } - .padding(.horizontal) - .padding(.vertical, 8) - } - - Divider() - - // Settings content - ScrollView { - VStack(spacing: 24) { - switch selectedCategory { - case .graphics: - graphicsSettings - .padding(.top) - case .system: - systemSettings - .padding(.top) - case .advanced: - advancedSettings - .padding(.top) + // Settings content + ScrollView { + VStack(spacing: 24) { + switch selectedCategory { + case .graphics: + graphicsSettings + .padding(.top) + case .system: + systemSettings + .padding(.top) + case .advanced: + advancedSettings + .padding(.top) - } - - Spacer(minLength: 50) - } - .padding(.bottom) } - .scrollDismissesKeyboardIfAvailable() + + Spacer(minLength: 50) + } + .padding(.bottom) + } + .background(Color(uiColor: .systemGroupedBackground)) + .scrollDismissesKeyboardIfAvailable() + .navigationItemBottomPalette { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(PerSettingsCategory.allCases, id: \.id) { category in + CategoryButton( + title: category.rawValue, + icon: category.icon, + isSelected: selectedCategory == category + ) { + selectedCategory = category + } + } + } + .defaultScrollAnchorIsAvailable(.center) + .padding(.horizontal) + .padding(.vertical, 8) } } .navigationTitle("Settings") 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 1abe0127f..7ed1f41a0 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift @@ -469,58 +469,32 @@ struct SettingsViewNew: View { var iOSSettings: 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 - } - } - } + // Settings content + ScrollView { + VStack(spacing: 24) { + 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) { - 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() + + Spacer(minLength: 50) } + .padding(.bottom) } + .scrollDismissesKeyboardIfAvailable() + .background(Color(uiColor: .systemGroupedBackground)) .navigationTitle("Settings") .navigationBarTitleDisplayMode(.large) // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic)) @@ -533,6 +507,25 @@ struct SettingsViewNew: View { settingsManager.saveSettings() } } + .navigationItemBottomPalette { + // 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 + } + } + } + .defaultScrollAnchorIsAvailable(.center) + .padding(.horizontal) + .padding(.vertical, 8) + } + } } } @@ -1517,12 +1510,23 @@ struct CategoryButton: View { } .foregroundColor(isSelected ? .blue : .secondary) .frame(width: 70, height: 56) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(isSelected ? Color.blue.opacity(0.15) : Color.clear) - ) + .animation(.bouncy(duration: 0.3), value: isSelected) } + .apply { + if #available(iOS 26, *) { + if isSelected { + $0.glassEffect(.regular.tint(Color.blue.opacity(0.15)), in: RoundedRectangle(cornerRadius: 16)) + } else { + $0 + } + } else { + $0.background( + RoundedRectangle(cornerRadius: 12) + .fill(isSelected ? Color.blue.opacity(0.15) : Color.clear) + ) + } + } } } @@ -1561,7 +1565,7 @@ struct SettingsCard: View { .padding() .background( RoundedRectangle(cornerRadius: 12) - .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white) + .fill(Color(.secondarySystemGroupedBackground)) .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) ) .padding(.horizontal) @@ -1686,3 +1690,14 @@ 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 + } + } +}