From 54c106e01e0675dedd3df267e2cd2ef35e586340 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Wed, 1 Jul 2026 15:20:57 +0800 Subject: [PATCH] radial menu demo --- VoidLink.xcodeproj/project.pbxproj | 19 +- VoidLink/AboutView.swift | 6 +- VoidLink/AlertControllerUtil.swift | 2 +- .../AbstractGamepadOverlayWidgetView.swift | 2 +- VoidLink/Input/CommandManager.swift | 2 +- VoidLink/Input/InputAccessoryBar.swift | 8 +- VoidLink/Input/OnScreenWidgetView.swift | 10 +- VoidLink/Input/PressureCurve.swift | 6 +- VoidLink/Input/StreamView.m | 20 +- VoidLink/Localization/Localizable.xcstrings | 71 ++- VoidLink/ThemeManager.swift | 4 +- VoidLink/Utility/GenericUtils.swift | 461 +++--------------- VoidLink/Utility/GraphicUtils.swift | 2 +- VoidLink/Utility/IAPManager.swift | 8 +- VoidLink/Utility/PublicExtensions.swift | 185 +++++++ VoidLink/Utility/PublicUtils.swift | 146 ++++++ ...LayoutOnScreenControlsViewController.swift | 6 +- .../OSCProfilesTableViewController.swift | 8 +- .../ViewControllers/MainFrameViewController.m | 30 +- .../ViewControllers/SWRevealViewController.m | 18 +- .../ViewControllers/SettingsViewController.m | 20 +- .../StreamFrameViewController.m | 8 +- .../ToolboxViewController.swift | 20 +- VoidLinkPreview/RadialMenuView.swift | 433 ++++++++++++++++ .../VoidLinkPreview-Bridging-Header.h | 6 + VoidLinkPreview/VoidLinkPreview.swift | 2 +- 26 files changed, 1027 insertions(+), 476 deletions(-) create mode 100644 VoidLink/Utility/PublicExtensions.swift create mode 100644 VoidLink/Utility/PublicUtils.swift create mode 100644 VoidLinkPreview/RadialMenuView.swift create mode 100644 VoidLinkPreview/VoidLinkPreview-Bridging-Header.h diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index 5b490ffe..a043e213 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -34,9 +34,14 @@ 18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; }; 18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; }; 18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; }; + 2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; }; + 2A263D4C2FF3B49600154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; }; + 2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; }; + 2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; }; 2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; }; 2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; 2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; }; + 2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; }; 2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; }; 2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; }; 2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; }; @@ -205,6 +210,8 @@ 18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = ""; }; 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = ""; }; 2A039E562F72892B00546768 /* VoidLinkPreview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoidLinkPreview.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicExtensions.swift; sourceTree = ""; }; + 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUtils.swift; sourceTree = ""; }; 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = ""; }; 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = ""; }; @@ -218,7 +225,6 @@ 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControls.m; sourceTree = ""; }; 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnScreenButtonState.m; sourceTree = ""; }; 84351B0A2C29ACCA00A77989 /* OSCProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCProfile.m; sourceTree = ""; }; - A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesManager.swift; sourceTree = ""; }; 84351B0C2C29ACCB00A77989 /* LayoutOnScreenControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutOnScreenControls.h; sourceTree = ""; }; 84351B0D2C29ACCB00A77989 /* OSCProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfile.h; sourceTree = ""; }; 84351B292C29AD6000A77989 /* ToolBarContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ToolBarContainerView.m; sourceTree = ""; }; @@ -320,6 +326,7 @@ 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporarySettings.m; path = Database/TemporarySettings.m; sourceTree = ""; }; A1900E002F9A100100BA853A /* InputAccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAccessoryBar.swift; sourceTree = ""; }; A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractGamepadOverlayWidgetView.swift; sourceTree = ""; }; + A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesManager.swift; sourceTree = ""; }; DC1F5A05206436B10037755F /* ConnectionHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConnectionHelper.h; sourceTree = ""; }; DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = ""; }; FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = ""; }; @@ -430,6 +437,7 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( AbstractGamepadView.swift, + RadialMenuView.swift, VirtualKeyboardView.swift, WidgetPickerView.swift, WidgetPickerViewController.swift, @@ -819,6 +827,8 @@ children = ( 18729EDD2E8BD68800101194 /* UUIDHelper.swift */, 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */, + 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */, + 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */, 18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */, 18780CC92F10D6CC00B90483 /* IAPManager.swift */, 97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */, @@ -1113,8 +1123,11 @@ buildActionMask = 2147483647; files = ( 2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */, + 2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */, 2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */, + 2A263D4C2FF3B49600154DEA /* PublicExtensions.swift in Sources */, 2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */, + 2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1137,6 +1150,7 @@ 843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */, 97E20DEE2E2165A5000D5DCA /* ImGuiPlots.mm in Sources */, FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */, + 2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */, FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */, FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */, FB89463419F646E200339C8A /* Utils.m in Sources */, @@ -1147,6 +1161,7 @@ FB4678FF1A565DAC00377732 /* WakeOnLanManager.m in Sources */, 97E21BBF2E21882D000D5DCA /* imgui_tables.cpp in Sources */, 97E21BC02E21882D000D5DCA /* imgui_demo.cpp in Sources */, + 2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */, 97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */, 97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */, 97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */, @@ -1324,6 +1339,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "VoidLinkPreview/VoidLinkPreview-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; @@ -1373,6 +1389,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "VoidLinkPreview/VoidLinkPreview-Bridging-Header.h"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/VoidLink/AboutView.swift b/VoidLink/AboutView.swift index f6ca3ad2..f0bcb486 100644 --- a/VoidLink/AboutView.swift +++ b/VoidLink/AboutView.swift @@ -80,19 +80,19 @@ public struct AboutView: View { Link(LocalizationHelper.localizedString(forKey: "加入QQ群"), destination: URL(string: LocalizationHelper.localizedString(forKey: "https://qm.qq.com/q/uM51CYWLS2"))!) } } - if GenericUtils.isIPhone() { + if PublicUtils.isIPhone { Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!) } } .padding(.top, 10) - if !GenericUtils.isIPhone() { + if !PublicUtils.isIPhone { Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!) } HStack(spacing: 15) { if #available(iOS 15.0, *) { - if GenericUtils.isIPad() { + if PublicUtils.isIPad { Button(LocalizationHelper.localizedString(forKey: "I'm an artist")) { IAPManager.checkPurchaseInfo(.PencilProPack) { info in if !info.valid { diff --git a/VoidLink/AlertControllerUtil.swift b/VoidLink/AlertControllerUtil.swift index bb678ef7..dffe5cb9 100644 --- a/VoidLink/AlertControllerUtil.swift +++ b/VoidLink/AlertControllerUtil.swift @@ -157,7 +157,7 @@ import UIKit return } - if GenericUtils.isRunningOnMacAsiPadApp && buttonTitle != "" { + if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" { alertController.dismiss(animated: false) { showAlert( in: viewController, diff --git a/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift b/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift index ffb1b2f7..475f4695 100644 --- a/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift +++ b/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift @@ -166,7 +166,7 @@ final class AbstractGamepadOverlayView: UIView { private var hideCloseButtonWorkItem: DispatchWorkItem? private let minWidth: CGFloat = 70 private let maxWidth: CGFloat = 520 - private let doubleTapWidths: [CGFloat] = [GenericUtils.isIPhone() ? 90 : 130, GenericUtils.isIPhone() ? 165 : 200] + private let doubleTapWidths: [CGFloat] = [PublicUtils.isIPhone ? 90 : 130, PublicUtils.isIPhone ? 165 : 200] private lazy var closeButton: UIButton = makeCloseButton() @objc weak var closeButtonDelegate: AbstractGamepadOverlayCloseButtonDelegate? diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index b7339d04..acb6d883 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -542,7 +542,7 @@ import UIKit @objc public func extractCmdStrings(from input: String) -> [String]? { var validCmdStrings: [String] = [] - if GenericUtils.isGUIWidgetPickerAvailable { + if PublicUtils.isGUIWidgetPickerAvailable { let cmdStrings = input .split(separator: "-") .map { String($0) } diff --git a/VoidLink/Input/InputAccessoryBar.swift b/VoidLink/Input/InputAccessoryBar.swift index aa53f47b..2ad7a3c1 100644 --- a/VoidLink/Input/InputAccessoryBar.swift +++ b/VoidLink/Input/InputAccessoryBar.swift @@ -23,12 +23,12 @@ private enum InputAccessoryBarMetrics { static let horizontalInset: CGFloat = 12 static let safeAreaExtraInset: CGFloat = 0 static let interSectionSpacing: CGFloat = 10 - static let itemSpacing: CGFloat = GenericUtils.isIPhone() ? 5 : 9 + static let itemSpacing: CGFloat = PublicUtils.isIPhone ? 5 : 9 static let edgeFadeWidth: CGFloat = 14 - static let sideButtonSize: CGFloat = GenericUtils.isIPhone() ? 38 : 46 - static let pillHeight: CGFloat = GenericUtils.isIPhone() ? 38 : 46 + static let sideButtonSize: CGFloat = PublicUtils.isIPhone ? 38 : 46 + static let pillHeight: CGFloat = PublicUtils.isIPhone ? 38 : 46 static let pillHorizontalPadding: CGFloat = 10 - static let pillMinWidth: CGFloat = GenericUtils.isIPhone() ? 38 : 46 + static let pillMinWidth: CGFloat = PublicUtils.isIPhone ? 38 : 46 static let borderWidth: CGFloat = 1 static let highlightedBorderWidth: CGFloat = 1.6 static let highlightedScale: CGFloat = 0.86 diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index a201960d..def58142 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -591,8 +591,8 @@ import ObjectiveC.runtime } if self.widgetType == .button { if self.isFolder || self.isFunctionalButton { - self.widthFactor = GenericUtils.isIPhone() ? 0.88 : 1.17 - self.heightFactor = GenericUtils.isIPhone() ? 0.56 : 0.77 + self.widthFactor = PublicUtils.isIPhone ? 0.88 : 1.17 + self.heightFactor = PublicUtils.isIPhone ? 0.56 : 0.77 } } @@ -2965,7 +2965,7 @@ import ObjectiveC.runtime if ["com.voidlink.iOS" , "com.voidlinkextreme.iOS" , "com.voidlink.tf.debug10.iOS" - ].contains(Bundle.main.bundleIdentifier) && GenericUtils.isIPad() { + ].contains(Bundle.main.bundleIdentifier) && PublicUtils.isIPad { self.functionalWidgetDelegate?.presentPressureCurveVC() } case "DISABLETOUCH": @@ -3423,7 +3423,7 @@ import ObjectiveC.runtime } // MARK: - Auto Dock - private static let autoDockExposedEdgeLength: CGFloat = GenericUtils.isIPhone() ? 70 : 90 + private static let autoDockExposedEdgeLength: CGFloat = PublicUtils.isIPhone ? 70 : 90 private static let autoDockExposedThickness: CGFloat = 17 private static let autoDockVerticalInset: CGFloat = 12 @objc var autoDockIdleDuration: TimeInterval = 0 @@ -3665,7 +3665,7 @@ import ObjectiveC.runtime self.frame = targetFrame self.transform = CGAffineTransform(scaleX: 0.985, y: 0.985) - if ControllerUtil.activeGCControllers.count > 0, !GenericUtils.iOS26Available { + if ControllerUtil.activeGCControllers.count > 0, !PublicUtils.iOS26Available { self.parentViewController?.setNeedsUpdateOfHomeIndicatorAutoHidden() } diff --git a/VoidLink/Input/PressureCurve.swift b/VoidLink/Input/PressureCurve.swift index cf459364..5b0004f0 100644 --- a/VoidLink/Input/PressureCurve.swift +++ b/VoidLink/Input/PressureCurve.swift @@ -661,7 +661,7 @@ class PressureCurveViewController: UIViewController, UIGestureRecognizerDelegate hookFilterLabel.font = UIFont.systemFont(ofSize: 14) hookFilterLabel.textColor = .black - if GenericUtils.liquidGlassEnabled { + if PublicUtils.liquidGlassEnabled { if #available(iOS 13.0, *) {hookFilterSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)} } hookFilterSlider.minimumValue = 1 @@ -670,7 +670,7 @@ class PressureCurveViewController: UIViewController, UIGestureRecognizerDelegate strokeEqualizationDepthLabel.font = UIFont.systemFont(ofSize: 14) strokeEqualizationDepthLabel.textColor = .black - if GenericUtils.liquidGlassEnabled { + if PublicUtils.liquidGlassEnabled { if #available(iOS 13.0, *) {strokeEqualizationDepthSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)} } strokeEqualizationDepthSlider.minimumValue = 1 @@ -679,7 +679,7 @@ class PressureCurveViewController: UIViewController, UIGestureRecognizerDelegate strokeEqualizationStrengthLabel.font = UIFont.systemFont(ofSize: 14) strokeEqualizationStrengthLabel.textColor = .black - if GenericUtils.liquidGlassEnabled { + if PublicUtils.liquidGlassEnabled { if #available(iOS 13.0, *) {strokeEqualizationStrengthSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)} } strokeEqualizationStrengthSlider.minimumValue = 0.1 diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 0f086c8c..ec05d3fb 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -93,7 +93,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; streamConfig:(StreamConfiguration*)streamConfig gameProfile:(OSCProfile* )profile streamFrameTopLayerView:(UIView* )topLayerView{ - if(!self.streamFrameVC) self.streamFrameVC = (StreamFrameViewController* )[GenericUtils parentViewControllerForView:self]; + if(!self.streamFrameVC) self.streamFrameVC = (StreamFrameViewController* )[PublicUtils parentViewControllerForView:self]; self->comboKeyModifierFlags = (UIKeyModifierControl|UIKeyModifierAlternate|UIKeyModifierShift); @@ -129,7 +129,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; keyboardToggleTip.layer.cornerRadius = 10; keyboardToggleTip.clipsToBounds = true; - designatedSoftKeyboardHeight = settings.softKeyboardHeight * GenericUtils.screenHeight; + designatedSoftKeyboardHeight = settings.softKeyboardHeight * PublicUtils.screenHeight; keyboardHeightDesignatedForLandscape = designatedSoftKeyboardHeight != 0; // if(touchMode == NativeTouchOnly) [self addGestureRecognizer:keyboardToggleRecognizer]; //keep legacy approach in pure native mode @@ -279,7 +279,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; NSDictionary *userInfo = notification.userInfo; // Get the keyboard size from the notification CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - CGFloat screenHeight = GenericUtils.screenHeight; + CGFloat screenHeight = PublicUtils.screenHeight; CGFloat totalKeyboardHeight = keyboardFrame.size.height; CGFloat toolbarHeight = settings.showKeyboardToolbar ? GenericUtils.inputAccessoryBarHeight : 0; @@ -299,7 +299,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; bool useDesignatedKeyboardHeight = false; if (@available(iOS 13.0, *)) { - useDesignatedKeyboardHeight = GenericUtils.isLandscape && keyboardHeightDesignatedForLandscape; + useDesignatedKeyboardHeight = PublicUtils.isLandscape && keyboardHeightDesignatedForLandscape; totalKeyboardHeight = useDesignatedKeyboardHeight ? designatedSoftKeyboardHeight+toolbarHeight : totalKeyboardHeight; } else { @@ -307,7 +307,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; totalKeyboardHeight = useDesignatedKeyboardHeight ? designatedSoftKeyboardHeight+toolbarHeight : totalKeyboardHeight; } - HeightViewLiftedTo = totalKeyboardHeight - keyboardToggleRecognizer.lowestTouchPointHeight + GenericUtils.screenHeight * (useDesignatedKeyboardHeight ? 0.1 : 0.15); // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing. + HeightViewLiftedTo = totalKeyboardHeight - keyboardToggleRecognizer.lowestTouchPointHeight + PublicUtils.screenHeight * (useDesignatedKeyboardHeight ? 0.1 : 0.15); // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing. if(HeightViewLiftedTo < 0) HeightViewLiftedTo = 0; // set HeightViewLiftedTo to 0 if it is high enough and not going to be covered by keyboard. CGRect liftedStreamFrame = self.frame; liftedStreamFrame.origin.y -= HeightViewLiftedTo; @@ -479,7 +479,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO isDoneButton:false]; UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; [customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]]; - if (GenericUtils.liquidGlassEnabled) { + if (PublicUtils.liquidGlassEnabled) { if (@available(iOS 26.0, *)) { for(UIBarButtonItem *button in customToolbarView.items){ button.hidesSharedBackground = true; @@ -695,7 +695,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; MotionHandler* motionHandler = [MotionHandler sharedWithProfile:profile]; // get streamFrameVC - if(!self.streamFrameVC) self.streamFrameVC = (StreamFrameViewController* )[GenericUtils parentViewControllerForView:self]; + if(!self.streamFrameVC) self.streamFrameVC = (StreamFrameViewController* )[PublicUtils parentViewControllerForView:self]; /* if (@available(iOS 13.0, *)) { @@ -1092,11 +1092,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; // [button setTitle:@"666" forState:UIControlStateNormal]; [button setImage:image forState:UIControlStateNormal]; - button.frame = GenericUtils.liquidGlassEnabled ? CGRectMake(0, 0, 30, 30) : CGRectMake(0, 0, 30, 30); + button.frame = PublicUtils.liquidGlassEnabled ? CGRectMake(0, 0, 30, 30) : CGRectMake(0, 0, 30, 30); button.imageView.contentMode = UIViewContentModeScaleAspectFit; button.imageView.backgroundColor = backgroundColor; button.imageView.layer.cornerRadius = 10.0; - button.imageEdgeInsets = (GenericUtils.liquidGlassEnabled + button.imageEdgeInsets = (PublicUtils.liquidGlassEnabled ? (isDoneButton ? UIEdgeInsetsMake(16, 16, 16, 16) : UIEdgeInsetsMake(27.5, 27.5, 27.5, 27.5)) : UIEdgeInsetsMake(6, 6, 6, 6)); [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside]; @@ -1114,7 +1114,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; isOn = !isOn; // Update the button's appearance based on its new state if (isOn) { - sender.imageView.backgroundColor = GenericUtils.liquidGlassEnabled ? [UIColor.systemGrayColor colorWithAlphaComponent:0.5] : [UIColor lightGrayColor]; + sender.imageView.backgroundColor = PublicUtils.liquidGlassEnabled ? [UIColor.systemGrayColor colorWithAlphaComponent:0.5] : [UIColor lightGrayColor]; } else { sender.imageView.backgroundColor = [UIColor blackColor]; } diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index e7c569c5..2356409e 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -8151,6 +8151,29 @@ } } }, + "Game Profiles" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "游戏配置" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "遊戲配置" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "遊戲配置" + } + } + } + }, "Gamepad" : { "extractionState" : "manual", "localizations" : { @@ -9056,6 +9079,29 @@ } } }, + "Host View" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主机视图" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機視圖" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "主機視圖" + } + } + } + }, "Host information updated" : { "extractionState" : "manual", "localizations" : { @@ -14693,6 +14739,29 @@ } } }, + "Settings Menu" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置菜单" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定選單" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定選單" + } + } + } + }, "Shape" : { "extractionState" : "manual", "localizations" : { @@ -17456,4 +17525,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/VoidLink/ThemeManager.swift b/VoidLink/ThemeManager.swift index f4479d0b..3f326c23 100644 --- a/VoidLink/ThemeManager.swift +++ b/VoidLink/ThemeManager.swift @@ -148,7 +148,7 @@ class ThemeManager: NSObject { case .light: if #available(iOS 13.0, *) { let lightTraits = UITraitCollection(userInterfaceStyle: .light) - return GenericUtils.liquidGlassEnabled ? legacySepratorColor : UIColor.separator.resolvedColor(with: lightTraits) + return PublicUtils.liquidGlassEnabled ? legacySepratorColor : UIColor.separator.resolvedColor(with: lightTraits) } else { return UIColor( red: 214.0/255.0, @@ -160,7 +160,7 @@ class ThemeManager: NSObject { default: if #available(iOS 13.0, *) { let darkTraits = UITraitCollection(userInterfaceStyle: .dark) - return GenericUtils.liquidGlassEnabled ? legacySepratorColor : UIColor.separator.resolvedColor(with: darkTraits) + return PublicUtils.liquidGlassEnabled ? legacySepratorColor : UIColor.separator.resolvedColor(with: darkTraits) } else {return UIColor(white: 0.28, alpha: 1)} } } diff --git a/VoidLink/Utility/GenericUtils.swift b/VoidLink/Utility/GenericUtils.swift index 9a93cd40..17699e02 100644 --- a/VoidLink/Utility/GenericUtils.swift +++ b/VoidLink/Utility/GenericUtils.swift @@ -13,13 +13,13 @@ import ObjectiveC import UIKit @objc public class GenericUtils: NSObject { - @objc static func installSegmentedControlPreviousSelectionTracking() { + @objc public static func installSegmentedControlPreviousSelectionTracking() { UISegmentedControl.installPreviousSelectionTracking() } - @objc static var hardwareKeyboardAlreadyDetected: Bool = false + @objc public static var hardwareKeyboardAlreadyDetected: Bool = false - @objc static func isHardwareKeyboardConnected() -> Bool { + @objc public static func isHardwareKeyboardConnected() -> Bool { if #available(iOS 14.0, tvOS 14.0, *) { hardwareKeyboardAlreadyDetected = GCKeyboard.coalesced != nil return GCKeyboard.coalesced != nil @@ -27,7 +27,7 @@ import UIKit return false } - @objc static func isFirstHardwareKeyboardOrMouseConnection() -> Bool { + @objc public static func isFirstHardwareKeyboardOrMouseConnection() -> Bool { let key = "hasConnectedHardwareKeyboardOrMouse" guard !UserDefaults.standard.bool(forKey: key) else { return false @@ -36,8 +36,8 @@ import UIKit return true } - @objc static func handleKeyboardOrMouseConnectionTip(in vc: UIViewController?) { - if isRunningOnMacAsiPadApp { + @objc public static func handleKeyboardOrMouseConnectionTip(in vc: UIViewController?) { + if PublicUtils.isRunningOnMacAsiPadApp { return } if hardwareKeyboardAlreadyDetected { @@ -56,7 +56,7 @@ import UIKit } } - @objc static func needUpdateDefaultSettings() -> Bool { + @objc public static func needUpdateDefaultSettings() -> Bool { // let key = "needUpdateDefaultSettings20260226-1" let key = "needUpdateDefaultSettings20260408-3" guard !UserDefaults.standard.bool(forKey: key) else { @@ -66,7 +66,7 @@ import UIKit return true } - @objc static func needUpdatePartialSettings() -> Bool { + @objc public static func needUpdatePartialSettings() -> Bool { // let key = "needUpdateDefaultSettings20260226-1" let key = "needUpdatePartialSettings20260620" guard !UserDefaults.standard.bool(forKey: key) else { @@ -76,7 +76,7 @@ import UIKit return true } - @objc static func isEnableOswForNativeTouchSwitchFirstFlipping() -> Bool { + @objc public static func isEnableOswForNativeTouchSwitchFirstFlipping() -> Bool { let key = "enableOswForNativeTouchSwitchFlipped" guard !UserDefaults.standard.bool(forKey: key) else { return false @@ -85,7 +85,7 @@ import UIKit return true } - @objc static func isFirstLaunchPressureCurveTool() -> Bool { + @objc public static func isFirstLaunchPressureCurveTool() -> Bool { let key = "hasLaunchedPressureCurveTool" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -96,7 +96,7 @@ import UIKit return false } - @objc static func isFirstLaunchGamepadOverlayFeature() -> Bool { + @objc public static func isFirstLaunchGamepadOverlayFeature() -> Bool { let key = "hasTouchedGamepadOverlayFeature20260405-2" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -107,7 +107,7 @@ import UIKit return false } - @objc static func isFirstTappingGameProfileSelectorFromMainFrame() -> Bool { + @objc public static func isFirstTappingGameProfileSelectorFromMainFrame() -> Bool { let key = "hasTappedGameProfileSelectorFromMainFrame-6" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -118,7 +118,7 @@ import UIKit return false } - @objc static func isFirstTappingFolderInLayoutTool() -> Bool { + @objc public static func isFirstTappingFolderInLayoutTool() -> Bool { let key = "hasTappedFolderInLayoutTool2" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -129,8 +129,8 @@ import UIKit return false } - @objc static var hasTappedMagnifier = false - @objc static func isFirstTappingMagnifier() -> Bool { + @objc public static var hasTappedMagnifier = false + @objc public static func isFirstTappingMagnifier() -> Bool { guard !hasTappedMagnifier else { return false } hasTappedMagnifier = true let key = "hasTappedMagnifier2" @@ -142,7 +142,7 @@ import UIKit } return false } - @objc static func handleMagnifierTip(in vc: UIViewController?) { + @objc public static func handleMagnifierTip(in vc: UIViewController?) { if isFirstTappingMagnifier() { AlertControllerUtil.showAlert( in: vc, @@ -155,8 +155,8 @@ import UIKit } } - @objc static var hasTappedVelocityBasedTouchpad = false - @objc static func isFirstTappingVelocityBasedTouchpad() -> Bool { + @objc public static var hasTappedVelocityBasedTouchpad = false + @objc public static func isFirstTappingVelocityBasedTouchpad() -> Bool { guard !hasTappedVelocityBasedTouchpad else { return false } hasTappedVelocityBasedTouchpad = true let key = "hasTappedVelocityBasedTouchpad6" @@ -168,7 +168,7 @@ import UIKit } return false } - @objc static func handleVelocityBasedTouchpadTip(in vc: UIViewController?) { + @objc public static func handleVelocityBasedTouchpadTip(in vc: UIViewController?) { if isFirstTappingVelocityBasedTouchpad() { AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial") AlertControllerUtil.showAlert( @@ -180,15 +180,15 @@ import UIKit countdown: 5, completion: { if AlertControllerUtil.actionCancelled { - GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "velocityBasedTouchpadLink")) + PublicUtils.openUrl(LocalizationHelper.localizedString(forKey: "velocityBasedTouchpadLink")) } } ) } } - @objc static var hasTappedSlideAndHoldFolderButton = false - @objc static func isFirstTappingSlideAndHoldFolderButton() -> Bool { + @objc public static var hasTappedSlideAndHoldFolderButton = false + @objc public static func isFirstTappingSlideAndHoldFolderButton() -> Bool { guard !hasTappedSlideAndHoldFolderButton else { return false } hasTappedSlideAndHoldFolderButton = true let key = "hasTappedSlideAndHoldFolderButton2" @@ -200,7 +200,7 @@ import UIKit } return false } - @objc static func handleSlideAndHoldFolderButtonTip(in vc: UIViewController?) { + @objc public static func handleSlideAndHoldFolderButtonTip(in vc: UIViewController?) { if isFirstTappingSlideAndHoldFolderButton() { AlertControllerUtil.showAlert( in: vc, @@ -213,8 +213,8 @@ import UIKit } } - @objc static var hasTappedGamingLayoutFolderInEditMode: Bool = false - @objc static func isFirstTappingGamingLayoutFolderInEditMode() -> Bool { + @objc public static var hasTappedGamingLayoutFolderInEditMode: Bool = false + @objc public static func isFirstTappingGamingLayoutFolderInEditMode() -> Bool { guard !hasTappedGamingLayoutFolderInEditMode else { return false } hasTappedGamingLayoutFolderInEditMode = true let key = "hasTappedGamingLayoutFolderInEditMode" @@ -226,7 +226,7 @@ import UIKit } return false } - @objc static func handleGamingLayoutFolderTip(in vc: UIViewController?) { + @objc public static func handleGamingLayoutFolderTip(in vc: UIViewController?) { if isFirstTappingGamingLayoutFolderInEditMode() { AlertControllerUtil.showAlert( in: vc, @@ -239,7 +239,7 @@ import UIKit } } - @objc static func isFirstEnteringLayoutTool() -> Bool { + @objc public static func isFirstEnteringLayoutTool() -> Bool { let key = "hasEnteredLayoutTool1" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -249,7 +249,7 @@ import UIKit } return false } - @objc static func handleLayoutToolTip(in vc: UIViewController?) { + @objc public static func handleLayoutToolTip(in vc: UIViewController?) { if isFirstEnteringLayoutTool() { AlertControllerUtil.showAlert( in: vc, @@ -262,8 +262,8 @@ import UIKit } } - @objc static var pencilProPurchaseProcessedWithImportingWidgetTemplates: Bool = false - @objc static func handleAddOnProductPurchaseIntent(for product:AddOnProduct) { + @objc public static var pencilProPurchaseProcessedWithImportingWidgetTemplates: Bool = false + @objc public static func handleAddOnProductPurchaseIntent(for product:AddOnProduct) { let key = "addOnProduct_\(product.productId())_purchased" let defaults = UserDefaults.standard let purchased = defaults.bool(forKey: key) @@ -277,8 +277,8 @@ import UIKit } } - @objc static var hasTappedOnscreenGyroButton = false - @objc static func isFirstTappingOnscreenGyroButton() -> Bool { + @objc public static var hasTappedOnscreenGyroButton = false + @objc public static func isFirstTappingOnscreenGyroButton() -> Bool { guard !hasTappedOnscreenGyroButton else { return false } hasTappedOnscreenGyroButton = true let key = "hasTappedOnscreenGyroButton" @@ -290,7 +290,7 @@ import UIKit } return false } - @objc static func handleGyroButtonTip(in vc: UIViewController?) { + @objc public static func handleGyroButtonTip(in vc: UIViewController?) { if isFirstTappingOnscreenGyroButton() { AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial") AlertControllerUtil.showAlert( @@ -302,15 +302,15 @@ import UIKit countdown: 5, completion: { if AlertControllerUtil.actionCancelled { - GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "yourMotionControlSoution")) + PublicUtils.openUrl(LocalizationHelper.localizedString(forKey: "yourMotionControlSoution")) } } ) } } - @objc static var hasTappedButtonModeSelector = false - @objc static func isFirstTappingButtonModeSelector() -> Bool { + @objc public static var hasTappedButtonModeSelector = false + @objc public static func isFirstTappingButtonModeSelector() -> Bool { guard !hasTappedButtonModeSelector else { return false } hasTappedButtonModeSelector = true let key = "hasTappedButtonModeSelector3" @@ -322,7 +322,7 @@ import UIKit } return false } - @objc static func handleButtonModeTip(in vc: UIViewController?) { + @objc public static func handleButtonModeTip(in vc: UIViewController?) { if isFirstTappingButtonModeSelector() { AlertControllerUtil.showAlert( in: vc, @@ -332,15 +332,15 @@ import UIKit buttonTitle: LocalizationHelper.localizedString(forKey: "Tutorial"), countdown: 3, completion: { - GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "buttonModeLink")) + PublicUtils.openUrl(LocalizationHelper.localizedString(forKey: "buttonModeLink")) } ) } } - @objc static var hasTappedStickWheel = false - @objc static func isFirstTappingStickWheel() -> Bool { + @objc public static var hasTappedStickWheel = false + @objc public static func isFirstTappingStickWheel() -> Bool { guard !hasTappedStickWheel else { return false } hasTappedStickWheel = true let key = "hasTappedStickWheel" @@ -352,7 +352,7 @@ import UIKit } return false } - @objc static func handleStickWheelTip(in vc: UIViewController?) { + @objc public static func handleStickWheelTip(in vc: UIViewController?) { if isFirstTappingStickWheel() { AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial") AlertControllerUtil.showAlert( @@ -364,15 +364,15 @@ import UIKit countdown: 5, completion: { if AlertControllerUtil.actionCancelled { - GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "stickWheelLink")) + PublicUtils.openUrl(LocalizationHelper.localizedString(forKey: "stickWheelLink")) } } ) } } - @objc static var hasTappedWidgetPanel = false - @objc static func isFirstTappingWidgetPanel() -> Bool { + @objc public static var hasTappedWidgetPanel = false + @objc public static func isFirstTappingWidgetPanel() -> Bool { guard !hasTappedWidgetPanel else { return false } hasTappedWidgetPanel = true let key = "hasTappedWidgetPanel7" @@ -384,7 +384,7 @@ import UIKit } return false } - @objc static func handleWidgetPanelTip(in vc: UIViewController?) { + @objc public static func handleWidgetPanelTip(in vc: UIViewController?) { if isFirstTappingWidgetPanel() { // AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial") AlertControllerUtil.showAlert( @@ -396,15 +396,15 @@ import UIKit countdown: 6, completion: { if AlertControllerUtil.actionCancelled { - GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "widgetPanelLink")) + PublicUtils.openUrl(LocalizationHelper.localizedString(forKey: "widgetPanelLink")) } } ) } } - @objc static var hasChangedTouchMode = false - @objc static func isFirstChangingTouchMode() -> Bool { + @objc public static var hasChangedTouchMode = false + @objc public static func isFirstChangingTouchMode() -> Bool { guard !hasChangedTouchMode else { return false } hasChangedTouchMode = true let key = "hasChangedTouchMode5" @@ -416,7 +416,7 @@ import UIKit } return false } - @objc static func handleTouchModeChangingTip(in vc: UIViewController?) { + @objc public static func handleTouchModeChangingTip(in vc: UIViewController?) { if isFirstChangingTouchMode() { AlertControllerUtil.showAlert( in: vc, @@ -431,7 +431,7 @@ import UIKit } } - @objc static func isFirstTappingInputAccessoryBar() -> Bool { + @objc public static func isFirstTappingInputAccessoryBar() -> Bool { let key = "isFirstTappingInputAccessoryBar" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -442,8 +442,8 @@ import UIKit return false } - @objc static func isFirstStreamingOnMac() -> Bool { - if !isRunningOnMacAsiPadApp {return false} + @objc public static func isFirstStreamingOnMac() -> Bool { + if !PublicUtils.isRunningOnMacAsiPadApp {return false} let key = "hasStreamedOnMac" let defaults = UserDefaults.standard let launchedBefore = defaults.bool(forKey: key) @@ -454,7 +454,7 @@ import UIKit return false } - @objc static func handleLegacyFramePacingTip(in vc: UIViewController?, with selector: UISegmentedControl, passAlert: Bool = false, uiAction: (() -> Void)? = nil) { + @objc public static func handleLegacyFramePacingTip(in vc: UIViewController?, with selector: UISegmentedControl, passAlert: Bool = false, uiAction: (() -> Void)? = nil) { if passAlert || selector.selectedSegmentIndex == FramePacingMode.queue.rawValue { uiAction?() return @@ -478,103 +478,63 @@ import UIKit ) } - @objc static func gamepadOverlayFeatureTipTitle() -> String { + @objc public static func gamepadOverlayFeatureTipTitle() -> String { LocalizationHelper.localizedString(forKey: "Gamepad Overlay") } - @objc static func gamepadOverlayFeatureTipMessage() -> String { + @objc public static func gamepadOverlayFeatureTipMessage() -> String { LocalizationHelper.localizedString(forKey: "gamepadOverlayFeatureTip") } - @objc static func gamepadOverlayFeatureTipButtonTitle() -> String { + @objc public static func gamepadOverlayFeatureTipButtonTitle() -> String { LocalizationHelper.localizedString(forKey: "Got it!") } - @objc static var pencilInStreaming:Bool = false + @objc public static var pencilInStreaming:Bool = false - @objc static func isIPhone() -> Bool { - return UIDevice.current.userInterfaceIdiom == .phone - } - - @objc static func isIPad() -> Bool { - return UIDevice.current.userInterfaceIdiom == .pad - } + @objc public static let menuSeparatorWidth: CGFloat = 0.7 + @objc public static let menuSectionSeparatorWidth: CGFloat = 0.7 - @objc static var isRunningOnMacAsiPadApp: Bool { - if #available(iOS 14.0, *) { - return ProcessInfo.processInfo.isiOSAppOnMac - } - return false - } - - @objc static let iOS26Available: Bool = { - if #available(iOS 26.0, tvOS 26.0, *) { - return true - } else { - return false - } - }() - - @objc static let liquidGlassEnabled: Bool = { - if #available(iOS 26.0, tvOS 26.0, *) { - let useLegacyUI = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool - return useLegacyUI != true - } else { - return false - } - }() - - @objc static let isGUIWidgetPickerAvailable: Bool = { - if #available(iOS 13.0, tvOS 13.0, *) { - return true - } else { - return false - } - }() - - @objc static let menuSeparatorWidth: CGFloat = 0.7 - @objc static let menuSectionSeparatorWidth: CGFloat = 0.7 - - @objc static var legacyToolbarHeight: CGFloat { + @objc public static var legacyToolbarHeight: CGFloat { return 44 } - @objc static var inputAccessoryBarHeight: CGFloat { + @objc public static var inputAccessoryBarHeight: CGFloat { if #available(iOS 13.0, *){ - return GenericUtils.isIPhone() ? 46 : 55 + return PublicUtils.isIPhone ? 46 : 55 } else {return 44} } - @objc static var hostViewNavigationBarHeight: CGFloat { + @objc public static var hostViewNavigationBarHeight: CGFloat { switch UIDevice.current.userInterfaceIdiom { case .phone: - return liquidGlassEnabled ? 54 : 44 + return PublicUtils.liquidGlassEnabled ? 54 : 44 case .pad: - return liquidGlassEnabled ? 54 : 50 + return PublicUtils.liquidGlassEnabled ? 54 : 50 default: - return liquidGlassEnabled ? 54 : 50 + return PublicUtils.liquidGlassEnabled ? 54 : 50 } } - @objc static var settingsMenuNavigationBarHeight: CGFloat { + @objc public static var settingsMenuNavigationBarHeight: CGFloat { // return isIPhone() ? 44 : hostViewNavigationBarHeight switch UIDevice.current.userInterfaceIdiom { case .phone: - return liquidGlassEnabled ? hostViewNavigationBarHeight+5 : hostViewNavigationBarHeight + return PublicUtils.liquidGlassEnabled ? hostViewNavigationBarHeight+5 : hostViewNavigationBarHeight case .pad: - return liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight + return PublicUtils.liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight default: - return liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight + return PublicUtils.liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight } } - @objc static var dockedNavBarTopAnchorOffset: CGFloat { - return liquidGlassEnabled ? 10 : 0 + @objc public static var dockedNavBarTopAnchorOffset: CGFloat { + return PublicUtils.liquidGlassEnabled ? 10 : 0 } @available(iOS 26.0, *) - @objc static func applyOffTintColor(_ view: UIView) { + @objc public static func applyOffTintColor(_ view: UIView) { let name = String(describing: type(of: view)) if name.contains("UISwitchModernVisualElement") { view.backgroundColor = ThemeManager.liquidGlassSwitchOffTint @@ -586,10 +546,10 @@ import UIKit } } - @objc static var autoPopSoftKeyboard: Bool = true - @objc static var textFieldShouldResignAfterReturn: Bool = false + @objc public static var autoPopSoftKeyboard: Bool = true + @objc public static var textFieldShouldResignAfterReturn: Bool = false - @objc static func getAtrributedPlaceHolder(text:String)-> NSAttributedString { + @objc public static func getAtrributedPlaceHolder(text:String)-> NSAttributedString { if #available(iOS 13.0, *) { return NSAttributedString( string: text, @@ -608,7 +568,7 @@ import UIKit } static var kScaleLayerKey: UInt8 = 0 - @objc static func setVerticalScale(view: UIView, show: Bool) { + @objc public static func setVerticalScale(view: UIView, show: Bool) { // 移除旧的 if let oldLayer = objc_getAssociatedObject(view, &kScaleLayerKey) as? CALayer { oldLayer.removeFromSuperlayer() @@ -664,272 +624,7 @@ import UIKit objc_setAssociatedObject(view, &kScaleLayerKey, container, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - - @objc static func toCGFloat(_ str: String) -> CGFloat { - return CGFloat(Double(str) ?? 0) - } - - @objc(openUrl:) - static func openUrl(_ urlString: String) { - guard let url = URL(string: urlString), - UIApplication.shared.canOpenURL(url) else { return } - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - @objc static func isLandscape() -> Bool { - if #available(iOS 13.0, *) { - guard let windowScene = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .first(where: { $0.activationState == .foregroundActive }) - else {return false} - return windowScene.interfaceOrientation.isLandscape - } - else {return GenericUtils.screenWidth > GenericUtils.screenHeight} - } - - @objc static func viewIsLandscape(_ view: UIView?) -> Bool { - guard let view else {return false} - return view.bounds.width > view.bounds.height - } - - @objc static var screenWidth: CGFloat { - return UIScreen.main.bounds.width - } - - @objc static var screenHeight: CGFloat { - return UIScreen.main.bounds.height - } - - @objc static var iOS18Available: Bool { - if #available(iOS 18.0, *) {return true} - else {return false} - } - - @objc static var globeAsEscape: Bool = false - @objc(parentViewControllerForView:) - static func parentViewController(for view: UIView?) -> UIViewController? { - var responder: UIResponder? = view - while let currentResponder = responder { - if let viewController = currentResponder as? UIViewController { - return viewController - } - responder = currentResponder.next - } - return nil - } - - @objc static func rootViewController() -> UIViewController? { - if #available(iOS 13.0, *) { - return UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .first { $0.activationState == .foregroundActive }? - .windows - .first { $0.isKeyWindow }? - .rootViewController - } else { - return UIApplication.shared.keyWindow?.rootViewController - } - } - - @objc static func topViewController() -> UIViewController? { - return topViewController(from: rootViewController()) - } - - private static func topViewController(from rootViewController: UIViewController?) -> UIViewController? { - if let navigationController = rootViewController as? UINavigationController { - return topViewController(from: navigationController.visibleViewController) - } - - if let tabBarController = rootViewController as? UITabBarController { - return topViewController(from: tabBarController.selectedViewController) - } - - if let splitViewController = rootViewController as? UISplitViewController, - let lastViewController = splitViewController.viewControllers.last { - return topViewController(from: lastViewController) - } - - if let presentedViewController = rootViewController?.presentedViewController { - return topViewController(from: presentedViewController) - } - - return rootViewController - } -} - -extension UIView { - var parentViewController: UIViewController? { - var responder: UIResponder? = self - while let r = responder { - if let vc = r as? UIViewController { - return vc - } - responder = r.next - } - return nil - } -} - -extension CGPoint { - var isValid: Bool { - x.isFinite && y.isFinite - } -} - -extension String { - var localized: String { - LocalizationHelper.localizedString(forKey: self) - } - - var localizedProfileName: String { - let parts = self.components(separatedBy: " - Restored") - let isRestored = self.contains(" - Restored") - let localized = isRestored ? "\(parts.first?.localized ?? "") - \("Restored".localized)" : self.localized - return localized - } -} - -private var previousSelectedSegmentIndexKey: UInt8 = 0 -private var lastKnownSelectedSegmentIndexKey: UInt8 = 0 -extension UISegmentedControl { - @objc static func installPreviousSelectionTracking() { - _ = enablePreviousSelectionTracking - } - - private static let enablePreviousSelectionTracking: Void = { - guard - let originalSetter = class_getInstanceMethod( - UISegmentedControl.self, - #selector(setter: UISegmentedControl.selectedSegmentIndex) - ), - let swizzledSetter = class_getInstanceMethod( - UISegmentedControl.self, - #selector(UISegmentedControl.vl_setSelectedSegmentIndex(_:)) - ), - let originalSendAction = class_getInstanceMethod( - UISegmentedControl.self, - #selector(UISegmentedControl.sendAction(_:to:for:)) - ), - let swizzledSendAction = class_getInstanceMethod( - UISegmentedControl.self, - #selector(UISegmentedControl.vl_sendAction(_:to:for:)) - ) - else { - return - } - - swizzleMethod( - on: UISegmentedControl.self, - originalMethod: originalSetter, - originalSelector: #selector(setter: UISegmentedControl.selectedSegmentIndex), - swizzledMethod: swizzledSetter, - swizzledSelector: #selector(UISegmentedControl.vl_setSelectedSegmentIndex(_:)) - ) - swizzleMethod( - on: UISegmentedControl.self, - originalMethod: originalSendAction, - originalSelector: #selector(UISegmentedControl.sendAction(_:to:for:)), - swizzledMethod: swizzledSendAction, - swizzledSelector: #selector(UISegmentedControl.vl_sendAction(_:to:for:)) - ) - }() - - @objc var previousSelectedSegmentIndex: Int { - Self.installPreviousSelectionTracking() - initializeLastKnownIndexIfNeeded() - return (objc_getAssociatedObject(self, &previousSelectedSegmentIndexKey) as? NSNumber)?.intValue - ?? selectedSegmentIndex - } - - @objc func resetPreviousSelectedSegmentIndex() { - Self.installPreviousSelectionTracking() - storePreviousSelectedSegmentIndex(selectedSegmentIndex) - storeLastKnownSelectedSegmentIndex(selectedSegmentIndex) - } - - @objc private func vl_setSelectedSegmentIndex(_ newValue: Int) { - Self.installPreviousSelectionTracking() - initializeLastKnownIndexIfNeeded() - - let currentValue = selectedSegmentIndex - if currentValue != newValue { - storePreviousSelectedSegmentIndex(newValue) - storeLastKnownSelectedSegmentIndex(newValue) - } - - vl_setSelectedSegmentIndex(newValue) - } - - @objc private func vl_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { - Self.installPreviousSelectionTracking() - updatePreviousSelectedSegmentIndexIfNeeded() - vl_sendAction(action, to: target, for: event) - } - - private func initializeLastKnownIndexIfNeeded() { - guard objc_getAssociatedObject(self, &lastKnownSelectedSegmentIndexKey) == nil else { - return - } - storeLastKnownSelectedSegmentIndex(selectedSegmentIndex) - } - - private func updatePreviousSelectedSegmentIndexIfNeeded() { - initializeLastKnownIndexIfNeeded() - - let currentValue = selectedSegmentIndex - let lastKnownValue = (objc_getAssociatedObject(self, &lastKnownSelectedSegmentIndexKey) as? NSNumber)?.intValue - ?? UISegmentedControl.noSegment - - guard currentValue != lastKnownValue else { - return - } - - storePreviousSelectedSegmentIndex(lastKnownValue) - storeLastKnownSelectedSegmentIndex(currentValue) - } - - private func storePreviousSelectedSegmentIndex(_ value: Int) { - objc_setAssociatedObject( - self, - &previousSelectedSegmentIndexKey, - NSNumber(value: value), - .OBJC_ASSOCIATION_RETAIN_NONATOMIC - ) - } - - private func storeLastKnownSelectedSegmentIndex(_ value: Int) { - objc_setAssociatedObject( - self, - &lastKnownSelectedSegmentIndexKey, - NSNumber(value: value), - .OBJC_ASSOCIATION_RETAIN_NONATOMIC - ) - } - - private static func swizzleMethod( - on cls: AnyClass, - originalMethod: Method, - originalSelector: Selector, - swizzledMethod: Method, - swizzledSelector: Selector - ) { - let didAddMethod = class_addMethod( - cls, - originalSelector, - method_getImplementation(swizzledMethod), - method_getTypeEncoding(swizzledMethod) - ) - - if didAddMethod { - class_replaceMethod( - cls, - swizzledSelector, - method_getImplementation(originalMethod), - method_getTypeEncoding(originalMethod) - ) - } else { - method_exchangeImplementations(originalMethod, swizzledMethod) - } - } + @objc public static var globeAsEscape: Bool = false } diff --git a/VoidLink/Utility/GraphicUtils.swift b/VoidLink/Utility/GraphicUtils.swift index fcdac87d..0b9f0b14 100644 --- a/VoidLink/Utility/GraphicUtils.swift +++ b/VoidLink/Utility/GraphicUtils.swift @@ -118,7 +118,7 @@ import SVGKit var trackPoint = CAShapeLayer() let path = UIBezierPath( arcCenter: CGPoint(x: 0, y: 0), - radius: GenericUtils.isIPhone() ? 12 : 15, + radius: PublicUtils.isIPhone ? 12 : 15, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true diff --git a/VoidLink/Utility/IAPManager.swift b/VoidLink/Utility/IAPManager.swift index 8aa92ffd..e853478c 100644 --- a/VoidLink/Utility/IAPManager.swift +++ b/VoidLink/Utility/IAPManager.swift @@ -207,7 +207,7 @@ import UIKit private func purchaseStoreKit2(_ product: AddOnProduct) async { guard IAPManager.canPurchase(product) else { AlertControllerUtil.showAlert( - in: GenericUtils.topViewController(), + in: PublicUtils.topViewController(), title: "In-app-purchase not available".localized, message: "In-app-purchase not available on this device.".localized, withCancel: false, @@ -241,7 +241,7 @@ import UIKit private static func canPurchase(_ product: AddOnProduct) -> Bool { switch product { case .PencilProPack: - return GenericUtils.isIPad() + return PublicUtils.isIPad default: return true } @@ -275,7 +275,7 @@ import UIKit @objc static func handlePurchaseSuccess(_ product: AddOnProduct) { switch product { case .PencilProPack: - if !GenericUtils.isIPad() {return} + if !PublicUtils.isIPad {return} let dataMan = DataManager() let settings = dataMan.retrieveSettings() settings?.onscreenControls = 1 @@ -450,7 +450,7 @@ import UIKit let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.learnMore, userInfo:["interruption": PurchaseInterruption.learnMore.rawValue]) - GenericUtils.openUrl(product.productURL()) + PublicUtils.openUrl(product.productURL()) return } diff --git a/VoidLink/Utility/PublicExtensions.swift b/VoidLink/Utility/PublicExtensions.swift new file mode 100644 index 00000000..e7288c82 --- /dev/null +++ b/VoidLink/Utility/PublicExtensions.swift @@ -0,0 +1,185 @@ +// +// PublicExtensions.swift +// VoidLink +// +// Created by True砖家 on 2026/6/30. +// Copyright © 2026 True砖家@Bilibili. All rights reserved. +// + +import UIKit + +public extension UIView { + var parentViewController: UIViewController? { + var responder: UIResponder? = self + while let r = responder { + if let vc = r as? UIViewController { + return vc + } + responder = r.next + } + return nil + } +} + +public extension CGPoint { + var isValid: Bool { + x.isFinite && y.isFinite + } +} + +public extension String { + var localized: String { + LocalizationHelper.localizedString(forKey: self) + } + + var localizedProfileName: String { + let parts = self.components(separatedBy: " - Restored") + let isRestored = self.contains(" - Restored") + let localized = isRestored ? "\(parts.first?.localized ?? "") - \("Restored".localized)" : self.localized + return localized + } +} + +private var previousSelectedSegmentIndexKey: UInt8 = 0 +private var lastKnownSelectedSegmentIndexKey: UInt8 = 0 +public extension UISegmentedControl { + @objc static func installPreviousSelectionTracking() { + _ = enablePreviousSelectionTracking + } + + private static let enablePreviousSelectionTracking: Void = { + guard + let originalSetter = class_getInstanceMethod( + UISegmentedControl.self, + #selector(setter: UISegmentedControl.selectedSegmentIndex) + ), + let swizzledSetter = class_getInstanceMethod( + UISegmentedControl.self, + #selector(UISegmentedControl.vl_setSelectedSegmentIndex(_:)) + ), + let originalSendAction = class_getInstanceMethod( + UISegmentedControl.self, + #selector(UISegmentedControl.sendAction(_:to:for:)) + ), + let swizzledSendAction = class_getInstanceMethod( + UISegmentedControl.self, + #selector(UISegmentedControl.vl_sendAction(_:to:for:)) + ) + else { + return + } + + swizzleMethod( + on: UISegmentedControl.self, + originalMethod: originalSetter, + originalSelector: #selector(setter: UISegmentedControl.selectedSegmentIndex), + swizzledMethod: swizzledSetter, + swizzledSelector: #selector(UISegmentedControl.vl_setSelectedSegmentIndex(_:)) + ) + swizzleMethod( + on: UISegmentedControl.self, + originalMethod: originalSendAction, + originalSelector: #selector(UISegmentedControl.sendAction(_:to:for:)), + swizzledMethod: swizzledSendAction, + swizzledSelector: #selector(UISegmentedControl.vl_sendAction(_:to:for:)) + ) + }() + + @objc var previousSelectedSegmentIndex: Int { + Self.installPreviousSelectionTracking() + initializeLastKnownIndexIfNeeded() + return (objc_getAssociatedObject(self, &previousSelectedSegmentIndexKey) as? NSNumber)?.intValue + ?? selectedSegmentIndex + } + + @objc func resetPreviousSelectedSegmentIndex() { + Self.installPreviousSelectionTracking() + storePreviousSelectedSegmentIndex(selectedSegmentIndex) + storeLastKnownSelectedSegmentIndex(selectedSegmentIndex) + } + + @objc private func vl_setSelectedSegmentIndex(_ newValue: Int) { + Self.installPreviousSelectionTracking() + initializeLastKnownIndexIfNeeded() + + let currentValue = selectedSegmentIndex + if currentValue != newValue { + storePreviousSelectedSegmentIndex(newValue) + storeLastKnownSelectedSegmentIndex(newValue) + } + + vl_setSelectedSegmentIndex(newValue) + } + + @objc private func vl_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { + Self.installPreviousSelectionTracking() + updatePreviousSelectedSegmentIndexIfNeeded() + vl_sendAction(action, to: target, for: event) + } + + private func initializeLastKnownIndexIfNeeded() { + guard objc_getAssociatedObject(self, &lastKnownSelectedSegmentIndexKey) == nil else { + return + } + storeLastKnownSelectedSegmentIndex(selectedSegmentIndex) + } + + private func updatePreviousSelectedSegmentIndexIfNeeded() { + initializeLastKnownIndexIfNeeded() + + let currentValue = selectedSegmentIndex + let lastKnownValue = (objc_getAssociatedObject(self, &lastKnownSelectedSegmentIndexKey) as? NSNumber)?.intValue + ?? UISegmentedControl.noSegment + + guard currentValue != lastKnownValue else { + return + } + + storePreviousSelectedSegmentIndex(lastKnownValue) + storeLastKnownSelectedSegmentIndex(currentValue) + } + + private func storePreviousSelectedSegmentIndex(_ value: Int) { + objc_setAssociatedObject( + self, + &previousSelectedSegmentIndexKey, + NSNumber(value: value), + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + + private func storeLastKnownSelectedSegmentIndex(_ value: Int) { + objc_setAssociatedObject( + self, + &lastKnownSelectedSegmentIndexKey, + NSNumber(value: value), + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + + private static func swizzleMethod( + on cls: AnyClass, + originalMethod: Method, + originalSelector: Selector, + swizzledMethod: Method, + swizzledSelector: Selector + ) { + let didAddMethod = class_addMethod( + cls, + originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod) + ) + + if didAddMethod { + class_replaceMethod( + cls, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod) + ) + } else { + method_exchangeImplementations(originalMethod, swizzledMethod) + } + } +} diff --git a/VoidLink/Utility/PublicUtils.swift b/VoidLink/Utility/PublicUtils.swift new file mode 100644 index 00000000..77885351 --- /dev/null +++ b/VoidLink/Utility/PublicUtils.swift @@ -0,0 +1,146 @@ +// +// PublicUtils.swift +// VoidLink +// +// Created by True砖家 on 2026/2/26. +// Copyright © 2026 True砖家@Bilibili. All rights reserved. +// + + +import Foundation +import ObjectiveC +import UIKit + +@objc public class PublicUtils: NSObject { + + @objc public static var isIPhone: Bool = { + return UIDevice.current.userInterfaceIdiom == .phone + }() + + @objc public static var isIPad: Bool = { + return UIDevice.current.userInterfaceIdiom == .pad + }() + + @objc public static var isRunningOnMacAsiPadApp: Bool = { + if #available(iOS 14.0, *) { + return ProcessInfo.processInfo.isiOSAppOnMac + } + return false + }() + + @objc public static let iOS26Available: Bool = { + if #available(iOS 26.0, tvOS 26.0, *) { + return true + } else { + return false + } + }() + + @objc public static let liquidGlassEnabled: Bool = { + if #available(iOS 26.0, tvOS 26.0, *) { + let useLegacyUI = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool + return useLegacyUI != true + } else { + return false + } + }() + + @objc public static var iOS18Available: Bool = { + if #available(iOS 18.0, *) {return true} + else {return false} + }() + + @objc public static let isGUIWidgetPickerAvailable: Bool = { + if #available(iOS 13.0, tvOS 13.0, *) { + return true + } else { + return false + } + }() + + @objc public static func isLandscape() -> Bool { + if #available(iOS 13.0, *) { + guard let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) + else {return false} + return windowScene.interfaceOrientation.isLandscape + } + else {return PublicUtils.screenWidth > PublicUtils.screenHeight} + } + + @objc public static func toCGFloat(_ str: String) -> CGFloat { + return CGFloat(Double(str) ?? 0) + } + + @objc public static func viewIsLandscape(_ view: UIView?) -> Bool { + guard let view else {return false} + return view.bounds.width > view.bounds.height + } + + @objc public static var screenWidth: CGFloat { + return UIScreen.main.bounds.width + } + + @objc public static var screenHeight: CGFloat { + return UIScreen.main.bounds.height + } + + @objc(parentViewControllerForView:) + static func parentViewController(for view: UIView?) -> UIViewController? { + var responder: UIResponder? = view + while let currentResponder = responder { + if let viewController = currentResponder as? UIViewController { + return viewController + } + responder = currentResponder.next + } + return nil + } + + @objc public static func rootViewController() -> UIViewController? { + if #available(iOS 13.0, *) { + return UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first { $0.activationState == .foregroundActive }? + .windows + .first { $0.isKeyWindow }? + .rootViewController + } else { + return UIApplication.shared.keyWindow?.rootViewController + } + } + + @objc public static func topViewController() -> UIViewController? { + return topViewController(from: rootViewController()) + } + + private static func topViewController(from rootViewController: UIViewController?) -> UIViewController? { + if let navigationController = rootViewController as? UINavigationController { + return topViewController(from: navigationController.visibleViewController) + } + + if let tabBarController = rootViewController as? UITabBarController { + return topViewController(from: tabBarController.selectedViewController) + } + + if let splitViewController = rootViewController as? UISplitViewController, + let lastViewController = splitViewController.viewControllers.last { + return topViewController(from: lastViewController) + } + + if let presentedViewController = rootViewController?.presentedViewController { + return topViewController(from: presentedViewController) + } + + return rootViewController + } + + + @objc(openUrl:) + static func openUrl(_ urlString: String) { + guard let url = URL(string: urlString), + UIApplication.shared.canOpenURL(url) else { return } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } +} diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift index 343bd460..50f53f8a 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift @@ -215,7 +215,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg if currentSettings?.unlockDisplayOrientation == true { return .all } else { - return GenericUtils.isIPhone() ? .landscape : getCurrentOrientation() + return PublicUtils.isIPhone ? .landscape : getCurrentOrientation() } } @@ -622,7 +622,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg buttonModeStack.isHidden = !hidden || (selectedWidget?.widgetType != .button) collectedWidgetsStack.isHidden = selectedWidget?.isFolder != true - if GenericUtils.isIPhone() { + if PublicUtils.isIPhone { vibrationStyleStack.isHidden = false } @@ -1070,7 +1070,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg countdown: 5, completion: { if AlertControllerUtil.actionCancelled { - GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "folderTutorialUrl")) + PublicUtils.openUrl(LocalizationHelper.localizedString(forKey: "folderTutorialUrl")) } else {return} } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift index d3352c2b..883307ac 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift @@ -46,7 +46,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat private var horizontalConstraintsConfigured = false private func contentWidthMultiplier() -> CGFloat { - GenericUtils.viewIsLandscape(view) ? (GenericUtils.isIPhone() ? 0.8 : 0.65) : (GenericUtils.isIPhone() ? 0.83 : 0.85) + PublicUtils.viewIsLandscape(view) ? (PublicUtils.isIPhone ? 0.8 : 0.65) : (PublicUtils.isIPhone ? 0.83 : 0.85) } private func updateHorizontalLayoutConstraints() { @@ -116,8 +116,8 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) - if GenericUtils.liquidGlassEnabled { - tableView.separatorColor = UIColor.white.withAlphaComponent(GenericUtils.isIPhone() ? 0.28 : 0.165) + if PublicUtils.liquidGlassEnabled { + tableView.separatorColor = UIColor.white.withAlphaComponent(PublicUtils.isIPhone ? 0.28 : 0.165) } else { tableView.separatorColor = UIColor.white.withAlphaComponent(0.3) } @@ -140,7 +140,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat tap.delegate = self view.addGestureRecognizer(tap) - if GenericUtils.liquidGlassEnabled, #available(iOS 26.0, *) { + if PublicUtils.liquidGlassEnabled, #available(iOS 26.0, *) { profileTableViewNavigationItem.leftBarButtonItems?.forEach { $0.hidesSharedBackground = true } profileTableViewNavigationItem.rightBarButtonItems?.forEach { $0.hidesSharedBackground = true } } diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index d5771fc5..2331ae35 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -724,7 +724,7 @@ static NSMutableSet* hostList; - (void) addHostTapped { Log(LOG_D, @"Tapped add host"); - GenericUtils.autoPopSoftKeyboard = !GenericUtils.isIPhone; + GenericUtils.autoPopSoftKeyboard = !PublicUtils.isIPhone; UIAlertController* alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Add Host Manually"] message:[LocalizationHelper localizedStringForKey:@"Enter IP address to add host manually"] preferredStyle:UIAlertControllerStyleAlert]; @@ -1456,7 +1456,7 @@ static NSMutableSet* hostList; - (UIBarButtonItem *)createAddHostButton{ // 创建按钮 - bool liquidGlassEnabled = GenericUtils.liquidGlassEnabled; + bool liquidGlassEnabled = PublicUtils.liquidGlassEnabled; // bool liquidGlassEnabled = false; CGFloat buttonHeight = 30; @@ -1519,7 +1519,7 @@ static NSMutableSet* hostList; // 设置图标(SF Symbol) if (@available(iOS 13.0, *)) { UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize: - GenericUtils.liquidGlassEnabled ? buttonHeight*0.73 : buttonHeight*0.85 + PublicUtils.liquidGlassEnabled ? buttonHeight*0.73 : buttonHeight*0.85 weight:UIImageSymbolWeightRegular]; UIImage *image = [UIImage systemImageNamed:@"questionmark.circle" withConfiguration:config]; [button setImage:image forState:UIControlStateNormal]; @@ -1585,7 +1585,7 @@ static NSMutableSet* hostList; self->_addHostButton = [self createAddHostButton]; self->_helpButton = [self createHelpButton]; - if (GenericUtils.liquidGlassEnabled) { + if (PublicUtils.liquidGlassEnabled) { if (@available(iOS 26.0, *)) { // _addHostButton.hidesSharedBackground = true; // _helpButton.hidesSharedBackground = true; @@ -1603,11 +1603,11 @@ static NSMutableSet* hostList; [_settingsButton setAction:@selector(revealToggle:)]; if (@available(iOS 13.0, *)) { [_settingsButton setTitle:nil]; - UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:GenericUtils.liquidGlassEnabled ? 18 : 23 weight:UIImageSymbolWeightMedium ]; + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 18 : 23 weight:UIImageSymbolWeightMedium ]; UIImage *image = [[UIImage systemImageNamed:@"sidebar.left" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [_settingsButton setImage:image]; - _settingsButton.imageInsets = GenericUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(10, 10, 0, 0); - if(GenericUtils.liquidGlassEnabled){ + _settingsButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(10, 10, 0, 0); + if(PublicUtils.liquidGlassEnabled){ // if(@available(iOS 26.0, *)) _settingsButton.hidesSharedBackground = YES; _settingsButton.tintColor = ThemeManager.appPrimaryColor; } @@ -1622,12 +1622,12 @@ static NSMutableSet* hostList; UIImageSymbolConfiguration *config; UIImage *image; - if(GenericUtils.iOS18Available){ - config = [UIImageSymbolConfiguration configurationWithPointSize:GenericUtils.liquidGlassEnabled ? 20.5 : 23 weight:UIImageSymbolWeightRegular ]; + if(PublicUtils.iOS18Available){ + config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 20.5 : 23 weight:UIImageSymbolWeightRegular ]; image = [[UIImage systemImageNamed: @"gamecontroller.circle" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [_profilesButton setImage:image]; - _profilesButton.imageInsets = GenericUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(10, 10, 0, 0); - if(GenericUtils.liquidGlassEnabled){ + _profilesButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(10, 10, 0, 0); + if(PublicUtils.liquidGlassEnabled){ _profilesButton.tintColor = ThemeManager.appPrimaryColor; } } @@ -1656,10 +1656,10 @@ static NSMutableSet* hostList; if (@available(iOS 13.0, *)) { [_upButton setTitle:@""]; - UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:GenericUtils.liquidGlassEnabled ? 16 : 21.5 weight:UIImageSymbolWeightMedium]; - UIImage *image = [[UIImage systemImageNamed:GenericUtils.liquidGlassEnabled ? @"macwindow.on.rectangle" : @"tv" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 16 : 21.5 weight:UIImageSymbolWeightMedium]; + UIImage *image = [[UIImage systemImageNamed:PublicUtils.liquidGlassEnabled ? @"macwindow.on.rectangle" : @"tv" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [_upButton setImage:image]; - _upButton.imageInsets = GenericUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 1) : UIEdgeInsetsMake(25, 20, 0, 15); + _upButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 1) : UIEdgeInsetsMake(25, 20, 0, 15); } else { [_upButton setTitle:[LocalizationHelper localizedStringForKey:@"Select New Host"]]; } @@ -1686,7 +1686,7 @@ static NSMutableSet* hostList; _settingsButton.tintColor = ThemeManager.appPrimaryColor; _upButton.tintColor = ThemeManager.appPrimaryColor; - ((UIButton*)_addHostButton.customView).backgroundColor = GenericUtils.liquidGlassEnabled ? UIColor.clearColor : ThemeManager.appPrimaryColor; + ((UIButton*)_addHostButton.customView).backgroundColor = PublicUtils.liquidGlassEnabled ? UIColor.clearColor : ThemeManager.appPrimaryColor; ((UIButton*)_helpButton.customView).tintColor = ThemeManager.appPrimaryColor; [self applyNavBarAppearance]; diff --git a/VoidLink/ViewControllers/SWRevealViewController.m b/VoidLink/ViewControllers/SWRevealViewController.m index 692dd215..9487ce16 100755 --- a/VoidLink/ViewControllers/SWRevealViewController.m +++ b/VoidLink/ViewControllers/SWRevealViewController.m @@ -1008,12 +1008,12 @@ const int FrontViewPositionNone = 0xff; [backButton setAction:@selector(foldRearView)]; if (@available(iOS 13.0, *)) { [backButton setTitle:nil]; - UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:GenericUtils.liquidGlassEnabled ? 18 : 23 weight:UIImageSymbolWeightMedium]; + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 18 : 23 weight:UIImageSymbolWeightMedium]; UIImage *image = [[UIImage systemImageNamed:@"sidebar.left" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [backButton setImage:image]; - backButton.imageInsets = GenericUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(20, 10, 0, 0); - if(GenericUtils.liquidGlassEnabled){ + backButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(20, 10, 0, 0); + if(PublicUtils.liquidGlassEnabled){ // if(@available(iOS 26.0, *)) backButton.hidesSharedBackground = YES; backButton.tintColor = ThemeManager.appPrimaryColor; } @@ -1033,14 +1033,14 @@ const int FrontViewPositionNone = 0xff; [_disconnectButton setTitle:nil]; UIImage *image; - if(GenericUtils.liquidGlassEnabled){ + if(PublicUtils.liquidGlassEnabled){ UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:16 weight:UIImageSymbolWeightSemibold]; image = [[UIImage systemImageNamed:@"rectangle.portrait.and.arrow.right" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } else image = [[UIImage imageNamed:@"disconnect.svg" ] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [_disconnectButton setImage:image]; - if(GenericUtils.liquidGlassEnabled) _disconnectButton.imageInsets = UIEdgeInsetsMake(5, 5, 5, 5); + if(PublicUtils.liquidGlassEnabled) _disconnectButton.imageInsets = UIEdgeInsetsMake(5, 5, 5, 5); _disconnectButton.tintColor = ThemeManager.appPrimaryColor; } else { @@ -1061,12 +1061,12 @@ const int FrontViewPositionNone = 0xff; //[moreButton setAction:@selector(moreButtonTapped:)]; if (@available(iOS 13.0, *)) { [_moreButton setTitle:nil]; - UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:GenericUtils.liquidGlassEnabled ? 18 : 23 - weight:GenericUtils.liquidGlassEnabled ? UIImageSymbolWeightMedium : UIImageSymbolWeightMedium]; + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 18 : 23 + weight:PublicUtils.liquidGlassEnabled ? UIImageSymbolWeightMedium : UIImageSymbolWeightMedium]; UIImage *image = [[UIImage systemImageNamed:@"ellipsis.circle" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [_moreButton setImage:image]; - _moreButton.imageInsets = GenericUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.75) : UIEdgeInsetsMake(20, 0, 0, -10); - if(GenericUtils.liquidGlassEnabled) _moreButton.tintColor = ThemeManager.appPrimaryColor; + _moreButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.75) : UIEdgeInsetsMake(20, 0, 0, -10); + if(PublicUtils.liquidGlassEnabled) _moreButton.tintColor = ThemeManager.appPrimaryColor; } else { [_moreButton setTitle:[LocalizationHelper localizedStringForKey:@"Options"]]; } diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index b36625cc..cb767464 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -1034,7 +1034,7 @@ BOOL isCustomResolution(int resolutionSelected) { || [bundleId isEqualToString:@"com.voidlinkextreme.iOS"] || [bundleId isEqualToString:@"com.voidlink.tf.debug10.iOS"]); - if ([GenericUtils isIPad] && loadPencilSection) { + if ([PublicUtils isIPad] && loadPencilSection) { MenuSectionView *pencilSection = [[MenuSectionView alloc] init]; pencilSection.delegate = self; pencilSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"=drawingToolkit"]; @@ -1550,7 +1550,7 @@ BOOL isCustomResolution(int resolutionSelected) { if (@available(iOS 13.0, *)) { if(stack.isGameProfileSetting){ UIImage* image; - if(GenericUtils.iOS18Available){ + if(PublicUtils.iOS18Available){ UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:14.5 weight:UIImageSymbolWeightRegular]; image = [UIImage systemImageNamed:@"gamecontroller.circle" withConfiguration:config]; } @@ -1814,7 +1814,7 @@ BOOL isCustomResolution(int resolutionSelected) { UIAlertAction *readInstruction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Online Documentation"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ - [GenericUtils openUrl:onlineDocLink]; + [PublicUtils openUrl:onlineDocLink]; }]; @@ -2476,7 +2476,7 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void)showCustomOswTip { - GenericUtils.autoPopSoftKeyboard = !GenericUtils.isIPhone; + GenericUtils.autoPopSoftKeyboard = !PublicUtils.isIPhone; NSString* edgeSide = self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex == 1 ? [LocalizationHelper localizedStringForKey:@"left"] : [LocalizationHelper localizedStringForKey:@"right"]; NSString* slideDist = [NSString stringWithFormat:@"%d%%", (int)(self.slideToMenuDistanceSlider.value*100)]; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Edit layout during streaming"] @@ -3316,7 +3316,7 @@ BOOL isCustomResolution(int resolutionSelected) { UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - self->softKeyboardHeight = [GenericUtils toCGFloat:alertController.textFields[0].text]; + self->softKeyboardHeight = [PublicUtils toCGFloat:alertController.textFields[0].text]; [GenericUtils setVerticalScaleWithView:self.view show:false]; [sender setOn:self->softKeyboardHeight!=0]; }]; @@ -3485,7 +3485,7 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void) promptCustomResolutionDialog { - GenericUtils.autoPopSoftKeyboard = !GenericUtils.isIPhone; + GenericUtils.autoPopSoftKeyboard = !PublicUtils.isIPhone; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey: @"Enter Custom Resolution"] message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { @@ -3729,7 +3729,7 @@ BOOL isCustomResolution(int resolutionSelected) { UISlider *slider = (UISlider *)subview; slider.tintColor = [UIColor clearColor]; slider.tintColor = ThemeManager.appSecondaryColor; - if(GenericUtils.liquidGlassEnabled) slider.maximumTrackTintColor = ThemeManager.liquidGlassSliderMaxTrackTint; + if(PublicUtils.liquidGlassEnabled) slider.maximumTrackTintColor = ThemeManager.liquidGlassSliderMaxTrackTint; } [self updateThemeForSliders:subview]; } @@ -3768,7 +3768,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self updateThemeForLabels:self.view]; [self updateThemeForSelectors:self.view]; [self updateThemeForSliders:self.view]; - if(GenericUtils.liquidGlassEnabled) [self updateThemeForSwitches:self.view]; + if(PublicUtils.liquidGlassEnabled) [self updateThemeForSwitches:self.view]; } - (void) frameQueueSizeSliderMoved:(UISlider* )sender { @@ -3952,7 +3952,7 @@ BOOL isCustomResolution(int resolutionSelected) { */ - (void)loadPencilSettings:(TemporarySettings*) tempSettings{ - if([GenericUtils isIPad]){ + if([PublicUtils isIPad]){ self.pencilTickSelector.selectedSegmentIndex = tempSettings.pencilTickMode.intValue; [self.pencilTickSelector addTarget:self action:@selector(pencilTickModeChanged:) forControlEvents:UIControlEventValueChanged]; [self pencilTickModeChanged:self.pencilTickSelector]; @@ -3975,7 +3975,7 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void)populatePencilSettings:(Settings*)currentSettings{ - if([GenericUtils isIPad]){ + if([PublicUtils isIPad]){ currentSettings.pencilTickMode = @(self.pencilTickSelector.selectedSegmentIndex); currentSettings.pencilTickIntervalUs = @(self.pencilTickIntervalSlider.value); } diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index e55be5b6..95c8a99b 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -802,7 +802,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) { NSString* cmdToolEdgeSide = _settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"right"] : [LocalizationHelper localizedStringForKey:@"left"]; uint8_t slideDist = (uint8_t)(_settings.slideToSettingsDistance.floatValue * 100); // 创建弹窗 - NSString* tipText = (GenericUtils.isRunningOnMacAsiPadApp + NSString* tipText = (PublicUtils.isRunningOnMacAsiPadApp ? [LocalizationHelper localizedStringForKey:@"keyboard&MouseStreamingTip"] : [LocalizationHelper localizedStringForKey:@"firstLaunchTip", settingsEdgeSide, slideDist, cmdToolEdgeSide, slideDist]); @@ -814,7 +814,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) { countdown:16 action:^{} completion:^{ - if(!GenericUtils.isRunningOnMacAsiPadApp && GenericUtils.isHardwareKeyboardConnected) [self popKeyboardAndMouseStreamingTip]; + if(!PublicUtils.isRunningOnMacAsiPadApp && GenericUtils.isHardwareKeyboardConnected) [self popKeyboardAndMouseStreamingTip]; }]; return; @@ -1476,7 +1476,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) { } - (void)applicationDidBecomeActive:(NSNotification *)notification { - if(!GenericUtils.isIPhone){ + if(!PublicUtils.isIPhone){ for(OnScreenWidgetView* widget in OnScreenWidgetView.mapping.allValues){ if(widget.parentSequence != -1 && !widget.autoDockEnabled) continue; if(widget.autoDockEnabled){ @@ -2237,7 +2237,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) { // CGFloat maxWidth = MIN(CGRectGetWidth(self.view.bounds) * 0.72, 620); // CGFloat standardWidth = MAX(420, maxWidth); - CGFloat standardWidth = GenericUtils.isIPhone ? 165 : 200; + CGFloat standardWidth = PublicUtils.isIPhone ? 165 : 200; CGFloat standardhHeight = standardWidth / 1.82; CGRect overlayFrame = CGRectMake(0, 0, standardWidth, standardhHeight); diff --git a/VoidLink/ViewControllers/ToolboxViewController.swift b/VoidLink/ViewControllers/ToolboxViewController.swift index 07192e8d..b024c2c0 100644 --- a/VoidLink/ViewControllers/ToolboxViewController.swift +++ b/VoidLink/ViewControllers/ToolboxViewController.swift @@ -87,7 +87,7 @@ import UIKit super.viewWillTransition(to: size, with: coordinator) DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { - self.pinButton.isHidden = !GenericUtils.viewIsLandscape(self.view) && GenericUtils.isIPhone() + self.pinButton.isHidden = !PublicUtils.viewIsLandscape(self.view) && PublicUtils.isIPhone } view.setNeedsUpdateConstraints() @@ -108,15 +108,15 @@ import UIKit titleLabel.font = UIFont.boldSystemFont(ofSize: 20) // Adjust font size as needed titleLabel.textColor = UIColor.white // Adjust color as needed titleLabel.textAlignment = .center - if !GenericUtils.isIPhone() {contentView.addSubview(titleLabel)} + if !PublicUtils.isIPhone {contentView.addSubview(titleLabel)} contentView.backgroundColor = viewBackgroundColor tableView.backgroundColor = .clear tableView.rowHeight = isIPhone() ? 49.9 : 60 tableView.separatorColor = .white.withAlphaComponent(0.33) - if GenericUtils.liquidGlassEnabled { - tableView.separatorColor = .white.withAlphaComponent(GenericUtils.isIPhone() ? 0.185 : 0.13) + if PublicUtils.liquidGlassEnabled { + tableView.separatorColor = .white.withAlphaComponent(PublicUtils.isIPhone ? 0.185 : 0.13) } else{ tableView.separatorColor = .white.withAlphaComponent(0.3) @@ -142,7 +142,7 @@ import UIKit contentView.addSubview(editButton) // contentView.addSubview(exitButton) contentView.addSubview(pinButton) - pinButton.isHidden = !GenericUtils.isLandscape() + pinButton.isHidden = !PublicUtils.isLandscape() self.view.addSubview(contentView) @@ -166,13 +166,13 @@ import UIKit private func contentWidthMultiplier() -> CGFloat { /* - if isIPhone() && GenericUtils.viewIsLandscape(self.view) { + if isIPhone() && PublicUtils.viewIsLandscape(self.view) { return 0.85 }*/ return isIPhone() - ? (GenericUtils.viewIsLandscape(self.view) ? 0.6 : 0.85) - : (GenericUtils.viewIsLandscape(self.view) ? 0.6 : 0.85) + ? (PublicUtils.viewIsLandscape(self.view) ? 0.6 : 0.85) + : (PublicUtils.viewIsLandscape(self.view) ? 0.6 : 0.85) } @@ -206,7 +206,7 @@ import UIKit // TableView constraints tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - tableView.topAnchor.constraint(equalTo: contentView.topAnchor, constant:GenericUtils.isIPhone() ? 6 : 50), + tableView.topAnchor.constraint(equalTo: contentView.topAnchor, constant:PublicUtils.isIPhone ? 6 : 50), tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -60), // ExitButton constraints @@ -231,7 +231,7 @@ import UIKit ]) // ViewTitle constrains - if !GenericUtils.isIPhone() { + if !PublicUtils.isIPhone { NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 13.5), titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), diff --git a/VoidLinkPreview/RadialMenuView.swift b/VoidLinkPreview/RadialMenuView.swift new file mode 100644 index 00000000..87e85eb8 --- /dev/null +++ b/VoidLinkPreview/RadialMenuView.swift @@ -0,0 +1,433 @@ +// +// RadialMenuView.swift +// VoidLink +// +// Created by True砖家 on 2026/6/29. +// Copyright © 2026 True砖家 @ Bilibili. All rights reserved. +// + +import SwiftUI +import Combine +import UIKit + +@available(iOS 13.0, *) +struct RadialMenuItem: Identifiable, Equatable { + let id = UUID() + let title: String + let subtitle: String + let systemImageName: String + + init(title: String, subtitle: String, systemImageName: String) { + self.title = title + self.subtitle = subtitle + self.systemImageName = systemImageName + } +} + +@available(iOS 13.0, *) +struct RadialMenuStyle: Equatable { + var ringColor = Color(white: 0.86) + var selectedRingColor = Color(red: 0.19, green: 0.72, blue: 0.96) + var ringStrokeColor = Color.white.opacity(0.72) + var centerFillColor = Color.white + var iconColor = Color(white: 0.23) + var selectedIconColor = Color(white: 0.16) + var titleColor = Color(white: 0.24) + var subtitleColor = Color(white: 0.48) + var shadowColor = Color.black.opacity(0.18) + var ringWidthRatio: CGFloat = 0.41 + var segmentGapWidth: CGFloat = 1 + var centerIconScale: CGFloat = 0.16 + var segmentIconScale: CGFloat = 0.085 +} + +@available(iOS 13.0, *) +final class RadialMenuSelectionState: ObservableObject { + @Published private(set) var selectedIndex: Int? + @Published private(set) var hasReceivedJoystickInput = false + + private var itemCount = 0 + + func updateSelection(xOffset: Float, yOffset: Float) { + updateSelection(xOffset: xOffset, yOffset: yOffset, deadZone: 0.1) + } + + func updateSelection(xOffset: Float, yOffset: Float, deadZone: Float) { + let nextSelectedIndex = Self.selectedIndex( + xOffset: xOffset, + yOffset: yOffset, + itemCount: itemCount, + deadZone: deadZone + ) + + guard !hasReceivedJoystickInput || nextSelectedIndex != selectedIndex else { + return + } + + hasReceivedJoystickInput = true + selectedIndex = nextSelectedIndex + } + + fileprivate func configureItemCount(_ itemCount: Int) { + self.itemCount = max(itemCount, 0) + if let selectedIndex, selectedIndex >= itemCount { + self.selectedIndex = nil + } + } + + static func selectedIndex(xOffset: Float, yOffset: Float, itemCount: Int, deadZone: Float = 0.1) -> Int? { + guard itemCount > 0 else { return nil } + + let x = max(min(xOffset, 1), -1) + let y = max(min(yOffset, 1), -1) + let distance = hypot(x, y) + guard distance >= deadZone else { + return nil + } + + let clockwiseDegreesFromTop = positiveRemainder(atan2(Double(x), Double(y)) * 180 / .pi, 360) + let segmentDegrees = 360 / Double(itemCount) + let centeredDegrees = positiveRemainder(clockwiseDegreesFromTop + segmentDegrees / 2, 360) + return min(Int(centeredDegrees / segmentDegrees), itemCount - 1) + } + + private static func positiveRemainder(_ value: Double, _ divisor: Double) -> Double { + let remainder = value.truncatingRemainder(dividingBy: divisor) + return remainder >= 0 ? remainder : remainder + divisor + } +} + +@available(iOS 13.0, *) +struct RadialMenuView: View { + let items: [RadialMenuItem] + var selectedIndex: Int? + var isTouchSelectionEnabled: Bool + var style: RadialMenuStyle + + @ObservedObject private var joystickSelectionState: RadialMenuSelectionState + @SwiftUI.State private var touchSelectedIndex: Int? + + private var effectiveSelectedIndex: Int? { + if isTouchSelectionEnabled, let touchSelectedIndex { + return touchSelectedIndex + } + + if joystickSelectionState.hasReceivedJoystickInput { + return joystickSelectionState.selectedIndex + } + + return selectedIndex + } + + private var selectedItem: RadialMenuItem? { + guard let selectedIndex = effectiveSelectedIndex, + items.indices.contains(selectedIndex) else { + return items.first + } + return items[selectedIndex] + } + + init( + items: [RadialMenuItem], + selectedIndex: Int? = nil, + isTouchSelectionEnabled: Bool = true, + style: RadialMenuStyle = RadialMenuStyle(), + selectionState: RadialMenuSelectionState = RadialMenuSelectionState() + ) { + self.items = items + self.selectedIndex = selectedIndex + self.isTouchSelectionEnabled = isTouchSelectionEnabled + self.style = style + self.joystickSelectionState = selectionState + self.joystickSelectionState.configureItemCount(items.count) + } + + func updateSelection(xOffset: Float, yOffset: Float) { + joystickSelectionState.configureItemCount(items.count) + joystickSelectionState.updateSelection(xOffset: xOffset, yOffset: yOffset) + } + + var body: some View { + GeometryReader { proxy in + let diameter = min(proxy.size.width, proxy.size.height) + let center = CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2) + let outerRadius = diameter / 2 + let innerRadius = outerRadius * (1 - style.ringWidthRatio) + + ZStack { + ForEach(items.indices, id: \.self) { index in + RadialMenuSegmentShape( + index: index, + count: items.count, + innerRadiusRatio: 1 - style.ringWidthRatio, + gapWidth: style.segmentGapWidth + ) + .fill(index == effectiveSelectedIndex ? style.selectedRingColor : style.ringColor) + .overlay( + RadialMenuSegmentShape( + index: index, + count: items.count, + innerRadiusRatio: 1 - style.ringWidthRatio, + gapWidth: style.segmentGapWidth + ) + .stroke(style.ringStrokeColor.opacity(index == effectiveSelectedIndex ? 0.95 : 0.28), lineWidth: index == effectiveSelectedIndex ? 1.6 : 0.8) + ) + .scaleEffect(index == effectiveSelectedIndex ? 1.018 : 1) + .animation(.spring(response: 0.24, dampingFraction: 0.78), value: effectiveSelectedIndex) + } + + Circle() + .fill(style.centerFillColor) + .frame(width: innerRadius * 2, height: innerRadius * 2) + .shadow(color: style.shadowColor, radius: 10, x: 0, y: 3) + + ForEach(items.indices, id: \.self) { index in + let item = items[index] + Image(systemName: item.systemImageName) + .font(.system(size: max(diameter * style.segmentIconScale, 18), weight: .medium)) + .foregroundColor(index == effectiveSelectedIndex ? style.selectedIconColor : style.iconColor) + .scaleEffect(index == effectiveSelectedIndex ? 1.18 : 1) + .position(iconPosition(index: index, count: items.count, center: center, radius: (outerRadius + innerRadius) / 2)) + .animation(.spring(response: 0.24, dampingFraction: 0.72), value: effectiveSelectedIndex) + } + + RadialMenuCenterView(item: selectedItem, style: style, diameter: diameter) + .frame(width: innerRadius * 1.72, height: innerRadius * 1.72) + .position(center) + .animation(.easeInOut(duration: 0.16), value: effectiveSelectedIndex) + } + .frame(width: proxy.size.width, height: proxy.size.height) + .contentShape(Circle()) + .gesture(touchSelectionGesture(center: center, innerRadius: innerRadius, outerRadius: outerRadius)) + } + .aspectRatio(1, contentMode: .fit) + } + + private func touchSelectionGesture(center: CGPoint, innerRadius: CGFloat, outerRadius: CGFloat) -> some Gesture { + DragGesture(minimumDistance: 0, coordinateSpace: .local) + .onChanged { value in + guard isTouchSelectionEnabled else { return } + touchSelectedIndex = index(for: value.location, center: center, innerRadius: innerRadius, outerRadius: outerRadius) + } + .onEnded { _ in + guard isTouchSelectionEnabled else { return } + withAnimation(.easeOut(duration: 0.18)) { + touchSelectedIndex = nil + } + } + } + + private func index(for location: CGPoint, center: CGPoint, innerRadius: CGFloat, outerRadius: CGFloat) -> Int? { + guard !items.isEmpty else { return nil } + + let dx = location.x - center.x + let dy = location.y - center.y + let distance = hypot(dx, dy) + guard distance >= innerRadius * 0.72, distance <= outerRadius * 1.08 else { + return nil + } + + let rawDegrees = atan2(dy, dx) * 180 / .pi + let clockwiseDegreesFromTop = positiveRemainder(rawDegrees + 450, 360) + let segmentDegrees = 360 / Double(items.count) + let centeredDegrees = positiveRemainder(clockwiseDegreesFromTop + segmentDegrees / 2, 360) + return min(Int(centeredDegrees / segmentDegrees), items.count - 1) + } + + private func iconPosition(index: Int, count: Int, center: CGPoint, radius: CGFloat) -> CGPoint { + guard count > 0 else { return center } + let segmentDegrees = 360 / Double(count) + let angleDegrees = -90 + Double(index) * segmentDegrees + let radians = angleDegrees * .pi / 180 + return CGPoint( + x: center.x + CGFloat(cos(radians)) * radius, + y: center.y + CGFloat(sin(radians)) * radius + ) + } + + private func positiveRemainder(_ value: Double, _ divisor: Double) -> Double { + let remainder = value.truncatingRemainder(dividingBy: divisor) + return remainder >= 0 ? remainder : remainder + divisor + } +} + +@available(iOS 13.0, *) +private struct RadialMenuCenterView: View { + let item: RadialMenuItem? + let style: RadialMenuStyle + let diameter: CGFloat + + var body: some View { + VStack(spacing: max(diameter * 0.018, 4)) { + Image(systemName: item?.systemImageName ?? "circle.grid.cross") + .font(.system(size: max(diameter * style.centerIconScale, 28), weight: .medium)) + .foregroundColor(style.iconColor) + .id(item?.id) + .transition(.opacity) + + Text(item?.title ?? "") + .font(.system(size: max(diameter * 0.047, 13), weight: .medium)) + .foregroundColor(style.titleColor) + .lineLimit(1) + .minimumScaleFactor(0.62) + + if item?.subtitle.isEmpty == false { + Text(item?.subtitle ?? "") + .font(.system(size: max(diameter * 0.032, 10), weight: .regular)) + .foregroundColor(style.subtitleColor) + .lineLimit(1) + .minimumScaleFactor(0.68) + } + } + .multilineTextAlignment(.center) + .padding(.horizontal, diameter * 0.035) + } +} + +@available(iOS 13.0, *) +private struct RadialMenuSegmentShape: Shape { + let index: Int + let count: Int + let innerRadiusRatio: CGFloat + let gapWidth: CGFloat + + func path(in rect: CGRect) -> Path { + guard count > 0 else { return Path() } + + let center = CGPoint(x: rect.midX, y: rect.midY) + let outerRadius = min(rect.width, rect.height) / 2 + let innerRadius = outerRadius * innerRadiusRatio + let segmentDegrees = 360 / Double(count) + let centerDegrees = -90 + Double(index) * segmentDegrees + let startBoundaryDegrees = centerDegrees - segmentDegrees / 2 + let endBoundaryDegrees = centerDegrees + segmentDegrees / 2 + let halfGap = min(gapWidth / 2, max(innerRadius - 1, 0)) + + var path = Path() + let outerStart = offsetBoundaryPoint(center: center, radius: outerRadius, boundaryDegrees: startBoundaryDegrees, inwardSign: 1, halfGap: halfGap) + let innerStart = offsetBoundaryPoint(center: center, radius: innerRadius, boundaryDegrees: startBoundaryDegrees, inwardSign: 1, halfGap: halfGap) + let outerEnd = offsetBoundaryPoint(center: center, radius: outerRadius, boundaryDegrees: endBoundaryDegrees, inwardSign: -1, halfGap: halfGap) + let innerEnd = offsetBoundaryPoint(center: center, radius: innerRadius, boundaryDegrees: endBoundaryDegrees, inwardSign: -1, halfGap: halfGap) + let outerStartDegrees = degrees(from: center, to: outerStart) + let outerEndDegrees = unwrappedEndDegrees(start: outerStartDegrees, end: degrees(from: center, to: outerEnd)) + let innerStartDegrees = degrees(from: center, to: innerStart) + let innerEndDegrees = unwrappedEndDegrees(start: innerStartDegrees, end: degrees(from: center, to: innerEnd)) + + path.move(to: outerStart) + + let outerSteps = max(Int((outerEndDegrees - outerStartDegrees) / 4), 6) + for step in 1...outerSteps { + let progress = Double(step) / Double(outerSteps) + let degrees = outerStartDegrees + (outerEndDegrees - outerStartDegrees) * progress + path.addLine(to: point(center: center, radius: outerRadius, degrees: degrees)) + } + + path.addLine(to: innerEnd) + + let innerSteps = max(Int((innerEndDegrees - innerStartDegrees) / 4), 6) + for step in 0...innerSteps { + let progress = Double(step) / Double(innerSteps) + let degrees = innerEndDegrees - (innerEndDegrees - innerStartDegrees) * progress + path.addLine(to: point(center: center, radius: innerRadius, degrees: degrees)) + } + + path.addLine(to: outerStart) + path.closeSubpath() + return path + } + + private func offsetBoundaryPoint( + center: CGPoint, + radius: CGFloat, + boundaryDegrees: Double, + inwardSign: CGFloat, + halfGap: CGFloat + ) -> CGPoint { + let radians = boundaryDegrees * .pi / 180 + let radialX = CGFloat(cos(radians)) + let radialY = CGFloat(sin(radians)) + let tangentX = -radialY + let tangentY = radialX + let radialDistance = sqrt(max(radius * radius - halfGap * halfGap, 0)) + + return CGPoint( + x: center.x + radialX * radialDistance + tangentX * halfGap * inwardSign, + y: center.y + radialY * radialDistance + tangentY * halfGap * inwardSign + ) + } + + private func point(center: CGPoint, radius: CGFloat, degrees: Double) -> CGPoint { + let radians = degrees * .pi / 180 + return CGPoint( + x: center.x + CGFloat(cos(radians)) * radius, + y: center.y + CGFloat(sin(radians)) * radius + ) + } + + private func degrees(from center: CGPoint, to point: CGPoint) -> Double { + atan2(point.y - center.y, point.x - center.x) * 180 / .pi + } + + private func unwrappedEndDegrees(start: Double, end: Double) -> Double { + end < start ? end + 360 : end + } +} + +@available(iOS 13.0, *) +struct RadialMenuDemoView: View { + @SwiftUI.State private var itemCount = 8 + @SwiftUI.State private var isTouchSelectionEnabled = true + + static let menuItems = [ + RadialMenuItem(title: "Settings Menu".localized, subtitle: "", systemImageName: "sidebar.left"), + RadialMenuItem(title: "Host View".localized, subtitle: "", systemImageName: PublicUtils.liquidGlassEnabled ? "macwindow.on.rectangle" : "tv"), + RadialMenuItem(title: "Game Profiles".localized, subtitle: "", systemImageName: "gamecontroller.circle"), + ] + + private var visibleItems: [RadialMenuItem] { + Array(Self.menuItems.prefix(itemCount)) + } + + var body: some View { + VStack(spacing: 28) { + RadialMenuView( + items: visibleItems, + selectedIndex: isTouchSelectionEnabled ? nil : 2, + isTouchSelectionEnabled: isTouchSelectionEnabled + ) + .frame(width: 320, height: 320) + .padding(.top, 24) + + VStack(spacing: 16) { + Stepper("Segments: \(itemCount)", value: $itemCount, in: 3...RadialMenuDemoView.menuItems.count) + .frame(maxWidth: 320) + + Toggle("Touch selection", isOn: $isTouchSelectionEnabled) + .frame(maxWidth: 320) + } + .font(.system(size: 15, weight: .medium)) + .padding(.horizontal, 24) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(white: 0.97)) + } +} + +@available(iOS 13.0, *) +struct RadialMenuView_Previews: PreviewProvider { + static var previews: some View { + Group { + RadialMenuDemoView() + .previewLayout(.sizeThatFits) + + RadialMenuView( + items: Array(RadialMenuDemoView.menuItems.prefix(6)), + selectedIndex: 1, + isTouchSelectionEnabled: false + ) + .frame(width: 300, height: 300) + .padding(36) + .previewLayout(.sizeThatFits) + } + } +} diff --git a/VoidLinkPreview/VoidLinkPreview-Bridging-Header.h b/VoidLinkPreview/VoidLinkPreview-Bridging-Header.h new file mode 100644 index 00000000..1f7c36d1 --- /dev/null +++ b/VoidLinkPreview/VoidLinkPreview-Bridging-Header.h @@ -0,0 +1,6 @@ +// +// VoidLinkPreview-Bridging-Header.h +// VoidLinkPreview +// + +#import "../VoidLink/Localization/LocalizationHelper.h" diff --git a/VoidLinkPreview/VoidLinkPreview.swift b/VoidLinkPreview/VoidLinkPreview.swift index 0a6845dc..d7bff668 100644 --- a/VoidLinkPreview/VoidLinkPreview.swift +++ b/VoidLinkPreview/VoidLinkPreview.swift @@ -5,7 +5,7 @@ import SwiftUI struct VoidLinkPreview: App { var body: some Scene { WindowGroup { - WidgetPickerView() + RadialMenuDemoView() } } }