diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 00000000..b599a8b7 --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,16 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "MNGA" + +[setup] +script = "" + +[[actions]] +name = "Run" +icon = "run" +command = "make launch" + +[[actions]] +name = "Build" +icon = "tool" +command = "make build" diff --git a/app/Shared/Localization/zh-Hans.lproj/Localizable.strings b/app/Shared/Localization/zh-Hans.lproj/Localizable.strings index e39b3494..51973861 100644 --- a/app/Shared/Localization/zh-Hans.lproj/Localizable.strings +++ b/app/Shared/Localization/zh-Hans.lproj/Localizable.strings @@ -156,6 +156,7 @@ "Appearance" = "外观"; "Custom Appearance" = "自定义外观"; "Theme Color" = "主题色"; +"Prefer High Refresh Rate" = "高刷新率优先"; "Hide Notification Shortcut" = "隐藏通知快捷入口"; "New Short Message" = "发表短消息"; "Send To" = "发送至"; diff --git a/app/Shared/MNGAApp.swift b/app/Shared/MNGAApp.swift index 62874d8e..5093296b 100644 --- a/app/Shared/MNGAApp.swift +++ b/app/Shared/MNGAApp.swift @@ -13,9 +13,7 @@ import TipKit struct MNGAApp: App { @ObserveInjection var forceRedraw - #if os(iOS) - @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - #endif + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject var prefs = PreferencesStorage() @StateObject var networkMonitor = NetworkMonitor() @@ -23,6 +21,7 @@ struct MNGAApp: App { init() { logger.info("MNGA Init") logicInitialConfigure() + ProMotionDisplayLink.shared.setEnabled(prefs.forceProMotionDisplayLink) #if DEBUG if prefs.debugResetTips, (try? Tips.resetDatastore()) != nil { @@ -50,6 +49,7 @@ struct MNGAApp: App { ContentView() .onChange(of: prefs.themeColor) { setupColor() } .onChange(of: prefs.alwaysPortraitOnPhone) { AppInterfaceOrientation.applyCurrentPreference() } + .onChange(of: prefs.forceProMotionDisplayLink) { ProMotionDisplayLink.shared.setEnabled($1) } .onAppear { setupColor() } .onAppear { AppInterfaceOrientation.applyCurrentPreference() } .environment(\.whatsNew, MNGAWhatsNew.environment) diff --git a/app/Shared/Storage/PreferencesStorage.swift b/app/Shared/Storage/PreferencesStorage.swift index ab795858..115a019f 100644 --- a/app/Shared/Storage/PreferencesStorage.swift +++ b/app/Shared/Storage/PreferencesStorage.swift @@ -33,6 +33,7 @@ class PreferencesStorage: ObservableObject { @AppStorage("useInsetGroupedModern") var useInsetGroupedModern = true @AppStorage("hideMNGAMeta") var hideMNGAMeta = false @AppStorage("showPlusInTitle") var showPlusInTitle = false + @AppStorage("forceProMotionDisplayLink") var forceProMotionDisplayLink = false @AppStorage(alwaysPortraitOnPhonePreferenceKey) var alwaysPortraitOnPhone = false @AppStorage("requestOption") var requestOptionWrapper = WrappedMessage(inner: RequestOption()) { diff --git a/app/Shared/Utilities/ProMotion.swift b/app/Shared/Utilities/ProMotion.swift new file mode 100644 index 00000000..ad70e325 --- /dev/null +++ b/app/Shared/Utilities/ProMotion.swift @@ -0,0 +1,50 @@ +// +// ProMotion.swift +// MNGA +// +// Created by Bugen Zhao on 2026/5/9. +// + +import Foundation +import QuartzCore +import UIKit + +final class ProMotionDisplayLink: NSObject { + static let shared = ProMotionDisplayLink() + + private var displayLink: CADisplayLink? + + func setEnabled(_ enabled: Bool) { + if enabled { + start() + } else { + stop() + } + } + + func start() { + guard displayLink == nil else { return } + + let targetFPS = Float(min(UIScreen.main.maximumFramesPerSecond, 120)) + let link = CADisplayLink(target: self, selector: #selector(tick)) + link.preferredFrameRateRange = CAFrameRateRange( + minimum: targetFPS, + maximum: targetFPS, + preferred: targetFPS, + ) + link.add(to: .main, forMode: .common) + displayLink = link + + logger.info("started ProMotion display link at \(targetFPS) FPS") + } + + func stop() { + guard let displayLink else { return } + displayLink.invalidate() + self.displayLink = nil + + logger.info("stopped ProMotion display link") + } + + @objc private func tick(_: CADisplayLink) {} +} diff --git a/app/Shared/Utilities/Window.swift b/app/Shared/Utilities/Window.swift index 4de0a6c8..3a40bd00 100644 --- a/app/Shared/Utilities/Window.swift +++ b/app/Shared/Utilities/Window.swift @@ -6,54 +6,51 @@ // import Foundation +import UIKit -#if canImport(UIKit) - import UIKit - - final class AppDelegate: NSObject, UIApplicationDelegate { - func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { - AppInterfaceOrientation.supportedOrientations - } +final class AppDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { + AppInterfaceOrientation.supportedOrientations } +} - enum AppInterfaceOrientation { - static var supportedOrientations: UIInterfaceOrientationMask { - guard UIDevice.current.userInterfaceIdiom == .phone else { - return .allButUpsideDown - } - return UserDefaults.standard.bool(forKey: alwaysPortraitOnPhonePreferenceKey) ? .portrait : .allButUpsideDown +enum AppInterfaceOrientation { + static var supportedOrientations: UIInterfaceOrientationMask { + guard UIDevice.current.userInterfaceIdiom == .phone else { + return .allButUpsideDown } + return UserDefaults.standard.bool(forKey: alwaysPortraitOnPhonePreferenceKey) ? .portrait : .allButUpsideDown + } - @MainActor - static func applyCurrentPreference() { - guard UIDevice.current.userInterfaceIdiom == .phone else { return } + @MainActor + static func applyCurrentPreference() { + guard UIDevice.current.userInterfaceIdiom == .phone else { return } - guard let windowScene = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .first(where: { $0.activationState == .foregroundActive }) ?? UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .first - else { return } + guard let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) ?? UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first + else { return } - windowScene.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() + windowScene.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() - let geometryPreferences = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: supportedOrientations) - windowScene.requestGeometryUpdate(geometryPreferences) { error in - logger.warning("failed to update interface orientation: \(error.localizedDescription)") - } + let geometryPreferences = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: supportedOrientations) + windowScene.requestGeometryUpdate(geometryPreferences) { error in + logger.warning("failed to update interface orientation: \(error.localizedDescription)") } } - - extension UIApplication { - static var myKeyWindow: UIWindow? { - // Get connected scenes - UIApplication.shared.connectedScenes - // Keep only the first `UIWindowScene` - .first(where: { $0 is UIWindowScene }) - // Get its associated windows - .flatMap { $0 as? UIWindowScene }?.windows - // Finally, keep only the key window - .first(where: \.isKeyWindow) - } +} + +extension UIApplication { + static var myKeyWindow: UIWindow? { + // Get connected scenes + UIApplication.shared.connectedScenes + // Keep only the first `UIWindowScene` + .first(where: { $0 is UIWindowScene }) + // Get its associated windows + .flatMap { $0 as? UIWindowScene }?.windows + // Finally, keep only the key window + .first(where: \.isKeyWindow) } -#endif +} diff --git a/app/Shared/Views/PreferencesView.swift b/app/Shared/Views/PreferencesView.swift index 23f27733..7c53692b 100644 --- a/app/Shared/Views/PreferencesView.swift +++ b/app/Shared/Views/PreferencesView.swift @@ -209,6 +209,13 @@ struct PreferencesInnerView: View { Text("Modern").tag(true) }.disableWithPlusCheck(.customAppearance) + if UIScreen.main.maximumFramesPerSecond > 60 { + Toggle(isOn: $pref.forceProMotionDisplayLink) { + Label("Prefer High Refresh Rate", systemImage: "bolt.fill") + } + .onChange(of: pref.forceProMotionDisplayLink) { ProMotionDisplayLink.shared.setEnabled($1) } + } + if UserInterfaceIdiom.current == .phone { Toggle(isOn: $pref.alwaysPortraitOnPhone) { Label("Lock Screen Rotation", systemImage: "iphone") diff --git a/app/iOS/Info.plist b/app/iOS/Info.plist index bce89aac..08d967da 100644 --- a/app/iOS/Info.plist +++ b/app/iOS/Info.plist @@ -31,6 +31,8 @@ CFBundleVersion 1 + CADisableMinimumFrameDurationOnPhone + ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS