radial menu demo

This commit is contained in:
True Zhuanjia
2026-07-01 15:20:57 +08:00
parent 41a5855211
commit 54c106e01e
26 changed files with 1027 additions and 476 deletions
+18 -1
View File
@@ -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 = "<group>"; };
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = "<group>"; };
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 = "<group>"; };
2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUtils.swift; sourceTree = "<group>"; };
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; };
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = "<group>"; };
@@ -218,7 +225,6 @@
84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControls.m; sourceTree = "<group>"; };
84351B092C29ACCA00A77989 /* OnScreenButtonState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnScreenButtonState.m; sourceTree = "<group>"; };
84351B0A2C29ACCA00A77989 /* OSCProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCProfile.m; sourceTree = "<group>"; };
A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesManager.swift; sourceTree = "<group>"; };
84351B0C2C29ACCB00A77989 /* LayoutOnScreenControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutOnScreenControls.h; sourceTree = "<group>"; };
84351B0D2C29ACCB00A77989 /* OSCProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfile.h; sourceTree = "<group>"; };
84351B292C29AD6000A77989 /* ToolBarContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ToolBarContainerView.m; sourceTree = "<group>"; };
@@ -320,6 +326,7 @@
98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporarySettings.m; path = Database/TemporarySettings.m; sourceTree = "<group>"; };
A1900E002F9A100100BA853A /* InputAccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAccessoryBar.swift; sourceTree = "<group>"; };
A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractGamepadOverlayWidgetView.swift; sourceTree = "<group>"; };
A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesManager.swift; sourceTree = "<group>"; };
DC1F5A05206436B10037755F /* ConnectionHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConnectionHelper.h; sourceTree = "<group>"; };
DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = "<group>"; };
FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = "<group>"; };
@@ -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";
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -157,7 +157,7 @@ import UIKit
return
}
if GenericUtils.isRunningOnMacAsiPadApp && buttonTitle != "" {
if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" {
alertController.dismiss(animated: false) {
showAlert(
in: viewController,
@@ -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?
+1 -1
View File
@@ -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) }
+4 -4
View File
@@ -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
+5 -5
View File
@@ -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()
}
+3 -3
View File
@@ -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
+10 -10
View File
@@ -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];
}
+70 -1
View File
@@ -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"
}
}
+2 -2
View File
@@ -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)}
}
}
+78 -383
View File
@@ -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
}
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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
}
+185
View File
@@ -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)
}
}
}
+146
View File
@@ -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)
}
}
@@ -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}
}
@@ -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 }
}
@@ -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];
@@ -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"]];
}
@@ -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);
}
@@ -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);
@@ -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),
+433
View File
@@ -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)
}
}
}
@@ -0,0 +1,6 @@
//
// VoidLinkPreview-Bridging-Header.h
// VoidLinkPreview
//
#import "../VoidLink/Localization/LocalizationHelper.h"
+1 -1
View File
@@ -5,7 +5,7 @@ import SwiftUI
struct VoidLinkPreview: App {
var body: some Scene {
WindowGroup {
WidgetPickerView()
RadialMenuDemoView()
}
}
}