radial menu demo
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
@@ -5,7 +5,7 @@ import SwiftUI
|
||||
struct VoidLinkPreview: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
WidgetPickerView()
|
||||
RadialMenuDemoView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user