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