From 48647d4d898123e57bce4407dcb30e81463bb395 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Wed, 12 Aug 2026 18:28:04 +0800 Subject: [PATCH] - update tip strings --- VoidLink/Input/ControllerNavigator.swift | 62 ++++++++-- VoidLink/Localization/Localizable.xcstrings | 107 +++++++++++++++--- VoidLink/Utility/GenericUtils.swift | 28 +++++ .../ViewControllers/SettingsViewController.m | 45 ++++++++ VoidLink/mul.lproj/iPad.xcstrings | 6 +- 5 files changed, 222 insertions(+), 26 deletions(-) diff --git a/VoidLink/Input/ControllerNavigator.swift b/VoidLink/Input/ControllerNavigator.swift index 8145dc4a..bfbe043c 100644 --- a/VoidLink/Input/ControllerNavigator.swift +++ b/VoidLink/Input/ControllerNavigator.swift @@ -152,6 +152,42 @@ private final class ControllerMouseCurvePreviewView: UIView { } } +private final class ControllerDrivenUIKitAnimationWakeToken { + private var wakeView: UIView? + private var toggled = false + + func wake(attachedTo view: UIView) { + let container = view.window ?? view + let wakeView = preparedWakeView(in: container) + + toggled.toggle() + UIView.animate( + withDuration: 0.6, + delay: 0, + options: [.allowUserInteraction, .beginFromCurrentState], + animations: { + wakeView.alpha = self.toggled ? 0.002 : 0.001 + } + ) + } + + private func preparedWakeView(in container: UIView) -> UIView { + if let wakeView, wakeView.superview === container { + return wakeView + } + + wakeView?.removeFromSuperview() + + let wakeView = UIView(frame: CGRect(x: -2, y: -2, width: 1, height: 1)) + wakeView.isUserInteractionEnabled = false + wakeView.backgroundColor = .black + wakeView.alpha = 0.001 + container.addSubview(wakeView) + self.wakeView = wakeView + return wakeView + } +} + @available(iOS 13.0, *) @objc protocol ControllerNavigatorRadialMenuDelegate: AnyObject { func controllerNavigatorDidSelect(item: RadialMenuItem) @@ -279,11 +315,16 @@ final class ControllerNavigator: NSObject { static var settingsReadTipSuppressNextRelease = false static var navigationTimer: SafeTimer? + private static let controllerDrivenUIKitAnimationWakeToken = ControllerDrivenUIKitAnimationWakeToken() @objc static weak var controllerNavigationHighlightedView: UIView? private static let installAlertControllerNavigationHook: Void = { UIAlertController.installControllerNavigationDelegateHook() }() + static func wakeControllerDrivenUIKitAnimationIfNeeded(attachedTo view: UIView) { + controllerDrivenUIKitAnimationWakeToken.wake(attachedTo: view) + } + @objc static func setRadialMenuDelegate(_ delegate: ControllerNavigatorRadialMenuDelegate?) { _ = installAlertControllerNavigationHook radialMenuDelegate = delegate @@ -1116,7 +1157,10 @@ extension SettingsViewController: ControllerUINavigationDelegate { if let highlightedView = ControllerNavigator.controllerNavigationHighlightedView, highlightedView is UIButton { if let section = highlightedView.superview as? MenuSectionView { - section.toggleFold() + ControllerNavigator.wakeControllerDrivenUIKitAnimationIfNeeded(attachedTo: highlightedView) + DispatchQueue.main.async{ + section.toggleFold() + } } } @@ -1124,12 +1168,16 @@ extension SettingsViewController: ControllerUINavigationDelegate { for view in stack.arrangedSubviews { if let selector = view as? UISegmentedControl { guard selector.isEnabled else {continue} - var targetIndex:Int = 0 - repeat { - targetIndex = selector.nextIndex(forward: forward) - selector.selectedSegmentIndex = targetIndex - } while !selector.isEnabledForSegment(at: targetIndex) - selector.sendActions(for: .valueChanged) + let updateSelector = { + var targetIndex:Int = 0 + repeat { + targetIndex = selector.nextIndex(forward: forward) + selector.selectedSegmentIndex = targetIndex + } while !selector.isEnabledForSegment(at: targetIndex) + selector.sendActions(for: .valueChanged) + } + ControllerNavigator.wakeControllerDrivenUIKitAnimationIfNeeded(attachedTo: selector) + DispatchQueue.main.async(execute: updateSelector) } if let uiSwitch = view as? UISwitch { guard uiSwitch.isEnabled else {continue} diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 09bf8676..082eba49 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -9356,25 +9356,25 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "- In general, higher interpolation resolutions require more processing power.\n\n- Most devices with A15 or newer chips can interpolate 60 FPS to 120 FPS at the default interpolation resolution (around 720p).\n\n- Upgrade to iOS 27 to unlock interpolation resolutions above 720p.\n\n- If you experience increased input latency, stuttering, or other issues, lower the interpolation resolution.\n\n- There is always a setting that balances image quality and performance for stable frame interpolation." + "value" : "- In general, higher interpolation resolutions require more processing power.\n\n- Most devices with A15 or newer chips can interpolate 60 FPS to 120 FPS at the default interpolation resolution (around 720p).\nRecommended configurations for your reference:\niPhone 13 Pro (A15) - 1344 × 624\niPad Pro 2024 (M4) - 1440 × 992\n\n- Upgrade to iOS 27 to unlock interpolation resolutions above 720p.\n\n- If you experience increased input latency, stuttering, or other issues, lower the interpolation resolution.\n\n- There is always a setting that balances image quality and performance for stable frame interpolation.\n\n- Older models may not be able to initiate the interpolator(such as A12 SoC devices)" } }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "- 一般而言,插帧分辨率越大,性能开销越大。\n\n- 大多数搭载 A15 或更新芯片的设备,可以在默认插帧分辨率(720p左右)下实现 60→120 FPS 插帧。\n\n- 如需解锁超过 720p 的插帧分辨率,请升级至 iOS 27。\n\n- 若出现操作延迟增大、卡顿等异常现象,请降低插帧分辨率。\n\n- 总有一个能够平衡画质与性能的档位,可以实现稳定插帧。" + "state" : "translated", + "value" : "- 一般而言,插帧分辨率越大,性能开销越大。\n\n- 大多数搭载 A15 或更新芯片的设备,可以在默认插帧分辨率(720p左右)下实现 60→120 FPS 插帧。\n以下配置供您参考:\niPhone 13 Pro (A15) - 1344 × 624\niPad Pro 2024 (M4) - 1440 × 992\n\n- 如需解锁超过 720p 的插帧分辨率,请升级至 iOS 27。\n\n- 若出现操作延迟增大、卡顿等异常现象,请降低插帧分辨率。\n\n- 总有一个能够平衡画质与性能的档位,可以实现稳定插帧。" } }, "zh-Hant" : { "stringUnit" : { - "state" : "needs_review", - "value" : "- 一般而言,影格插補解析度越高,效能負載越大。\n\n- 多數搭載 A15 或更新晶片的裝置,可在預設影格插補解析度(720p左右)下實現 60→120 FPS 影格插補。\n\n- 如需解鎖超過 720p 的影格插補解析度,請升級至 iOS 27。\n\n- 若出現操作延遲增加、卡頓等異常情況,請降低影格插補解析度。\n\n- 總能找到一個兼顧畫質與效能的設定,以穩定進行影格插補。" + "state" : "translated", + "value" : "- 一般而言,影格插補解析度越高,效能負載越大。\n\n- 多數搭載 A15 或更新晶片的裝置,可在預設影格插補解析度(720p左右)下實現 60→120 FPS 影格插補。\n以下配置供您参考:\niPhone 13 Pro (A15) - 1344 × 624\niPad Pro 2024 (M4) - 1440 × 992\n\n- 如需解鎖超過 720p 的影格插補解析度,請升級至 iOS 27。\n\n- 若出現操作延遲增加、卡頓等異常情況,請降低影格插補解析度。\n\n- 總能找到一個兼顧畫質與效能的設定,以穩定進行影格插補。" } }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", - "value" : "- 一般而言,影格插補解析度越高,效能負載越大。\n\n- 多數搭載 A15 或更新晶片的裝置,可在預設影格插補解析度(720p左右)下實現 60→120 FPS 影格插補。\n\n- 如需解鎖超過 720p 的影格插補解析度,請升級至 iOS 27。\n\n- 若出現操作延遲增加、卡頓等異常情況,請降低影格插補解析度。\n\n- 總能找到一個兼顧畫質與效能的設定,以穩定進行影格插補。" + "state" : "translated", + "value" : "- 一般而言,影格插補解析度越高,效能負載越大。\n\n- 多數搭載 A15 或更新晶片的裝置,可在預設影格插補解析度(720p左右)下實現 60→120 FPS 影格插補。\n以下配置供您参考:\niPhone 13 Pro (A15) - 1344 × 624\niPad Pro 2024 (M4) - 1440 × 992\n\n- 如需解鎖超過 720p 的影格插補解析度,請升級至 iOS 27。\n\n- 若出現操作延遲增加、卡頓等異常情況,請降低影格插補解析度。\n\n- 總能找到一個兼顧畫質與效能的設定,以穩定進行影格插補。" } } } @@ -11702,25 +11702,25 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy Frame Pacing and No Frame Pacing modes may reduce streaming performance or cause frequent jitter and stuttering, and do not support screen-off keep-alive or background streaming without Picture-in-Picture. These modes are intended only for the rare cases where the streamed image exhibits color corruption.\n\nTap [Confirm] to continue.\nTap [Cancel] to return to Queue Buffering mode." + "value" : "Legacy Frame Pacing and No Frame Pacing modes may reduce streaming performance or cause frequent jitter and stuttering, and do not support screen-off keep-alive or background streaming without Picture-in-Picture. \n\nTap [Confirm] to continue.\nTap [Cancel] to return to Queue Buffering mode." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "传统帧步进、无帧步进模式将导致串流性能衰减,或频繁的画面抖动和卡顿,且不支持熄屏保持以及无画中画的后台串流,仅适用于串流画面出现颜色异常的极少数情况。\n\n点【确定】以继续。\n点【取消】返回队列缓冲模式。" + "value" : "传统帧步进、无帧步进模式将导致串流性能衰减,或频繁的画面抖动和卡顿,且不支持熄屏保持以及无画中画的后台串流。\n\n点【确定】以继续。\n点【取消】返回队列缓冲模式。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "傳統影格步調、無影格步調模式可能導致串流效能下降,或頻繁發生畫面抖動與卡頓,且不支援熄屏保持及無子母畫面的背景串流,僅適用於極少數串流畫面出現色彩異常的情況。\n\n點選【確定】以繼續。\n點選【取消】返回佇列緩衝模式。" + "value" : "傳統影格步調、無影格步調模式可能導致串流效能下降,或頻繁發生畫面抖動與卡頓,且不支援熄屏保持及無子母畫面的背景串流。\n\n點選【確定】以繼續。\n點選【取消】返回佇列緩衝模式。" } }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", - "value" : "傳統影格步調、無影格步調模式可能導致串流效能下降,或頻繁發生畫面抖動與卡頓,僅適用於極少數串流畫面出現色彩異常的情況。\n\n點選【確定】以繼續。\n點選【取消】返回佇列緩衝模式。" + "state" : "translated", + "value" : "傳統影格步調、無影格步調模式可能導致串流效能下降,或頻繁發生畫面抖動與卡頓,且不支援熄屏保持及無子母畫面的背景串流。\n\n點選【確定】以繼續。\n點選【取消】返回佇列緩衝模式。" } } } @@ -17873,31 +17873,60 @@ } } }, + "streamDimensionScaleStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can use this setting to reduce the difference between the streaming resolution and the intermediate-frame resolution, minimizing interpolation-induced flickering for a more natural viewing experience." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以通过该设置减小串流分辨率与中间帧分辨率的差距,从而减少插帧造成的闪烁效应,从而获得更自然的观感。\n" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以透過此設定縮小串流解析度與中間影格解析度之間的差距,減少影格插補造成的閃爍,使畫面看起來更加自然。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以透過此設定縮小串流解析度與中間影格解析度之間的差距,減少影格插補造成的閃爍,使畫面看起來更加自然。" + } + } + } + }, "streamingRadialMenuButtonTip" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Press the same or a different button for controller navigation during streaming. [Home] button is also available.\n\nNote: if you want to designate the Home button, double tap to pass it through to VoidLink app on iOS 26+." + "value" : "Press the same or a different button for controller navigation during streaming. [Home] button is also available.\n\nNote: if you want to designate the Home button, double tap to pass it through to VoidLink app on iOS 26+.\nAlso, on iOS 27+, go to Settings -> General -> Game Controller -> Home Button Overrides, then add VoidLink to enable direct Home button passthrough." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "请按下相同或不同的按键,作为串流时的手柄导航键。\n您也可以指定 [Home] 鍵。\n\n注意:iOS 26+ 系统中,需要双击Home键才能将该按键透传至 VoidLink。" + "value" : "请按下相同或不同的按键,作为串流时的手柄导航键。\n您也可以指定 [Home] 鍵。\n\n注意:iOS 26+ 系统中,需要双击Home键才能将该按键透传至 VoidLink。\n另外,您可以在 iOS 27+ 系统中前往 设置 -> 通用 -> 游戏控制器 -> 主屏幕按钮覆盖,添加 VoidLink 以实现 Home 键直通。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "請按下相同或不同的按鍵,作為串流時的手把導覽鍵。 \n您也可以指定 [Home] 鍵。\n\n注意:在 iOS 26 以上系統中,需要雙擊 Home 鍵才能將該按鍵透傳至 VoidLink。" + "value" : "請按下相同或不同的按鍵,作為串流時的手把導覽鍵。 \n您也可以指定 [Home] 鍵。\n\n注意:在 iOS 26 以上系統中,需要雙擊 Home 鍵才能將該按鍵透傳至 VoidLink。\n另外,在 iOS 27 或以上系統中,前往「設定」->「一般」->「遊戲控制器」->「主畫面按鈕覆寫」,然後新增 VoidLink,即可啟用 Home 鍵直通。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "請按下相同或不同的按鍵,作為串流時的手把導覽鍵。 \n您也可以指定 [Home] 鍵。\n\n注意:在 iOS 26 以上系統中,需要雙擊 Home 鍵才能將該按鍵透傳至 VoidLink。" + "value" : "請按下相同或不同的按鍵,作為串流時的手把導覽鍵。 \n您也可以指定 [Home] 鍵。\n\n注意:在 iOS 26 以上系統中,需要雙擊 Home 鍵才能將該按鍵透傳至 VoidLink。\n另外,在 iOS 27 或以上系統中,前往「設定」->「一般」->「遊戲控制器」->「主畫面按鈕覆寫」,然後新增 VoidLink,即可啟用 Home 鍵直通。" } } } @@ -18397,6 +18426,29 @@ } } }, + "This device does not support frame interpolation." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前设备不支持插帧。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前裝置不支援影格插補。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前裝置不支援影格插補。" + } + } + } + }, "This functional widget cannot be combined with gamepad buttons" : { "extractionState" : "manual", "localizations" : { @@ -18466,6 +18518,29 @@ } } }, + "This system version does not support frame interpolation." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前系统版本不支持插帧。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前系統版本不支援影格插補。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前系統版本不支援影格插補。" + } + } + } + }, "This tip won't be shown again" : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/Utility/GenericUtils.swift b/VoidLink/Utility/GenericUtils.swift index dd7bc039..0991a940 100644 --- a/VoidLink/Utility/GenericUtils.swift +++ b/VoidLink/Utility/GenericUtils.swift @@ -89,6 +89,34 @@ import UIKit countdown: 8 ) } + + @objc public static func handleFrameInterpolationAvailabilityTip(in vc: UIViewController?) -> Bool { + guard #available(iOS 26.0, tvOS 26.0, *) else { + AlertControllerUtil.showAlert( + in: vc, + title: LocalizationHelper.localizedString(forKey: "Tips"), + message: LocalizationHelper.localizedString(forKey: "This system version does not support frame interpolation."), + withCancel: false, + buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"), + countdown: 0 + ) + return false + } + + guard FrameInterpolator.deviceSupportsInterpolation else { + AlertControllerUtil.showAlert( + in: vc, + title: LocalizationHelper.localizedString(forKey: "Tips"), + message: LocalizationHelper.localizedString(forKey: "This device does not support frame interpolation."), + withCancel: false, + buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"), + countdown: 0 + ) + return false + } + + return true + } @objc public static func needUpdateDefaultSettings() -> Bool { // let key = "needUpdateDefaultSettings20260226-1" diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 4408643e..bda20119 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -898,9 +898,11 @@ BOOL isCustomResolution(int resolutionSelected) { if(!FrameInterpolator.deviceSupportsInterpolation) [self.framePacingModeSelector setEnabled:false forSegmentAtIndex:FramePacingModeInterpolation]; self.interpolationLevelStack.hasDynamicLabel = YES; + self.interpolationLevelStack.hasInfoTag = YES; [self addSetting:self.interpolationLevelStack ofId:@"interpolationLevelStack" to:videoSection]; self.streamDimensionScaleStack.hasDynamicLabel = YES; + self.streamDimensionScaleStack.hasInfoTag = YES; [self addSetting:self.streamDimensionScaleStack ofId:@"streamDimensionScaleStack" to:videoSection]; self.frameQueueSizeStack.hasDynamicLabel = YES; @@ -1840,6 +1842,14 @@ BOOL isCustomResolution(int resolutionSelected) { tipText = [LocalizationHelper localizedStringForKey:@"globeAsEscapeStackTip"]; showOnlineDocAction = false; } + if([sender.superview.accessibilityIdentifier isEqualToString: @"interpolationLevelStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"frameInterpolationResolutionTip"]; + showOnlineDocAction = false; + } + if([sender.superview.accessibilityIdentifier isEqualToString: @"streamDimensionScaleStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"streamDimensionScaleStackTip"]; + showOnlineDocAction = false; + } UIStackView* parentStack = (UIStackView* )sender.superview; NSString* edgeSide = self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex == 1 ? [LocalizationHelper localizedStringForKey:@"left"] : [LocalizationHelper localizedStringForKey:@"right"]; @@ -2262,6 +2272,11 @@ BOOL isCustomResolution(int resolutionSelected) { [LocalizationHelper localizedStringForKey:@"Scaled Stream Resolution"]; [self.streamDimensionScaleSlider addTarget:self action:@selector(streamDimensionScaleSliderMoved:) forControlEvents:UIControlEventValueChanged]; [self.framePacingModeSelector addTarget:self action:@selector(framePacingModeChanged:) forControlEvents:UIControlEventValueChanged]; + UITapGestureRecognizer *interpolationAvailabilityTap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(framePacingModeSelectorTapped:)]; + interpolationAvailabilityTap.cancelsTouchesInView = NO; + [self.framePacingModeSelector addGestureRecognizer:interpolationAvailabilityTap]; [self.framePacingModeSelector sendActionsForControlEvents:UIControlEventValueChanged]; // [self.frameTimebaseSwitch setOn:self->tempSettings.enableFrameTimebase]; @@ -2742,6 +2757,17 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void)framePacingModeChanged:(UISegmentedControl *)sender { + if (sender.selectedSegmentIndex == FramePacingModeInterpolation) { + if (@available(iOS 26.0, tvOS 26.0, *)) { + if (!FrameInterpolator.deviceSupportsInterpolation) { + return; + } + } + else { + return; + } + } + // Hide frame queue size for Off and Legacy modes [GenericUtils handleLegacyFramePacingTipIn:self with:sender passAlert:settingsViewJustExpanded uiAction:^{ [self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue @@ -2794,6 +2820,25 @@ BOOL isCustomResolution(int resolutionSelected) { }*/ } +- (void)framePacingModeSelectorTapped:(UITapGestureRecognizer *)recognizer { + UISegmentedControl *selector = (UISegmentedControl *)recognizer.view; + if (selector.numberOfSegments == 0) { + return; + } + + CGFloat segmentWidth = CGRectGetWidth(selector.bounds) / selector.numberOfSegments; + NSInteger visualIndex = MIN((NSInteger)([recognizer locationInView:selector].x / segmentWidth), + selector.numberOfSegments - 1); + NSInteger segmentIndex = selector.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft + ? selector.numberOfSegments - visualIndex - 1 + : visualIndex; + if (segmentIndex != FramePacingModeInterpolation) { + return; + } + + [GenericUtils handleFrameInterpolationAvailabilityTipIn:self]; +} + - (void)onScreenWidgetChanged{ self.layoutOnScreenControlsVC = [self instantiateOscLayoutViewController]; diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 63f49acc..6fb6d44f 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -2690,19 +2690,19 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Pencil模式" + "value" : "Pencil模式(非内购功能)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Pencil模式" + "value" : "Pencil模式(非內購功能)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Pencil模式" + "value" : "Pencil模式(非內購功能)" } } }