- controller navigation preparations
- disable tilt button for pencil - widget picker UI improvement & fixes - allow triggers to be controller mouse buttons
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
||||
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
|
||||
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
|
||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
|
||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */; };
|
||||
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE242EFB9B5A00BA853A /* SVGKit */; };
|
||||
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE262EFB9B5A00BA853A /* SVGKitSwift */; };
|
||||
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */; };
|
||||
@@ -63,8 +63,8 @@
|
||||
8454AC662C31907F00F0A302 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC682C31907F00F0A302 /* iPad.storyboard */; };
|
||||
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC6C2C319BDA00F0A302 /* iPhone.storyboard */; };
|
||||
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 84591A502BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m */; };
|
||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 847D2FDA2DDA015F00937A62 /* MenuSectionView.m */; };
|
||||
848F456A2DDCD12500581E36 /* HostCardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 848F45692DDCD12500581E36 /* HostCardView.m */; };
|
||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */; };
|
||||
848F456A2DDCD12500581E36 /* HostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F45692DDCD12500581E36 /* HostCardView.swift */; };
|
||||
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEB2C4F67D300547904 /* CommandManager.swift */; };
|
||||
84E1CDEF2C4F688E00547904 /* ToolboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */; };
|
||||
84E1CDFA2C4FE83400547904 /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; };
|
||||
@@ -107,6 +107,8 @@
|
||||
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; };
|
||||
A1900E012F9A100100BA853A /* InputAccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1900E002F9A100100BA853A /* InputAccessoryBar.swift */; };
|
||||
A1B2C3D42F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */; };
|
||||
A1C0DE012FBC000100000001 /* ControllerNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE002FBC000100000001 /* ControllerNavigator.swift */; };
|
||||
A1C0DE032FBC000100000001 /* RadialMenuOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE022FBC000100000001 /* RadialMenuOverlayView.swift */; };
|
||||
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
|
||||
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
|
||||
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
|
||||
@@ -200,8 +202,7 @@
|
||||
1888C1912E24C33100108730 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
|
||||
188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplatesIPhone.bin; path = PresetConfigs/widgetTemplatesIPhone.bin; sourceTree = "<group>"; };
|
||||
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; sourceTree = "<group>"; };
|
||||
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; };
|
||||
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = "<group>"; };
|
||||
189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUtils.swift; sourceTree = "<group>"; };
|
||||
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = "<group>"; };
|
||||
18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = "<group>"; };
|
||||
@@ -244,10 +245,8 @@
|
||||
8454AC6D2C319BDA00F0A302 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/iPhone.xcstrings; sourceTree = "<group>"; };
|
||||
84591A4F2BE16240006AAB10 /* CustomEdgeSlideGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomEdgeSlideGestureRecognizer.h; sourceTree = "<group>"; };
|
||||
84591A502BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomEdgeSlideGestureRecognizer.m; sourceTree = "<group>"; };
|
||||
847D2FD82DDA00C800937A62 /* MenuSectionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuSectionView.h; sourceTree = "<group>"; };
|
||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuSectionView.m; sourceTree = "<group>"; };
|
||||
848F45682DDCD0D600581E36 /* HostCardView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCardView.h; sourceTree = "<group>"; };
|
||||
848F45692DDCD12500581E36 /* HostCardView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCardView.m; sourceTree = "<group>"; };
|
||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSectionView.swift; sourceTree = "<group>"; };
|
||||
848F45692DDCD12500581E36 /* HostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostCardView.swift; sourceTree = "<group>"; };
|
||||
84D8EB932C5146AD00FC10A9 /* VoidLink-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "VoidLink-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
84E1CDEB2C4F67D300547904 /* CommandManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandManager.swift; sourceTree = "<group>"; };
|
||||
84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -326,6 +325,8 @@
|
||||
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>"; };
|
||||
A1C0DE002FBC000100000001 /* ControllerNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerNavigator.swift; sourceTree = "<group>"; };
|
||||
A1C0DE022FBC000100000001 /* RadialMenuOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialMenuOverlayView.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>"; };
|
||||
@@ -695,11 +696,9 @@
|
||||
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */,
|
||||
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */,
|
||||
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
||||
848F45682DDCD0D600581E36 /* HostCardView.h */,
|
||||
848F45692DDCD12500581E36 /* HostCardView.m */,
|
||||
848F45692DDCD12500581E36 /* HostCardView.swift */,
|
||||
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
|
||||
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
|
||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
|
||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */,
|
||||
FBDE86E419F82297001C18A8 /* UIAppView.h */,
|
||||
FBDE86E519F82297001C18A8 /* UIAppView.m */,
|
||||
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
|
||||
@@ -779,6 +778,8 @@
|
||||
18B317022EE67A39009B62C2 /* InertialScroller.swift */,
|
||||
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */,
|
||||
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */,
|
||||
A1C0DE002FBC000100000001 /* ControllerNavigator.swift */,
|
||||
A1C0DE022FBC000100000001 /* RadialMenuOverlayView.swift */,
|
||||
1850C8292E667E4000845308 /* MicHandler.swift */,
|
||||
18E4BCE92E891895000A8657 /* MotionHandler.swift */,
|
||||
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
|
||||
@@ -860,8 +861,7 @@
|
||||
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */,
|
||||
FB89462419F646E200339C8A /* MainFrameViewController.h */,
|
||||
FB89462519F646E200339C8A /* MainFrameViewController.m */,
|
||||
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */,
|
||||
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */,
|
||||
189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */,
|
||||
FB89462619F646E200339C8A /* StreamFrameViewController.h */,
|
||||
FB89462719F646E200339C8A /* StreamFrameViewController.m */,
|
||||
FBD3494E19FF2174002D2A60 /* SettingsViewController.h */,
|
||||
@@ -1166,11 +1166,13 @@
|
||||
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
|
||||
97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */,
|
||||
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */,
|
||||
A1C0DE012FBC000100000001 /* ControllerNavigator.swift in Sources */,
|
||||
A1C0DE032FBC000100000001 /* RadialMenuOverlayView.swift in Sources */,
|
||||
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */,
|
||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */,
|
||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.swift in Sources */,
|
||||
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */,
|
||||
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */,
|
||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */,
|
||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.swift in Sources */,
|
||||
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
|
||||
84351B112C29ACCB00A77989 /* OSCProfilesManager.swift in Sources */,
|
||||
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
||||
@@ -1203,7 +1205,7 @@
|
||||
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
||||
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
||||
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */,
|
||||
848F456A2DDCD12500581E36 /* HostCardView.m in Sources */,
|
||||
848F456A2DDCD12500581E36 /* HostCardView.swift in Sources */,
|
||||
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
|
||||
9827E7A32514366900F25707 /* HapticContext.m in Sources */,
|
||||
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */,
|
||||
|
||||
@@ -1922,7 +1922,7 @@
|
||||
<constraint firstAttribute="height" constant="31" id="MVP-k4-wjG"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="fPh-yy-10O" userLabel="substack2">
|
||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="fPh-yy-10O">
|
||||
<rect key="frame" x="0.0" y="36" width="336" height="31"/>
|
||||
<subviews>
|
||||
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Roll → Left Stick" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="EFR-Ee-KiK" userLabel="uiLabel">
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// HostCardView.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 5/18/25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
@protocol HostCardActionDelegate <NSObject>
|
||||
- (void)appButtonTappedForHost:(TemporaryHost *)host;
|
||||
- (void)launchButtonTappedForHost:(TemporaryHost *)host;
|
||||
- (void)wakeupButtonTappedForHost:(TemporaryHost *)host;
|
||||
- (void)pairButtonTappedForHost:(TemporaryHost *)host;
|
||||
- (void)hostCardLongPressed:(TemporaryHost *)host view:(UIView *)view;
|
||||
- (bool)isStreaming;
|
||||
@end
|
||||
|
||||
|
||||
@interface HostCardView : UIView
|
||||
@property (nonatomic, assign) CGFloat sizeFactor;
|
||||
@property (nonatomic, readonly) CGSize size;
|
||||
@property (nonatomic, weak) id<HostCardActionDelegate> delegate; // Delegate property
|
||||
|
||||
- (void)resizeBySizeFactor:(CGFloat)factor;
|
||||
- (id) initWithHost:(TemporaryHost*)host;
|
||||
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor;
|
||||
- (void)updateTheme;
|
||||
|
||||
@end
|
||||
@@ -1,644 +0,0 @@
|
||||
//
|
||||
// HostCardView.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 5/18/25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HostCardView.h"
|
||||
#import "LocalizationHelper.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
#import "FixedTintImageView.h"
|
||||
|
||||
|
||||
@interface HostCardView ()
|
||||
|
||||
@property (nonatomic, strong) UIView *iconBackgroundView;
|
||||
@property (nonatomic, strong) UIImageView *hostIconView;
|
||||
@property (nonatomic, strong) UILabel *hostNameLabel;
|
||||
@property (nonatomic, strong) UIImageView *statusIcon;
|
||||
@property (nonatomic, strong) UILabel *statusLabel;
|
||||
@property (nonatomic, strong) UIButton *appButton;
|
||||
@property (nonatomic, strong) UIButton *launchButton;
|
||||
@property (nonatomic, strong) UIButton *pairButton;
|
||||
@property (nonatomic, strong) UIButton *wakeupButton;
|
||||
@property (nonatomic, strong) UIButton *transparentButton;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *widthConstraint;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
|
||||
@property (nonatomic, strong) UIView *separatorLine;
|
||||
@property (nonatomic, assign) CGFloat cardContentpadding;
|
||||
@property (nonatomic, assign) CGSize size;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation HostCardView {
|
||||
TemporaryHost* _host;
|
||||
UIActivityIndicatorView* _hostSpinner;
|
||||
UIImageView* lockIconView;
|
||||
CGFloat computerIconMonitorCenterYOffset;
|
||||
CGFloat buttonHeight;
|
||||
CGFloat buttonLabelFontSize;
|
||||
CGFloat iconAndButtonSpacing;
|
||||
UIColor *defaultBlue;
|
||||
UIColor *defaultGreen;
|
||||
CAGradientLayer *backgroundLayer;
|
||||
bool longPressFired;
|
||||
}
|
||||
|
||||
static const float REFRESH_CYCLE = 2.0f;
|
||||
|
||||
- (BOOL)isIPhone{
|
||||
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
buttonLabelFontSize = 15*_sizeFactor;
|
||||
longPressFired = false;
|
||||
computerIconMonitorCenterYOffset = [self isIPhone] ? -2.75*_sizeFactor : -3.2*_sizeFactor;
|
||||
iconAndButtonSpacing = 37*_sizeFactor;
|
||||
buttonHeight = 39*_sizeFactor;
|
||||
defaultBlue = ThemeManager.appPrimaryColor;
|
||||
defaultGreen = [UIColor colorWithRed:52.0/255.0 green:199.0/255.0 blue:89.0/255.0 alpha:1.0];
|
||||
// self.userIterfaceStyle = UIUserInterfaceStyleLight
|
||||
|
||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostCardLongPressed:)];
|
||||
[self addGestureRecognizer:longPressRecognizer];
|
||||
|
||||
[self createBackgroundLayer];
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithHost:(TemporaryHost*)host {
|
||||
self.sizeFactor = 1.0;
|
||||
self = [self init];
|
||||
_host = host;
|
||||
|
||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
||||
// for tvOS devices and iOS prior to 13.0.
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor {
|
||||
_sizeFactor = sizeFactor;
|
||||
self = [self init];
|
||||
_host = host;
|
||||
|
||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
||||
// for tvOS devices and iOS prior to 13.0.
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)appButtonTapped{
|
||||
if(longPressFired){
|
||||
longPressFired = false;
|
||||
return;
|
||||
}
|
||||
if ([self.delegate respondsToSelector:@selector(appButtonTappedForHost:)]) {
|
||||
[self.delegate appButtonTappedForHost:_host];
|
||||
} else NSLog(@"Delegate not set or does not respond to appButtonTappedForHost:");
|
||||
}
|
||||
|
||||
- (void)launchButtonTapped{
|
||||
if(longPressFired){
|
||||
longPressFired = false;
|
||||
return;
|
||||
}
|
||||
if ([self.delegate respondsToSelector:@selector(launchButtonTappedForHost:)]) {
|
||||
[self.delegate launchButtonTappedForHost:_host];
|
||||
} else NSLog(@"Delegate not set or does not respond to launchButtonTappedForHost:");
|
||||
}
|
||||
|
||||
- (void)wakeupButtonTapped{
|
||||
if(longPressFired){
|
||||
longPressFired = false;
|
||||
return;
|
||||
}
|
||||
if ([self.delegate respondsToSelector:@selector(wakeupButtonTappedForHost:)]) {
|
||||
[self.delegate wakeupButtonTappedForHost:_host];
|
||||
} else NSLog(@"Delegate not set or does not respond to launchButtonTappedForHost:");
|
||||
}
|
||||
|
||||
|
||||
-(void)pairButtonTapped{
|
||||
if(longPressFired){
|
||||
longPressFired = false;
|
||||
return;
|
||||
}
|
||||
if ([self.delegate respondsToSelector:@selector(pairButtonTappedForHost:)]) {
|
||||
[self.delegate pairButtonTappedForHost:_host];
|
||||
} else NSLog(@"Delegate not set or does not respond to pairButtonTappedForHost:");
|
||||
|
||||
}
|
||||
|
||||
- (void)hostCardLongPressed:(UILongPressGestureRecognizer* )gesture{
|
||||
if(gesture.state != UIGestureRecognizerStateBegan) return;
|
||||
longPressFired = true;
|
||||
if ([self.delegate respondsToSelector:@selector(hostCardLongPressed:view:)]) {
|
||||
[self.delegate hostCardLongPressed:_host view:self];
|
||||
} else NSLog(@"Delegate not set or does not respond");
|
||||
}
|
||||
|
||||
- (void)resizeBySizeFactor:(CGFloat)factor{
|
||||
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
self.sizeFactor = factor;
|
||||
[self setupUI];
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
self.userInteractionEnabled = YES;
|
||||
self.backgroundColor = ThemeManager.widgetBackgroundColor; // theme
|
||||
self.cardContentpadding = 13 * _sizeFactor;
|
||||
self.clipsToBounds = YES;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
CGFloat appButtonWidth = 100*_sizeFactor;
|
||||
CGFloat launchButtonWidth = 120*_sizeFactor;
|
||||
CGFloat cardWidth = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
||||
self.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0603/2) ;
|
||||
/*
|
||||
NSLayoutConstraint *topAnchorConstraint;
|
||||
topAnchorConstraint = [self.topAnchor constraintEqualToAnchor:self.superview.topAnchor constant:500];
|
||||
topAnchorConstraint.active = YES;
|
||||
*/
|
||||
_heightConstraint = [self.heightAnchor constraintEqualToConstant:300];
|
||||
_heightConstraint.active = YES;
|
||||
_widthConstraint = [self.widthAnchor constraintEqualToConstant:300];
|
||||
_widthConstraint.active = YES;
|
||||
|
||||
|
||||
// 图标背景
|
||||
self.iconBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(_cardContentpadding, _cardContentpadding, 80*_sizeFactor, 80*_sizeFactor)];
|
||||
self.iconBackgroundView.backgroundColor = defaultBlue;
|
||||
self.iconBackgroundView.layer.cornerRadius = 2*(uint16_t)(20*_sizeFactor/2);
|
||||
[self addSubview:self.iconBackgroundView];
|
||||
|
||||
// 图标图片
|
||||
self.hostIconView = [[UIImageView alloc] init];
|
||||
self.hostIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.hostIconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self.hostIconView.image = [[UIImage imageNamed:@"display"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
} else {
|
||||
self.hostIconView.image = [UIImage imageNamed:@"Computer"];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.hostIconView.heightAnchor constraintEqualToConstant:57*_sizeFactor],
|
||||
[self.hostIconView.widthAnchor constraintEqualToConstant:57*_sizeFactor],
|
||||
]];
|
||||
}
|
||||
self.hostIconView.tintColor = [UIColor whiteColor];
|
||||
[self.iconBackgroundView addSubview:self.hostIconView];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.hostIconView.centerXAnchor constraintEqualToAnchor:self.iconBackgroundView.centerXAnchor constant:0],
|
||||
[self.hostIconView.centerYAnchor constraintEqualToAnchor:self.iconBackgroundView.centerYAnchor constant:0],
|
||||
[self.hostIconView.heightAnchor constraintEqualToConstant:53.9*_sizeFactor],
|
||||
[self.hostIconView.widthAnchor constraintEqualToConstant:53.9*_sizeFactor],
|
||||
]];
|
||||
// [self.iconBackgroundView layoutIfNeeded];
|
||||
// [self.iconImageView layoutIfNeeded];
|
||||
|
||||
//spinner
|
||||
bool oldiOS = false;
|
||||
if (@available(iOS 13.0, *)){}
|
||||
else oldiOS = true;
|
||||
|
||||
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
|
||||
_hostSpinner.userInteractionEnabled = NO;
|
||||
_hostSpinner.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_hostSpinner.hidesWhenStopped = YES;
|
||||
[self.iconBackgroundView addSubview:_hostSpinner];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_hostSpinner.centerXAnchor constraintEqualToAnchor:self.iconBackgroundView.centerXAnchor constant:0],
|
||||
[_hostSpinner.centerYAnchor constraintEqualToAnchor:self.iconBackgroundView.centerYAnchor constant:oldiOS ? -5.5*_sizeFactor : computerIconMonitorCenterYOffset],
|
||||
]];
|
||||
_hostSpinner.transform = CGAffineTransformMakeScale(_sizeFactor, _sizeFactor);
|
||||
[_hostSpinner stopAnimating];
|
||||
// [_hostSpinner startAnimating];
|
||||
|
||||
|
||||
// lockIcon
|
||||
lockIconView =[[UIImageView alloc] init];
|
||||
lockIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
lockIconView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
lockIconView.image = [[UIImage imageNamed:@"lock.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
} else {
|
||||
lockIconView.image = [UIImage imageNamed:@"LockedOverlayIcon"];
|
||||
computerIconMonitorCenterYOffset = -5 * _sizeFactor;
|
||||
}
|
||||
lockIconView.tintColor = [UIColor whiteColor];
|
||||
[self.iconBackgroundView insertSubview:lockIconView aboveSubview:_iconBackgroundView];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[lockIconView.centerXAnchor constraintEqualToAnchor:_iconBackgroundView.centerXAnchor constant:0],
|
||||
[lockIconView.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.centerYAnchor constant:computerIconMonitorCenterYOffset],
|
||||
[lockIconView.widthAnchor constraintEqualToConstant:17.05*_sizeFactor],
|
||||
[lockIconView.heightAnchor constraintEqualToConstant:17.05*_sizeFactor]
|
||||
]];
|
||||
lockIconView.hidden = true;
|
||||
|
||||
// 设备名
|
||||
CGFloat hostNameLabelCoordX = _cardContentpadding+_iconBackgroundView.frame.size.width+16*_sizeFactor;
|
||||
self.hostNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(hostNameLabelCoordX, _cardContentpadding+_iconBackgroundView.frame.size.height*0.02, cardWidth-hostNameLabelCoordX-_cardContentpadding, 30*_sizeFactor)];
|
||||
self.hostNameLabel.numberOfLines = 1;
|
||||
self.hostNameLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.hostNameLabel.minimumScaleFactor = 0.8; // 最小字体缩放比例
|
||||
self.hostNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
|
||||
self.hostIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.hostNameLabel.text = @"RazerBlade 16";
|
||||
self.hostNameLabel.textColor = [UIColor whiteColor]; //theme
|
||||
self.hostNameLabel.font = [UIFont boldSystemFontOfSize:18*_sizeFactor];
|
||||
[self addSubview:self.hostNameLabel];
|
||||
|
||||
// 在线文字
|
||||
self.statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(205, 68, 100, 24)];
|
||||
self.statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.statusLabel.text = [LocalizationHelper localizedStringForKey:@"Online"];
|
||||
self.statusLabel.font = [UIFont systemFontOfSize:14*_sizeFactor weight:UIFontWeightMedium];
|
||||
self.statusLabel.textColor = defaultGreen;
|
||||
// self.statusLabel.font = [UIFont systemFontOfSize:16*_sizeFactor];
|
||||
// 在线状态图标
|
||||
self.statusIcon = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 16*_sizeFactor, 16*_sizeFactor)];
|
||||
self.statusIcon.contentMode = UIViewContentModeScaleAspectFit;
|
||||
self.statusIcon.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.statusIcon.image = [[UIImage imageNamed:@"wifi_green"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
// self.statusIcon.tintColor = defaultGreen;
|
||||
[self addSubview:self.statusIcon];
|
||||
[self addSubview:self.statusLabel];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.statusLabel.leadingAnchor constraintEqualToAnchor:self.statusIcon.trailingAnchor constant:3*_sizeFactor],
|
||||
[self.statusLabel.topAnchor constraintEqualToAnchor:self.hostNameLabel.bottomAnchor constant:0*_sizeFactor],
|
||||
|
||||
[self.statusIcon.centerYAnchor constraintEqualToAnchor:self.statusLabel.centerYAnchor constant:0],
|
||||
[self.statusIcon.leadingAnchor constraintEqualToAnchor:self.hostNameLabel.leadingAnchor],
|
||||
[self.statusIcon.widthAnchor constraintEqualToConstant:16*_sizeFactor]
|
||||
]];
|
||||
|
||||
// 启动应用按钮
|
||||
self.appButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.appButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.appButton.frame = CGRectMake(20, 200, 150, buttonHeight);
|
||||
[self.appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
|
||||
[self.appButton setTitleColor:ThemeManager.textColorGray forState:UIControlStateNormal]; //theme
|
||||
self.appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
|
||||
[self.appButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
[self addSubview:self.appButton];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.appButton.leadingAnchor constraintEqualToAnchor:self.iconBackgroundView.leadingAnchor constant:0],
|
||||
[self.appButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
|
||||
[self.appButton.widthAnchor constraintEqualToConstant:appButtonWidth],
|
||||
[self.appButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
||||
// [self.launchButton.titleLabel.leadingAnchor constraintEqualToAnchor:self.launchButton.imageView.trailingAnchor constant:3*_sizeFactor]
|
||||
]];
|
||||
|
||||
|
||||
// 开始串流按钮
|
||||
self.launchButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.launchButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.launchButton.frame = CGRectMake(0, 0, 150, 50);
|
||||
self.launchButton.backgroundColor = defaultBlue;
|
||||
|
||||
self.launchButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||
[self.launchButton setTitle:[LocalizationHelper localizedStringForKey:@" Launch"] forState:UIControlStateNormal];
|
||||
[self.launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
||||
self.launchButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
|
||||
self.launchButton.tintColor = [UIColor whiteColor];
|
||||
[self.launchButton addTarget:self action:@selector(launchButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
[self addSubview:self.launchButton];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
||||
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.launchButton.leadingAnchor constraintEqualToAnchor:self.appButton.trailingAnchor constant:15*_sizeFactor],
|
||||
[self.launchButton.centerYAnchor constraintEqualToAnchor:self.appButton.centerYAnchor constant:0],
|
||||
[self.launchButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
|
||||
[self.launchButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
||||
// [self.launchButton.titleLabel.leadingAnchor constraintEqualToAnchor:self.launchButton.imageView.trailingAnchor constant:3*_sizeFactor]
|
||||
]];
|
||||
|
||||
// 配对按钮
|
||||
self.pairButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.pairButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.pairButton.frame = CGRectMake(0, 0, 150, 50);
|
||||
self.pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||
self.pairButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||
[self.pairButton setTitle:[LocalizationHelper localizedStringForKey:@" Pair with PIN"] forState:UIControlStateNormal];
|
||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
self.pairButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.4 weight:UIImageSymbolWeightBold];
|
||||
UIImage *templateImage = [UIImage systemImageNamed:@"lock.open.fill" withConfiguration:config];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:defaultBlue renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
[self.pairButton setImage:coloredImage forState:UIControlStateNormal];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
self.pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
|
||||
[self.pairButton addTarget:self action:@selector(pairButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
[self addSubview:self.pairButton];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.pairButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
|
||||
[self.pairButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
|
||||
[self.pairButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
|
||||
// [self.pairButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
|
||||
[self.pairButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
||||
]];
|
||||
|
||||
|
||||
|
||||
|
||||
// 唤醒按钮
|
||||
self.wakeupButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.wakeupButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.wakeupButton.frame = CGRectMake(0, 0, 150, 50);
|
||||
self.wakeupButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||
self.wakeupButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||
[self.wakeupButton setTitle:[LocalizationHelper localizedStringForKey:@" Wake-on-LAN"] forState:UIControlStateNormal];
|
||||
[self.wakeupButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
self.wakeupButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
|
||||
[self.wakeupButton addTarget:self action:@selector(wakeupButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
[self addSubview:self.wakeupButton];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.wakeupButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
|
||||
[self.wakeupButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
|
||||
[self.wakeupButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
|
||||
// [self.pairButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
|
||||
[self.wakeupButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
||||
]];
|
||||
|
||||
|
||||
|
||||
//分隔线
|
||||
_separatorLine = [[UIView alloc] init];
|
||||
_separatorLine.backgroundColor = [UIColor colorWithWhite:0.3 alpha:5.0];
|
||||
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_separatorLine];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
|
||||
[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
|
||||
[_separatorLine.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.bottomAnchor constant: iconAndButtonSpacing/2],
|
||||
[_separatorLine.heightAnchor constraintEqualToConstant:1.0/UIScreen.mainScreen.scale]
|
||||
]];
|
||||
|
||||
// 透明按钮
|
||||
self.transparentButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
// self.transparentButton.frame = CGRectMake(50, 100, 100, 100); // 设置点击区域
|
||||
self.transparentButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
// 设置透明背景和无内容
|
||||
self.transparentButton.backgroundColor = [UIColor clearColor];
|
||||
[self.transparentButton setTitle:@"" forState:UIControlStateNormal];
|
||||
[self.transparentButton setImage:nil forState:UIControlStateNormal];
|
||||
[self.transparentButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
|
||||
[self addSubview:self.transparentButton];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.transparentButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:0],
|
||||
[self.transparentButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:0],
|
||||
[self.transparentButton.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
|
||||
[self.transparentButton.bottomAnchor constraintEqualToAnchor:self.separatorLine.topAnchor],
|
||||
]];
|
||||
|
||||
_widthConstraint.constant = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
||||
_heightConstraint.constant = _cardContentpadding*2 + _iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1;
|
||||
|
||||
_size = CGSizeMake(_widthConstraint.constant, _heightConstraint.constant);
|
||||
|
||||
[self updateTheme];
|
||||
}
|
||||
|
||||
- (void)updateBackgroundLayerTheme{
|
||||
UIColor *gradientColorDark = [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0];
|
||||
UIColor *gradientColorLight = [gradientColorDark colorWithAlphaComponent:0.52];
|
||||
CGColorRef gradientColorRef = ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark ? gradientColorDark.CGColor : gradientColorLight.CGColor;
|
||||
backgroundLayer.colors = @[
|
||||
(__bridge id)[UIColor clearColor].CGColor,
|
||||
(__bridge id)[UIColor clearColor].CGColor,
|
||||
(__bridge id)[UIColor clearColor].CGColor,
|
||||
(__bridge id)gradientColorRef
|
||||
];
|
||||
}
|
||||
|
||||
- (void)createBackgroundLayer{
|
||||
backgroundLayer = [CAGradientLayer layer];
|
||||
[self updateBackgroundLayerTheme];
|
||||
backgroundLayer.locations = @[@0, @0.18, @0.5, @1];
|
||||
backgroundLayer.startPoint = CGPointMake(0.25, 0.5);
|
||||
backgroundLayer.endPoint = CGPointMake(0.75, 0.5);
|
||||
|
||||
CGAffineTransform transform = CGAffineTransformMake(-1.01, -1, 1, -3.67, 0.5, 2.83);
|
||||
backgroundLayer.transform = CATransform3DMakeAffineTransform(transform);
|
||||
[self.layer insertSublayer:backgroundLayer atIndex:0];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
backgroundLayer.bounds = CGRectInset(self.bounds, -0.5 * self.bounds.size.width, -0.5 * self.bounds.size.height);
|
||||
backgroundLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
|
||||
[self updateContentsForHost:_host]; // quick update before looping
|
||||
}
|
||||
|
||||
- (void)tintAdjustmentModeDidChange {
|
||||
// [super tintAdjustmentModeDidChange];
|
||||
NSLog(@"tintChanged........");
|
||||
self.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
|
||||
}
|
||||
|
||||
|
||||
- (void)updateTheme {
|
||||
self.backgroundColor = ThemeManager.widgetBackgroundColor;
|
||||
_hostNameLabel.textColor = ThemeManager.textColor;
|
||||
[_appButton setTitleColor:ThemeManager.appPrimaryColor forState:UIControlStateNormal];
|
||||
_separatorLine.backgroundColor = ThemeManager.hostCardSeparatorColor;
|
||||
|
||||
backgroundLayer.hidden = NO;
|
||||
[self updateBackgroundLayerTheme];
|
||||
|
||||
[self updateContentsForHost:_host];
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
// Start our update loop when we are added to our cell
|
||||
if (self.superview != nil && _host != nil) {
|
||||
NSLog(@"start update loop");
|
||||
[self updateLoop];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||
/*
|
||||
[super traitCollectionDidChange:previousTraitCollection];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
|
||||
[self updateTheme:self.traitCollection.userInterfaceStyle];
|
||||
}
|
||||
} else {
|
||||
[self updateTheme:UIUserInterfaceStyleDark];
|
||||
}*/
|
||||
}
|
||||
|
||||
- (bool)isDarkTheme{
|
||||
return ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark;
|
||||
}
|
||||
|
||||
- (void) updateLoop {
|
||||
// Stop immediately if the view has been detached
|
||||
if (self.superview == nil || [self.delegate isStreaming]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NSLog(@"hostCard update loop %f", CACurrentMediaTime());
|
||||
|
||||
[self updateContentsForHost:_host];
|
||||
|
||||
// Queue the next refresh cycle
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
}
|
||||
|
||||
- (void) updateContentsForHost:(TemporaryHost*)host {
|
||||
_hostNameLabel.text = host.name;
|
||||
backgroundLayer.hidden = !(host.state == StateOnline && host.pairState == PairStatePaired);
|
||||
|
||||
BOOL hostPaired = host.pairState == PairStatePaired;
|
||||
bool hasValidMac = host.mac != nil && ![host.mac isEqualToString:@"00:00:00:00:00:00"];
|
||||
|
||||
switch (host.state) {
|
||||
case StateOnline:
|
||||
[_hostSpinner stopAnimating];
|
||||
_statusLabel.textColor = defaultGreen;
|
||||
_statusLabel.text = [LocalizationHelper localizedStringForKey:@"Online"];
|
||||
self.statusIcon.image = [[UIImage imageNamed:@"wifi_green"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
self.statusIcon.hidden = NO;
|
||||
_appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
|
||||
if(host.pairState == PairStatePaired){
|
||||
_hostIconView.tintColor = [UIColor whiteColor];
|
||||
_iconBackgroundView.backgroundColor = defaultBlue;
|
||||
lockIconView.hidden = YES;
|
||||
[_appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
|
||||
[_launchButton setTitle:[LocalizationHelper localizedStringForKey:@" Launch"] forState:UIControlStateNormal];
|
||||
[_appButton setEnabled:YES];
|
||||
[_launchButton setEnabled:YES];
|
||||
_appButton.hidden = NO;
|
||||
_launchButton.hidden = NO;
|
||||
_pairButton.hidden = YES;
|
||||
_wakeupButton.hidden = YES;
|
||||
|
||||
[_appButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
||||
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
||||
} else {
|
||||
}
|
||||
_launchButton.backgroundColor = defaultBlue;
|
||||
[_launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
||||
}
|
||||
else {
|
||||
_iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha;
|
||||
UIColor* iconColor = [[UIColor whiteColor] colorWithAlphaComponent:[self isDarkTheme] ? 0.63 : 1];
|
||||
_hostIconView.tintColor = iconColor;
|
||||
lockIconView.tintColor = iconColor;
|
||||
lockIconView.hidden = NO;
|
||||
_appButton.hidden = YES;
|
||||
_launchButton.hidden = YES;
|
||||
_pairButton.hidden = NO;
|
||||
_wakeupButton.hidden = YES;
|
||||
}
|
||||
break;
|
||||
case StateOffline:
|
||||
[_hostSpinner stopAnimating];
|
||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
||||
_iconBackgroundView.backgroundColor = ThemeManager.offlineHostIconBackgroundColor;
|
||||
_statusLabel.textColor = ThemeManager.textColorGray;
|
||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Offline"];
|
||||
_statusIcon.tintColor = ThemeManager.textColorGray;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_statusIcon.image = [UIImage systemImageNamed:@"exclamationmark.triangle.fill"];
|
||||
} else {
|
||||
self.statusIcon.hidden = YES;
|
||||
}
|
||||
|
||||
_hostIconView.tintColor = ThemeManager.lowProfileGray;
|
||||
lockIconView.tintColor = ThemeManager.lowProfileGray;
|
||||
lockIconView.hidden = hostPaired;
|
||||
_appButton.hidden = YES;
|
||||
_launchButton.hidden = YES;
|
||||
_pairButton.hidden = YES;
|
||||
_wakeupButton.hidden = NO;
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : ThemeManager.textColorGray renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
self.wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : [ThemeManager.textColorGray colorWithAlphaComponent:0.2];
|
||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : ThemeManager.textColorGray forState:UIControlStateNormal];
|
||||
|
||||
break;
|
||||
case StateUnknown:
|
||||
_hostSpinner.color = [UIColor whiteColor];
|
||||
[_hostSpinner startAnimating];
|
||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
||||
_iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha;
|
||||
_statusLabel.textColor = ThemeManager.textColorGray;
|
||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Detecting..."];
|
||||
_statusIcon.tintColor = ThemeManager.textColorGray;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_statusIcon.image = [UIImage systemImageNamed:@"antenna.radiowaves.left.and.right"];
|
||||
} else {
|
||||
self.statusIcon.hidden = YES;
|
||||
}
|
||||
// _hostIconView.tintColor = [UIColor lowProfileGray];
|
||||
_hostIconView.tintColor = defaultBlue;
|
||||
lockIconView.hidden = YES;
|
||||
_appButton.hidden = YES;
|
||||
_launchButton.hidden = YES;
|
||||
_pairButton.hidden = YES;
|
||||
_wakeupButton.hidden = NO;
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : ThemeManager.textColorGray renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
self.wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : [ThemeManager.textColorGray colorWithAlphaComponent:0.2];
|
||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : ThemeManager.textColorGray forState:UIControlStateNormal];
|
||||
break;
|
||||
default:break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,593 @@
|
||||
//
|
||||
// HostCardView.m -> HostCardView.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025.5.18
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
protocol HostCardActionDelegate: NSObjectProtocol {
|
||||
@objc(appButtonTappedForHost:)
|
||||
func appButtonTapped(for host: TemporaryHost)
|
||||
@objc(launchButtonTappedForHost:)
|
||||
func launchButtonTapped(for host: TemporaryHost)
|
||||
@objc(wakeupButtonTappedForHost:)
|
||||
func wakeupButtonTapped(for host: TemporaryHost)
|
||||
@objc(pairButtonTappedForHost:)
|
||||
func pairButtonTapped(for host: TemporaryHost)
|
||||
@objc(hostCardLongPressed:view:)
|
||||
func hostCardLongPressed(_ host: TemporaryHost, view: UIView)
|
||||
@objc(isStreaming)
|
||||
func isStreaming() -> Bool
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
class HostCardView: UIView {
|
||||
var sizeFactor: CGFloat = 1
|
||||
private(set) var size: CGSize = .zero
|
||||
weak var delegate: HostCardActionDelegate?
|
||||
|
||||
private var iconBackgroundView: UIView!
|
||||
private var hostIconView: UIImageView!
|
||||
private var hostNameLabel: UILabel!
|
||||
private var statusIcon: UIImageView!
|
||||
private var statusLabel: UILabel!
|
||||
private var appButton: UIButton!
|
||||
private var launchButton: UIButton!
|
||||
private var pairButton: UIButton!
|
||||
private var wakeupButton: UIButton!
|
||||
private var transparentButton: UIButton!
|
||||
private var widthConstraint: NSLayoutConstraint!
|
||||
private var heightConstraint: NSLayoutConstraint!
|
||||
private var separatorLine: UIView!
|
||||
private var cardContentpadding: CGFloat = 0
|
||||
|
||||
private var host: TemporaryHost?
|
||||
private var hostSpinner: UIActivityIndicatorView!
|
||||
private var lockIconView: UIImageView!
|
||||
private var computerIconMonitorCenterYOffset: CGFloat = 0
|
||||
private var buttonHeight: CGFloat = 0
|
||||
private var buttonLabelFontSize: CGFloat = 0
|
||||
private var iconAndButtonSpacing: CGFloat = 0
|
||||
private var defaultBlue: UIColor = ThemeManager.appPrimaryColor
|
||||
private var defaultGreen: UIColor = UIColor(red: 52 / 255, green: 199 / 255, blue: 89 / 255, alpha: 1)
|
||||
private var backgroundLayer: CAGradientLayer!
|
||||
private var longPressFired = false
|
||||
|
||||
private static let refreshCycle: TimeInterval = 2
|
||||
private let stateUnknown = State(rawValue: 0)!
|
||||
private let stateOffline = State(rawValue: 1)!
|
||||
private let stateOnline = State(rawValue: 2)!
|
||||
private let pairStatePaired = PairState(rawValue: 2)!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@objc(initWithHost:)
|
||||
convenience init(host: TemporaryHost) {
|
||||
self.init(host: host, andSizeFactor: 1)
|
||||
}
|
||||
|
||||
@objc(initWithHost:andSizeFactor:)
|
||||
init(host: TemporaryHost, andSizeFactor sizeFactor: CGFloat) {
|
||||
self.sizeFactor = sizeFactor
|
||||
self.host = host
|
||||
super.init(frame: .zero)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
buttonLabelFontSize = 15 * sizeFactor
|
||||
longPressFired = false
|
||||
computerIconMonitorCenterYOffset = isIPhone() ? -2.75 * sizeFactor : -3.2 * sizeFactor
|
||||
iconAndButtonSpacing = 37 * sizeFactor
|
||||
buttonHeight = 39 * sizeFactor
|
||||
defaultBlue = ThemeManager.appPrimaryColor
|
||||
defaultGreen = UIColor(red: 52 / 255, green: 199 / 255, blue: 89 / 255, alpha: 1)
|
||||
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(hostCardLongPressed(_:)))
|
||||
addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
createBackgroundLayer()
|
||||
setupUI()
|
||||
}
|
||||
|
||||
private func isIPhone() -> Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .phone
|
||||
}
|
||||
|
||||
@objc func appButtonTapped() {
|
||||
if longPressFired {
|
||||
longPressFired = false
|
||||
return
|
||||
}
|
||||
guard let host else { return }
|
||||
if let delegate {
|
||||
delegate.appButtonTapped(for: host)
|
||||
} else {
|
||||
NSLog("Delegate not set or does not respond to appButtonTappedForHost:")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func launchButtonTapped() {
|
||||
if longPressFired {
|
||||
longPressFired = false
|
||||
return
|
||||
}
|
||||
guard let host else { return }
|
||||
if let delegate {
|
||||
delegate.launchButtonTapped(for: host)
|
||||
} else {
|
||||
NSLog("Delegate not set or does not respond to launchButtonTappedForHost:")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func wakeupButtonTapped() {
|
||||
if longPressFired {
|
||||
longPressFired = false
|
||||
return
|
||||
}
|
||||
guard let host else { return }
|
||||
if let delegate {
|
||||
delegate.wakeupButtonTapped(for: host)
|
||||
} else {
|
||||
NSLog("Delegate not set or does not respond to launchButtonTappedForHost:")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func pairButtonTapped() {
|
||||
if longPressFired {
|
||||
longPressFired = false
|
||||
return
|
||||
}
|
||||
guard let host else { return }
|
||||
if let delegate {
|
||||
delegate.pairButtonTapped(for: host)
|
||||
} else {
|
||||
NSLog("Delegate not set or does not respond to pairButtonTappedForHost:")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func hostCardLongPressed(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard gesture.state == .began, let host else { return }
|
||||
longPressFired = true
|
||||
if let delegate {
|
||||
delegate.hostCardLongPressed(host, view: self)
|
||||
} else {
|
||||
NSLog("Delegate not set or does not respond")
|
||||
}
|
||||
}
|
||||
|
||||
@objc(resizeBySizeFactor:)
|
||||
func resizeBySizeFactor(_ factor: CGFloat) {
|
||||
subviews.forEach { $0.removeFromSuperview() }
|
||||
sizeFactor = factor
|
||||
setupUI()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
isUserInteractionEnabled = true
|
||||
backgroundColor = ThemeManager.widgetBackgroundColor
|
||||
cardContentpadding = 13 * sizeFactor
|
||||
clipsToBounds = true
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let appButtonWidth = 100 * sizeFactor
|
||||
let launchButtonWidth = 120 * sizeFactor
|
||||
let cardWidth = cardContentpadding * 2 + appButtonWidth + 15 * sizeFactor + launchButtonWidth
|
||||
layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0603 / 2))
|
||||
if #available(iOS 13.0, *) {
|
||||
layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: 300)
|
||||
heightConstraint.isActive = true
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: 300)
|
||||
widthConstraint.isActive = true
|
||||
|
||||
iconBackgroundView = UIView(frame: CGRect(x: cardContentpadding, y: cardContentpadding, width: 80 * sizeFactor, height: 80 * sizeFactor))
|
||||
iconBackgroundView.backgroundColor = defaultBlue
|
||||
iconBackgroundView.layer.cornerRadius = 2 * CGFloat(UInt16(20 * sizeFactor / 2))
|
||||
if #available(iOS 13.0, *) {
|
||||
iconBackgroundView.layer.cornerCurve = .continuous
|
||||
}
|
||||
addSubview(iconBackgroundView)
|
||||
|
||||
hostIconView = UIImageView()
|
||||
hostIconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostIconView.contentMode = .scaleAspectFit
|
||||
if #available(iOS 13.0, *) {
|
||||
hostIconView.image = UIImage(named: "display")?.withRenderingMode(.alwaysTemplate)
|
||||
} else {
|
||||
hostIconView.image = UIImage(named: "Computer")
|
||||
NSLayoutConstraint.activate([
|
||||
hostIconView.heightAnchor.constraint(equalToConstant: 57 * sizeFactor),
|
||||
hostIconView.widthAnchor.constraint(equalToConstant: 57 * sizeFactor),
|
||||
])
|
||||
}
|
||||
hostIconView.tintColor = .white
|
||||
iconBackgroundView.addSubview(hostIconView)
|
||||
NSLayoutConstraint.activate([
|
||||
hostIconView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||
hostIconView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
|
||||
hostIconView.heightAnchor.constraint(equalToConstant: 53.9 * sizeFactor),
|
||||
hostIconView.widthAnchor.constraint(equalToConstant: 53.9 * sizeFactor),
|
||||
])
|
||||
|
||||
let oldIOS: Bool
|
||||
if #available(iOS 13.0, *) {
|
||||
oldIOS = false
|
||||
} else {
|
||||
oldIOS = true
|
||||
}
|
||||
|
||||
hostSpinner = UIActivityIndicatorView(style: .white)
|
||||
hostSpinner.isUserInteractionEnabled = false
|
||||
hostSpinner.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostSpinner.hidesWhenStopped = true
|
||||
iconBackgroundView.addSubview(hostSpinner)
|
||||
NSLayoutConstraint.activate([
|
||||
hostSpinner.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||
hostSpinner.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor, constant: oldIOS ? -5.5 * sizeFactor : computerIconMonitorCenterYOffset),
|
||||
])
|
||||
hostSpinner.transform = CGAffineTransform(scaleX: sizeFactor, y: sizeFactor)
|
||||
hostSpinner.stopAnimating()
|
||||
|
||||
lockIconView = UIImageView()
|
||||
lockIconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
lockIconView.contentMode = .scaleAspectFit
|
||||
if #available(iOS 13.0, *) {
|
||||
lockIconView.image = UIImage(named: "lock.fill")?.withRenderingMode(.alwaysTemplate)
|
||||
} else {
|
||||
lockIconView.image = UIImage(named: "LockedOverlayIcon")
|
||||
computerIconMonitorCenterYOffset = -5 * sizeFactor
|
||||
}
|
||||
lockIconView.tintColor = .white
|
||||
iconBackgroundView.addSubview(lockIconView)
|
||||
NSLayoutConstraint.activate([
|
||||
lockIconView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||
lockIconView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor, constant: computerIconMonitorCenterYOffset),
|
||||
lockIconView.widthAnchor.constraint(equalToConstant: 17.05 * sizeFactor),
|
||||
lockIconView.heightAnchor.constraint(equalToConstant: 17.05 * sizeFactor),
|
||||
])
|
||||
lockIconView.isHidden = true
|
||||
|
||||
let hostNameLabelCoordX = cardContentpadding + iconBackgroundView.frame.size.width + 16 * sizeFactor
|
||||
hostNameLabel = UILabel(frame: CGRect(x: hostNameLabelCoordX, y: cardContentpadding + iconBackgroundView.frame.size.height * 0.02, width: cardWidth - hostNameLabelCoordX - cardContentpadding, height: 30 * sizeFactor))
|
||||
hostNameLabel.numberOfLines = 1
|
||||
hostNameLabel.adjustsFontSizeToFitWidth = true
|
||||
hostNameLabel.minimumScaleFactor = 0.8
|
||||
hostNameLabel.lineBreakMode = .byTruncatingTail
|
||||
hostNameLabel.text = "RazerBlade 16"
|
||||
hostNameLabel.textColor = .white
|
||||
hostNameLabel.font = .boldSystemFont(ofSize: 18 * sizeFactor)
|
||||
addSubview(hostNameLabel)
|
||||
|
||||
statusLabel = UILabel(frame: CGRect(x: 205, y: 68, width: 100, height: 24))
|
||||
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusLabel.text = LocalizationHelper.localizedString(forKey: "Online")
|
||||
statusLabel.font = .systemFont(ofSize: 14 * sizeFactor, weight: .medium)
|
||||
statusLabel.textColor = defaultGreen
|
||||
|
||||
statusIcon = UIImageView(frame: CGRect(x: 0, y: 0, width: 16 * sizeFactor, height: 16 * sizeFactor))
|
||||
statusIcon.contentMode = .scaleAspectFit
|
||||
statusIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusIcon.image = UIImage(named: "wifi_green")?.withRenderingMode(.alwaysOriginal)
|
||||
addSubview(statusIcon)
|
||||
addSubview(statusLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
statusLabel.leadingAnchor.constraint(equalTo: statusIcon.trailingAnchor, constant: 3 * sizeFactor),
|
||||
statusLabel.topAnchor.constraint(equalTo: hostNameLabel.bottomAnchor),
|
||||
statusIcon.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor),
|
||||
statusIcon.leadingAnchor.constraint(equalTo: hostNameLabel.leadingAnchor),
|
||||
statusIcon.widthAnchor.constraint(equalToConstant: 16 * sizeFactor),
|
||||
])
|
||||
|
||||
appButton = UIButton(type: .system)
|
||||
appButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
appButton.frame = CGRect(x: 20, y: 200, width: 150, height: buttonHeight)
|
||||
appButton.setTitle(LocalizationHelper.localizedString(forKey: "Applications"), for: .normal)
|
||||
appButton.setTitleColor(ThemeManager.textColorGray, for: .normal)
|
||||
appButton.titleLabel?.font = .systemFont(ofSize: buttonLabelFontSize)
|
||||
appButton.addTarget(self, action: #selector(appButtonTapped), for: .primaryActionTriggered)
|
||||
addSubview(appButton)
|
||||
NSLayoutConstraint.activate([
|
||||
appButton.leadingAnchor.constraint(equalTo: iconBackgroundView.leadingAnchor),
|
||||
appButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
|
||||
appButton.widthAnchor.constraint(equalToConstant: appButtonWidth),
|
||||
appButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||
])
|
||||
|
||||
launchButton = UIButton(type: .system)
|
||||
launchButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
launchButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
|
||||
launchButton.backgroundColor = defaultBlue
|
||||
launchButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
|
||||
if #available(iOS 13.0, *) {
|
||||
launchButton.layer.cornerCurve = .continuous
|
||||
}
|
||||
launchButton.setTitle(LocalizationHelper.localizedString(forKey: " Launch"), for: .normal)
|
||||
launchButton.setTitleColor(.white, for: .normal)
|
||||
launchButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
|
||||
launchButton.tintColor = .white
|
||||
launchButton.addTarget(self, action: #selector(launchButtonTapped), for: .primaryActionTriggered)
|
||||
addSubview(launchButton)
|
||||
if #available(iOS 13.0, *) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight * 0.263)
|
||||
launchButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
launchButton.leadingAnchor.constraint(equalTo: appButton.trailingAnchor, constant: 15 * sizeFactor),
|
||||
launchButton.centerYAnchor.constraint(equalTo: appButton.centerYAnchor),
|
||||
launchButton.widthAnchor.constraint(equalToConstant: launchButtonWidth),
|
||||
launchButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||
])
|
||||
|
||||
pairButton = UIButton(type: .system)
|
||||
pairButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
pairButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
|
||||
pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha
|
||||
pairButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
|
||||
if #available(iOS 13.0, *) {
|
||||
pairButton.layer.cornerCurve = .continuous
|
||||
}
|
||||
pairButton.setTitle(LocalizationHelper.localizedString(forKey: " Pair with PIN"), for: .normal)
|
||||
pairButton.setTitleColor(defaultBlue, for: .normal)
|
||||
pairButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
|
||||
if #available(iOS 13.0, *) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.4, weight: .bold)
|
||||
let templateImage = UIImage(systemName: "lock.open.fill", withConfiguration: config)
|
||||
let coloredImage = templateImage?.withTintColor(defaultBlue, renderingMode: .alwaysOriginal)
|
||||
pairButton.setImage(coloredImage, for: .normal)
|
||||
}
|
||||
pairButton.addTarget(self, action: #selector(pairButtonTapped), for: .primaryActionTriggered)
|
||||
addSubview(pairButton)
|
||||
NSLayoutConstraint.activate([
|
||||
pairButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
|
||||
pairButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
|
||||
pairButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
|
||||
pairButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||
])
|
||||
|
||||
wakeupButton = UIButton(type: .system)
|
||||
wakeupButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
wakeupButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
|
||||
wakeupButton.backgroundColor = ThemeManager.textTintColorWithAlpha
|
||||
wakeupButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
|
||||
if #available(iOS 13.0, *) {
|
||||
wakeupButton.layer.cornerCurve = .continuous
|
||||
}
|
||||
wakeupButton.setTitle(LocalizationHelper.localizedString(forKey: " Wake-on-LAN"), for: .normal)
|
||||
wakeupButton.setTitleColor(defaultBlue, for: .normal)
|
||||
wakeupButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
|
||||
wakeupButton.addTarget(self, action: #selector(wakeupButtonTapped), for: .primaryActionTriggered)
|
||||
addSubview(wakeupButton)
|
||||
NSLayoutConstraint.activate([
|
||||
wakeupButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
|
||||
wakeupButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
|
||||
wakeupButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
|
||||
wakeupButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||
])
|
||||
|
||||
separatorLine = UIView()
|
||||
separatorLine.backgroundColor = UIColor(white: 0.3, alpha: 5)
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(separatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
|
||||
separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
|
||||
separatorLine.centerYAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing / 2),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
|
||||
])
|
||||
|
||||
transparentButton = UIButton(type: .custom)
|
||||
transparentButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
transparentButton.backgroundColor = .clear
|
||||
transparentButton.setTitle("", for: .normal)
|
||||
transparentButton.setImage(nil, for: .normal)
|
||||
transparentButton.addTarget(self, action: #selector(appButtonTapped), for: .primaryActionTriggered)
|
||||
addSubview(transparentButton)
|
||||
NSLayoutConstraint.activate([
|
||||
transparentButton.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
transparentButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
transparentButton.topAnchor.constraint(equalTo: topAnchor),
|
||||
transparentButton.bottomAnchor.constraint(equalTo: separatorLine.topAnchor),
|
||||
])
|
||||
|
||||
widthConstraint.constant = cardContentpadding * 2 + appButtonWidth + 15 * sizeFactor + launchButtonWidth
|
||||
heightConstraint.constant = cardContentpadding * 2 + iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1
|
||||
size = CGSize(width: widthConstraint.constant, height: heightConstraint.constant)
|
||||
|
||||
updateTheme()
|
||||
}
|
||||
|
||||
private func updateBackgroundLayerTheme() {
|
||||
let gradientColorDark = UIColor(red: 0, green: 0.319, blue: 0.64, alpha: 1)
|
||||
let gradientColorLight = gradientColorDark.withAlphaComponent(0.52)
|
||||
let gradientColor = ThemeManager.userInterfaceStyle() == .dark ? gradientColorDark.cgColor : gradientColorLight.cgColor
|
||||
backgroundLayer.colors = [
|
||||
UIColor.clear.cgColor,
|
||||
UIColor.clear.cgColor,
|
||||
UIColor.clear.cgColor,
|
||||
gradientColor,
|
||||
]
|
||||
}
|
||||
|
||||
private func createBackgroundLayer() {
|
||||
backgroundLayer = CAGradientLayer()
|
||||
updateBackgroundLayerTheme()
|
||||
backgroundLayer.locations = [0, 0.18, 0.5, 1]
|
||||
backgroundLayer.startPoint = CGPoint(x: 0.25, y: 0.5)
|
||||
backgroundLayer.endPoint = CGPoint(x: 0.75, y: 0.5)
|
||||
let transform = CGAffineTransform(a: -1.01, b: -1, c: 1, d: -3.67, tx: 0.5, ty: 2.83)
|
||||
backgroundLayer.transform = CATransform3DMakeAffineTransform(transform)
|
||||
layer.insertSublayer(backgroundLayer, at: 0)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
backgroundLayer.bounds = bounds.insetBy(dx: -0.5 * bounds.size.width, dy: -0.5 * bounds.size.height)
|
||||
backgroundLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
updateContents(for: host)
|
||||
}
|
||||
|
||||
func tintAdjustmentModeDidChange() {
|
||||
NSLog("tintChanged........")
|
||||
tintAdjustmentMode = .normal
|
||||
}
|
||||
|
||||
func updateTheme() {
|
||||
backgroundColor = ThemeManager.widgetBackgroundColor
|
||||
hostNameLabel?.textColor = ThemeManager.textColor
|
||||
appButton?.setTitleColor(ThemeManager.appPrimaryColor, for: .normal)
|
||||
separatorLine?.backgroundColor = ThemeManager.hostCardSeparatorColor
|
||||
|
||||
backgroundLayer?.isHidden = false
|
||||
updateBackgroundLayerTheme()
|
||||
updateContents(for: host)
|
||||
}
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
if superview != nil && host != nil {
|
||||
NSLog("start update loop")
|
||||
updateLoop()
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
}
|
||||
|
||||
private func isDarkTheme() -> Bool {
|
||||
ThemeManager.userInterfaceStyle() == .dark
|
||||
}
|
||||
|
||||
@objc func updateLoop() {
|
||||
if superview == nil || delegate?.isStreaming() == true {
|
||||
return
|
||||
}
|
||||
|
||||
updateContents(for: host)
|
||||
perform(#selector(updateLoop), with: self, afterDelay: Self.refreshCycle)
|
||||
}
|
||||
|
||||
private func updateContents(for host: TemporaryHost?) {
|
||||
guard let host else { return }
|
||||
|
||||
hostNameLabel.text = host.name
|
||||
backgroundLayer.isHidden = !(host.state == stateOnline && host.pairState == pairStatePaired)
|
||||
|
||||
let hostPaired = host.pairState == pairStatePaired
|
||||
let hasValidMac = host.mac != nil && host.mac != "00:00:00:00:00:00"
|
||||
|
||||
switch host.state {
|
||||
case stateOnline:
|
||||
hostSpinner.stopAnimating()
|
||||
statusLabel.textColor = defaultGreen
|
||||
statusLabel.text = LocalizationHelper.localizedString(forKey: "Online")
|
||||
statusIcon.image = UIImage(named: "wifi_green")?.withRenderingMode(.alwaysOriginal)
|
||||
statusIcon.isHidden = false
|
||||
appButton.titleLabel?.font = .systemFont(ofSize: buttonLabelFontSize)
|
||||
if host.pairState == pairStatePaired {
|
||||
hostIconView.tintColor = .white
|
||||
iconBackgroundView.backgroundColor = defaultBlue
|
||||
lockIconView.isHidden = true
|
||||
appButton.setTitle(LocalizationHelper.localizedString(forKey: "Applications"), for: .normal)
|
||||
launchButton.setTitle(LocalizationHelper.localizedString(forKey: " Launch"), for: .normal)
|
||||
appButton.isEnabled = true
|
||||
launchButton.isEnabled = true
|
||||
appButton.isHidden = false
|
||||
launchButton.isHidden = false
|
||||
pairButton.isHidden = true
|
||||
wakeupButton.isHidden = true
|
||||
appButton.setTitleColor(defaultBlue, for: .normal)
|
||||
if #available(iOS 13.0, *) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight * 0.263)
|
||||
launchButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
|
||||
}
|
||||
launchButton.backgroundColor = defaultBlue
|
||||
launchButton.setTitleColor(.white, for: .normal)
|
||||
} else {
|
||||
iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha
|
||||
let iconColor = UIColor.white.withAlphaComponent(isDarkTheme() ? 0.63 : 1)
|
||||
hostIconView.tintColor = iconColor
|
||||
lockIconView.tintColor = iconColor
|
||||
lockIconView.isHidden = false
|
||||
appButton.isHidden = true
|
||||
launchButton.isHidden = true
|
||||
pairButton.isHidden = false
|
||||
wakeupButton.isHidden = true
|
||||
}
|
||||
|
||||
case stateOffline:
|
||||
hostSpinner.stopAnimating()
|
||||
iconBackgroundView.backgroundColor = ThemeManager.offlineHostIconBackgroundColor
|
||||
statusLabel.textColor = ThemeManager.textColorGray
|
||||
statusLabel.text = LocalizationHelper.localizedString(forKey: "Offline")
|
||||
statusIcon.tintColor = ThemeManager.textColorGray
|
||||
if #available(iOS 13.0, *) {
|
||||
statusIcon.image = UIImage(systemName: "exclamationmark.triangle.fill")
|
||||
} else {
|
||||
statusIcon.isHidden = true
|
||||
}
|
||||
|
||||
hostIconView.tintColor = ThemeManager.lowProfileGray
|
||||
lockIconView.tintColor = ThemeManager.lowProfileGray
|
||||
lockIconView.isHidden = hostPaired
|
||||
appButton.isHidden = true
|
||||
launchButton.isHidden = true
|
||||
pairButton.isHidden = true
|
||||
wakeupButton.isHidden = false
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.45, weight: .bold)
|
||||
let templateImage = UIImage(systemName: "power", withConfiguration: config)
|
||||
let color = hasValidMac ? defaultBlue : ThemeManager.textColorGray
|
||||
wakeupButton.setImage(templateImage?.withTintColor(color, renderingMode: .alwaysOriginal), for: .normal)
|
||||
}
|
||||
|
||||
wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : ThemeManager.textColorGray.withAlphaComponent(0.2)
|
||||
wakeupButton.setTitleColor(hasValidMac ? defaultBlue : ThemeManager.textColorGray, for: .normal)
|
||||
|
||||
case stateUnknown:
|
||||
hostSpinner.color = .white
|
||||
hostSpinner.startAnimating()
|
||||
iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha
|
||||
statusLabel.textColor = ThemeManager.textColorGray
|
||||
statusLabel.text = LocalizationHelper.localizedString(forKey: "Detecting...")
|
||||
statusIcon.tintColor = ThemeManager.textColorGray
|
||||
if #available(iOS 13.0, *) {
|
||||
statusIcon.image = UIImage(systemName: "antenna.radiowaves.left.and.right")
|
||||
} else {
|
||||
statusIcon.isHidden = true
|
||||
}
|
||||
hostIconView.tintColor = defaultBlue
|
||||
lockIconView.isHidden = true
|
||||
appButton.isHidden = true
|
||||
launchButton.isHidden = true
|
||||
pairButton.isHidden = true
|
||||
wakeupButton.isHidden = false
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.45, weight: .bold)
|
||||
let templateImage = UIImage(systemName: "power", withConfiguration: config)
|
||||
let color = hasValidMac ? defaultBlue : ThemeManager.textColorGray
|
||||
wakeupButton.setImage(templateImage?.withTintColor(color, renderingMode: .alwaysOriginal), for: .normal)
|
||||
}
|
||||
wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : ThemeManager.textColorGray.withAlphaComponent(0.2)
|
||||
wakeupButton.setTitleColor(hasValidMac ? defaultBlue : ThemeManager.textColorGray, for: .normal)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,7 @@ import UIKit
|
||||
"BRUSH",
|
||||
"ERASER",
|
||||
"NOSINGLETOUCH",
|
||||
"DISABLETILT",
|
||||
"DISABLETOUCH",
|
||||
"GAMEPADOVERLAY",
|
||||
]
|
||||
@@ -161,7 +162,8 @@ import UIKit
|
||||
"PENCILHOVER",
|
||||
"BRUSH",
|
||||
"ERASER",
|
||||
"NOSINGLETOUCH"
|
||||
"NOSINGLETOUCH",
|
||||
"DISABLETILT"
|
||||
]
|
||||
@objc public static let motionControlButtonCmds: [String] = ["GYRO","GYROPAUSE","ACCEL","MOTION"]
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// ControllerNavigator.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2026/7/1.
|
||||
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GameController
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
@objc protocol ControllerNavigatorRadialMenuDelegate: AnyObject {
|
||||
func controllerNavigatorDidSelectSettings()
|
||||
func controllerNavigatorDidSelectHostView()
|
||||
func controllerNavigatorDidSelectGameProfiles()
|
||||
func controllerNavigatorDidSelectExit()
|
||||
func isStreaming() -> Bool
|
||||
func isInAppView() -> Bool
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
@objc protocol ControllerUINavigationDelegate: AnyObject {
|
||||
func nextItemForControllerNavigator()
|
||||
func previousItemForControllerNavigator()
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
final class ControllerNavigator: NSObject {
|
||||
typealias Handler = (_ buttonDict: NSDictionary, _ gamepad: GCExtendedGamepad, _ element: GCControllerElement) -> Void
|
||||
|
||||
private static var connectObserver: NSObjectProtocol?
|
||||
private static var disconnectObserver: NSObjectProtocol?
|
||||
private static weak var listeningController: GCController?
|
||||
private static weak var radialMenuDelegate: ControllerNavigatorRadialMenuDelegate?
|
||||
private static weak var uiNavigationDelegate: ControllerUINavigationDelegate?
|
||||
private static var swapABXY = false
|
||||
private static var radialMenuView: RadialMenuOverlayView?
|
||||
private static var localRadialMenuButton: ControllerButton = .leftShoulder
|
||||
private static var localRadialMenuButtonFlag: Bool = false
|
||||
|
||||
@objc static func setRadialMenuDelegate(_ delegate: ControllerNavigatorRadialMenuDelegate?) {
|
||||
radialMenuDelegate = delegate
|
||||
}
|
||||
|
||||
@objc static func setUINavigationDelegate(_ delegate: ControllerUINavigationDelegate?) {
|
||||
uiNavigationDelegate = delegate
|
||||
}
|
||||
|
||||
@objc static func start(swapABXY: Bool = false) {
|
||||
Self.swapABXY = swapABXY
|
||||
installControllerObserversIfNeeded()
|
||||
listenFirstAvailableController()
|
||||
}
|
||||
|
||||
@objc static func stop() {
|
||||
stopListeningCurrentController()
|
||||
removeControllerObservers()
|
||||
}
|
||||
|
||||
static func listen(controller: GCController, swapABXY: Bool = false) {
|
||||
guard controller.extendedGamepad != nil else { return }
|
||||
listeningController = controller
|
||||
ControllerUtil.listen(controller: controller, swapABXY: swapABXY) { buttonDict, gamepad, element in
|
||||
|
||||
if let gcButton = buttonDict[localRadialMenuButton.rawValue] as? GCControllerButtonInput {
|
||||
if gcButton.isPressed != localRadialMenuButtonFlag {
|
||||
if gcButton.isPressed, radialMenuView == nil {
|
||||
RadialMenuOverlayView.menuSectors.removeAll()
|
||||
RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "Settings Menu".localized, subtitle: "", systemImageName: "sidebar.left", item: .settings))
|
||||
if radialMenuDelegate?.isInAppView() == true, !(radialMenuDelegate?.isStreaming() ?? false) {RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "Host View".localized, subtitle: "", systemImageName: PublicUtils.liquidGlassEnabled ? "macwindow.on.rectangle" : "tv", item: .hostView))}
|
||||
if radialMenuDelegate?.isStreaming() == true {RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "Disconnect".localized, subtitle: "", systemImageName: PublicUtils.disconnectSymbol(), item: .exit))}
|
||||
RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "Game Profiles".localized, subtitle: "", systemImageName:PublicUtils.iOS18Available ? "gamecontroller.circle" : "gamecontroller.fill", item: .gameProfiles))
|
||||
|
||||
radialMenuView = RadialMenuOverlayView.presentInKeyWindow()
|
||||
}
|
||||
if !gcButton.isPressed {
|
||||
radialMenuView?.dismiss()
|
||||
radialMenuView = nil
|
||||
}
|
||||
}
|
||||
localRadialMenuButtonFlag = gcButton.isPressed
|
||||
}
|
||||
|
||||
if let selectedItem = radialMenuView?.updateSelection(
|
||||
xOffset: gamepad.rightThumbstick.xAxis.value,
|
||||
yOffset: gamepad.rightThumbstick.yAxis.value
|
||||
) {
|
||||
performRadialMenuAction(selectedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func installControllerObserversIfNeeded() {
|
||||
guard connectObserver == nil, disconnectObserver == nil else { return }
|
||||
|
||||
connectObserver = NotificationCenter.default.addObserver(
|
||||
forName: .GCControllerDidConnect,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { notification in
|
||||
guard (notification.object as? GCController)?.extendedGamepad != nil else { return }
|
||||
listenFirstAvailableController()
|
||||
}
|
||||
|
||||
disconnectObserver = NotificationCenter.default.addObserver(
|
||||
forName: .GCControllerDidDisconnect,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { notification in
|
||||
listenFirstAvailableController()
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeControllerObservers() {
|
||||
if let connectObserver {
|
||||
NotificationCenter.default.removeObserver(connectObserver)
|
||||
self.connectObserver = nil
|
||||
}
|
||||
|
||||
if let disconnectObserver {
|
||||
NotificationCenter.default.removeObserver(disconnectObserver)
|
||||
self.disconnectObserver = nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func listenFirstAvailableController() {
|
||||
guard let controller = GCController.controllers().first(where: { $0.extendedGamepad != nil }) else {
|
||||
stopListeningCurrentController()
|
||||
radialMenuView?.dismiss()
|
||||
radialMenuView = nil
|
||||
return
|
||||
}
|
||||
|
||||
listen(controller: controller)
|
||||
}
|
||||
|
||||
private static func listen(controller: GCController) {
|
||||
guard controller !== listeningController else { return }
|
||||
stopListeningCurrentController()
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
for element in controller.physicalInputProfile.allElements {
|
||||
element.preferredSystemGestureState = .disabled
|
||||
}
|
||||
}
|
||||
|
||||
listen(controller: controller, swapABXY: swapABXY)
|
||||
}
|
||||
|
||||
private static func stopListeningCurrentController() {
|
||||
listeningController?.extendedGamepad?.valueChangedHandler = nil
|
||||
listeningController = nil
|
||||
}
|
||||
|
||||
private static func performRadialMenuAction(_ item: RadialMenuItem) {
|
||||
switch item {
|
||||
case .settings:
|
||||
radialMenuDelegate?.controllerNavigatorDidSelectSettings()
|
||||
|
||||
case .hostView:
|
||||
radialMenuDelegate?.controllerNavigatorDidSelectHostView()
|
||||
|
||||
case .gameProfiles:
|
||||
radialMenuDelegate?.controllerNavigatorDidSelectGameProfiles()
|
||||
|
||||
case .exit:
|
||||
radialMenuDelegate?.controllerNavigatorDidSelectExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1161,7 @@ double rc_expo(double x, double expo) {
|
||||
if(button.pressed){
|
||||
if(buttonFlagId.intValue == self->_controllerMouseSwitch){
|
||||
self->_mouseSwitchButtonPressed = true;
|
||||
self->mouseSwitchDownTimestamp = CACurrentMediaTime();
|
||||
if(self->mouseSwitchDownTimestamp == 0) self->mouseSwitchDownTimestamp = CACurrentMediaTime();
|
||||
}
|
||||
}
|
||||
else{
|
||||
@@ -1620,7 +1620,7 @@ double rc_expo(double x, double expo) {
|
||||
-(VoidController* )assignController:(GCController*)controller {
|
||||
NSLog(@"run assignController");
|
||||
|
||||
bool newGCControllerArrival = ![ControllerUtil.activeGCControllers containsObject:controller];
|
||||
bool newGCControllerArrival = ![ControllerUtil.activeStreamingGCControllers containsObject:controller];
|
||||
|
||||
if(!newGCControllerArrival){
|
||||
VoidController* voidController = [_voidControllers objectForKey:@(controller.playerIndex)];
|
||||
@@ -1642,7 +1642,7 @@ double rc_expo(double x, double expo) {
|
||||
VoidController* voidController = [[VoidController alloc] init];
|
||||
|
||||
|
||||
[ControllerUtil.activeGCControllers addObject:controller];
|
||||
[ControllerUtil.activeStreamingGCControllers addObject:controller];
|
||||
controller.playerIndex = i;
|
||||
voidController.playerIndex = i;
|
||||
[self updateVoidController:voidController withGCController:controller];
|
||||
@@ -1722,7 +1722,7 @@ double rc_expo(double x, double expo) {
|
||||
NSLog(@"controller count: iterating");
|
||||
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
NSLog(@"controller count: is supported,is contained by dict: %d", [ControllerUtil.activeGCControllers containsObject:controller]);
|
||||
NSLog(@"controller count: is supported,is contained by dict: %d", [ControllerUtil.activeStreamingGCControllers containsObject:controller]);
|
||||
NSLog(@"controller obj +1 in dic");
|
||||
[self assignController:controller];
|
||||
NSLog(@"controller obj num in dict: %lu", (unsigned long)_voidControllers.allValues.count);
|
||||
@@ -1839,7 +1839,7 @@ double rc_expo(double x, double expo) {
|
||||
_delegate = delegate;
|
||||
_controllerStreamLock = [[NSLock alloc] init];
|
||||
_voidControllers = [[NSMutableDictionary alloc] init];
|
||||
[ControllerUtil.activeGCControllers removeAllObjects];
|
||||
[ControllerUtil.activeStreamingGCControllers removeAllObjects];
|
||||
_controllerNumbers = 0;
|
||||
|
||||
_captureMouse = (streamConfig.localMousePointerMode == 0);
|
||||
@@ -1892,8 +1892,8 @@ double rc_expo(double x, double expo) {
|
||||
|
||||
[self unregisterControllerCallbacks:controller];
|
||||
|
||||
if([ControllerUtil.activeGCControllers containsObject:controller]){
|
||||
[ControllerUtil.activeGCControllers removeObject:controller];
|
||||
if([ControllerUtil.activeStreamingGCControllers containsObject:controller]){
|
||||
[ControllerUtil.activeStreamingGCControllers removeObject:controller];
|
||||
self->_controllerNumbers &= ~(1 << controller.playerIndex);
|
||||
}
|
||||
Log(LOG_I, @"Unassigning controller index: %ld", (long)controller.playerIndex);
|
||||
|
||||
@@ -166,7 +166,7 @@ import Foundation
|
||||
return result
|
||||
}
|
||||
|
||||
@objc static var activeGCControllers:NSMutableSet = NSMutableSet()
|
||||
@objc static var activeStreamingGCControllers:NSMutableSet = NSMutableSet()
|
||||
|
||||
@objc static func string(for button: ControllerButton) -> String {
|
||||
switch button {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
bool physicalGamepadConnected = ControllerUtil.activeGCControllers.count>0;
|
||||
bool physicalGamepadConnected = ControllerUtil.activeStreamingGCControllers.count>0;
|
||||
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, physicalGamepadConnected ? 0.005*NSEC_PER_SEC : 0), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if(!physicalGamepadConnected)
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
|
||||
@@ -189,7 +189,7 @@ import CoreMotion
|
||||
else {
|
||||
if #available(iOS 14.0, *) {
|
||||
if activeGCController == nil {
|
||||
if let controllers = ControllerUtil.activeGCControllers as? Set<GCController> {
|
||||
if let controllers = ControllerUtil.activeStreamingGCControllers as? Set<GCController> {
|
||||
for controller in controllers {
|
||||
if controller.playerIndex == .index1 {
|
||||
activeGCController = controller
|
||||
|
||||
@@ -65,6 +65,7 @@ import ObjectiveC.runtime
|
||||
func setAllowSingleTouchEnabled(_ enabled:Bool)
|
||||
func replaceBrush(shortcut:String)
|
||||
func replaceEraser(shortcut:String)
|
||||
func setPencilTilt(disabled:Bool)
|
||||
func presentPressureCurveVC()
|
||||
func toggleTouch(disabled:Bool)
|
||||
func toggleGamepadOverlay(overlayEnabled:Bool)
|
||||
@@ -491,9 +492,9 @@ import ObjectiveC.runtime
|
||||
|
||||
// print("widgetType: \(self.widgetType)")
|
||||
// print("touchPadString: \(self.touchPadString)")
|
||||
for comboButtonString in comboButtonStrings {
|
||||
// for comboButtonString in comboButtonStrings {
|
||||
// print("comboButtonString: \(comboButtonString)")
|
||||
}
|
||||
// }
|
||||
|
||||
self.widgetLabel = buttonLabel
|
||||
self.shape = shape
|
||||
@@ -670,7 +671,8 @@ import ObjectiveC.runtime
|
||||
&& (buttonMode == .slideAndHold || buttonMode == .slideToToggle)))*/
|
||||
self.hasTrackPoint = true
|
||||
self.hasNonEditableLabel = (self.cmdString == "DISABLETOUCH"
|
||||
|| self.cmdString == "GAMEPADOVERLAY")
|
||||
|| self.cmdString == "GAMEPADOVERLAY"
|
||||
|| self.cmdString == "DISABLETILT")
|
||||
self.hasTemporaryLabel = CommandManager.velocityBasedTouchPads.contains(self.touchPadString) && (self.isMotionControlButton || self.buttonString == "NULL")
|
||||
|| self.cmdString == "RSVPAD"
|
||||
|| self.cmdString == "LSVPAD"
|
||||
@@ -1055,6 +1057,11 @@ import ObjectiveC.runtime
|
||||
self.nonEditableWidgetLabel = LocalizationHelper.localizedString(forKey: touchDisabledFLag ? "=EnableTouch" : "=DisableTouch" )
|
||||
case "GAMEPADOVERLAY":
|
||||
self.nonEditableWidgetLabel = LocalizationHelper.localizedString(forKey: OnScreenWidgetView.gamepadOverlayFLag ? "=GamepadOverlayOn" : "=GamepadOverlayOff" )
|
||||
case "DISABLETILT":
|
||||
if let pencilHandler = PencilHandler.shared {
|
||||
self.nonEditableWidgetLabel = LocalizationHelper.localizedString(forKey: pencilHandler.disableTilt ? "=enableTilt" : "=disableTilt" )
|
||||
}
|
||||
else {self.nonEditableWidgetLabel = "=disableTilt".localized}
|
||||
default:
|
||||
nonEditableWidgetLabel = ""
|
||||
}
|
||||
@@ -2945,6 +2952,10 @@ import ObjectiveC.runtime
|
||||
self.functionalWidgetDelegate?.bringUpSoftKeyboard()
|
||||
case "ABSTCHDRAG":
|
||||
self.functionalWidgetDelegate?.alterAbsTouchDragWith(mouseButton:BUTTON_LEFT)
|
||||
case "DISABLETOUCH":
|
||||
self.handleTouchDisableButtonUp()
|
||||
case "GAMEPADOVERLAY":
|
||||
self.gamepadOverlayButtonUp()
|
||||
case "PENCILHOVER":
|
||||
if !self.isPencilProEnabled() {break}
|
||||
self.functionalWidgetDelegate?.disablePencilHover()
|
||||
@@ -2961,6 +2972,12 @@ import ObjectiveC.runtime
|
||||
var eraserShortcut = self.cmdString.replacingOccurrences(of: "ERASER+", with: "")
|
||||
eraserShortcut = eraserShortcut.replacingOccurrences(of: "ERASER", with: "")
|
||||
self.functionalWidgetDelegate?.replaceEraser(shortcut: eraserShortcut)
|
||||
case "DISABLETILT":
|
||||
if !self.isPencilProEnabled() {break}
|
||||
if let pencilHandler = PencilHandler.shared {
|
||||
pencilHandler.disableTilt = !pencilHandler.disableTilt
|
||||
self.setupAtrributedText()
|
||||
}
|
||||
case "PRESSURECURVE":
|
||||
if ["com.voidlink.iOS"
|
||||
, "com.voidlinkextreme.iOS"
|
||||
@@ -2968,10 +2985,6 @@ import ObjectiveC.runtime
|
||||
].contains(Bundle.main.bundleIdentifier) && PublicUtils.isIPad {
|
||||
self.functionalWidgetDelegate?.presentPressureCurveVC()
|
||||
}
|
||||
case "DISABLETOUCH":
|
||||
self.handleTouchDisableButtonUp()
|
||||
case "GAMEPADOVERLAY":
|
||||
self.gamepadOverlayButtonUp()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -3665,7 +3678,7 @@ import ObjectiveC.runtime
|
||||
self.frame = targetFrame
|
||||
self.transform = CGAffineTransform(scaleX: 0.985, y: 0.985)
|
||||
|
||||
if ControllerUtil.activeGCControllers.count > 0, !PublicUtils.iOS26Available {
|
||||
if ControllerUtil.activeStreamingGCControllers.count > 0, !PublicUtils.iOS26Available {
|
||||
self.parentViewController?.setNeedsUpdateOfHomeIndicatorAutoHidden()
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import UIKit
|
||||
private var touchBeganForce: Float = 0
|
||||
@objc static private(set) var isDrawing: Bool = false
|
||||
@objc static private(set) var pencilPausesNativeTouch: Bool = false
|
||||
|
||||
@objc var disableTilt: Bool = false
|
||||
|
||||
// static private let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
static private var selectedProfile:OSCProfile?
|
||||
@@ -223,7 +225,7 @@ import UIKit
|
||||
|
||||
func getTilt(fromAltitudeAngle altitudeAngle: Float) -> UInt8 {
|
||||
let altitudeDegs = abs(Int16(altitudeAngle * (180.0 / .pi)))
|
||||
return UInt8(90 - min(90, Int(altitudeDegs)))
|
||||
return disableTilt ? 0 : UInt8(90 - min(90, Int(altitudeDegs)))
|
||||
}
|
||||
|
||||
var previousForce: Float = 0
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// RadialMenuOverlayView.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by Codex on 2026/7/1.
|
||||
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
final class RadialMenuOverlayView: UIView {
|
||||
private enum Metrics {
|
||||
static let diameter: CGFloat = 260
|
||||
static let bottomInset: CGFloat = 92
|
||||
}
|
||||
|
||||
private let selectionState = RadialMenuSelectionState()
|
||||
private var hostingController: UIHostingController<RadialMenuView>?
|
||||
|
||||
static var menuSectors: [RadialMenuSector] = []
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
setupView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
static func presentInKeyWindow() -> RadialMenuOverlayView? {
|
||||
guard let window = keyWindow() else { return nil }
|
||||
|
||||
let overlayView = RadialMenuOverlayView()
|
||||
overlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||
window.addSubview(overlayView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
overlayView.widthAnchor.constraint(equalToConstant: Metrics.diameter),
|
||||
overlayView.heightAnchor.constraint(equalToConstant: Metrics.diameter),
|
||||
overlayView.centerXAnchor.constraint(equalTo: window.centerXAnchor),
|
||||
overlayView.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -Metrics.bottomInset)
|
||||
])
|
||||
|
||||
return overlayView
|
||||
}
|
||||
|
||||
func updateSelection(xOffset: Float, yOffset: Float) -> RadialMenuItem? {
|
||||
let previousSelectedIndex = selectionState.selectedIndex
|
||||
selectionState.updateSelection(xOffset: xOffset, yOffset: yOffset)
|
||||
|
||||
guard previousSelectedIndex != nil, selectionState.selectedIndex == nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let previousSelectedIndex,
|
||||
Self.menuSectors.indices.contains(previousSelectedIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Self.menuSectors[previousSelectedIndex].item
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
hostingController?.willMove(toParent: nil)
|
||||
hostingController?.view.removeFromSuperview()
|
||||
hostingController?.removeFromParent()
|
||||
hostingController = nil
|
||||
removeFromSuperview()
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
clipsToBounds = false
|
||||
|
||||
let radialMenuView = RadialMenuView(
|
||||
sectors: Self.menuSectors,
|
||||
isTouchSelectionEnabled: false,
|
||||
selectionState: selectionState
|
||||
)
|
||||
|
||||
let hostingController = UIHostingController(rootView: radialMenuView)
|
||||
hostingController.view.backgroundColor = .clear
|
||||
hostingController.view.isUserInteractionEnabled = false
|
||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.hostingController = hostingController
|
||||
addSubview(hostingController.view)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
hostingController.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private static func keyWindow() -> UIWindow? {
|
||||
UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
|
||||
}
|
||||
}
|
||||
@@ -1092,6 +1092,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"=disableTilt" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🚫 Tilt"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🚫 笔刷倾斜"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🚫 筆刷傾斜"
|
||||
}
|
||||
},
|
||||
"zh-HK" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🚫 筆刷傾斜"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"=DisableTouch" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -1161,7 +1190,7 @@
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "绘画"
|
||||
}
|
||||
},
|
||||
@@ -1237,6 +1266,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"=enableTilt" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🟢 Tilt"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🟢 笔刷倾斜"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🟢 筆刷傾斜"
|
||||
}
|
||||
},
|
||||
"zh-HK" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "🟢 筆刷傾斜"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"=EnableTouch" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -1451,7 +1509,7 @@
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "功能键"
|
||||
}
|
||||
},
|
||||
@@ -9079,29 +9137,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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" : {
|
||||
@@ -9154,6 +9189,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Host View" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "主机视图"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "主機視圖"
|
||||
}
|
||||
},
|
||||
"zh-HK" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "主機視圖"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hosts" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -17525,4 +17583,4 @@
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// MenuSectionView.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 5/18/25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
// MenuSectionView.h
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "DataManager.h"
|
||||
|
||||
@protocol MenuSectionDelegate <NSObject>
|
||||
- (void)hideOverlappedDynamicLabels;
|
||||
- (SettingsMenuMode)getSettingsMenuMode;
|
||||
@end
|
||||
|
||||
|
||||
@interface MenuSectionView : UIView
|
||||
|
||||
@property (class, nonatomic, assign) BOOL overridePersistedFoldState;
|
||||
|
||||
// 外部可访问属性
|
||||
@property (nonatomic, strong) UIStackView *rootStackView;
|
||||
@property (nonatomic, assign) CGFloat leadingTrailingPadding;
|
||||
@property (nonatomic, assign) CGFloat separatorLinePadding;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, copy) NSString *sectionTitle;
|
||||
@property (nonatomic, copy) NSString *identifier;
|
||||
@property (nonatomic, assign) BOOL isExpanded;
|
||||
@property (nonatomic, assign) BOOL expandable;
|
||||
@property (nonatomic, copy) void (^lockedSectionHandler)(void);
|
||||
@property (nonatomic, strong) UIColor *backgroundColor;
|
||||
@property (nonatomic, assign) CGFloat rootStackViewSpacing;
|
||||
@property (nonatomic, assign) CGFloat headerViewHeight;
|
||||
@property (nonatomic, assign) CGFloat headerViewVerticalSpacing;
|
||||
@property (nonatomic, strong) NSMutableArray<UIStackView *> *subStackViews;
|
||||
@property (nonatomic, strong) UIImageView *iconImageView;
|
||||
@property (nonatomic, strong) UIView *separatorLine;
|
||||
@property (nonatomic, weak) id<MenuSectionDelegate> delegate; // Delegate property
|
||||
|
||||
// 方法
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size sizeConstraint:(CGFloat)constant;
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size weight:(UIImageSymbolWeight)weight sizeConstraint:(CGFloat)constant API_AVAILABLE(ios(13.0));
|
||||
- (void)addSubStackView:(UIStackView *)stackView;
|
||||
- (void)addToParentStack:(UIStackView *)parentStack;
|
||||
- (void)removeSubStackView:(UIStackView *)stackView;
|
||||
- (void)updateLayout;
|
||||
- (void)updateViewForFoldState;
|
||||
- (void)setExpanded:(BOOL)overridePersistedState;
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,425 +0,0 @@
|
||||
//
|
||||
// MenuSectionView.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 5/18/25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
// MenuSectionView.m
|
||||
|
||||
#import "MenuSectionView.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
|
||||
@interface MenuSectionView ()
|
||||
|
||||
@property (nonatomic, strong) UIButton *toggleButton;
|
||||
@property (nonatomic, strong) UIButton *toggleArea;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
|
||||
@property (nonatomic, strong) UIView *headerView;
|
||||
@property (assign) CGFloat titleOffset;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MenuSectionView
|
||||
|
||||
static BOOL overridePersistedFoldState = YES;
|
||||
|
||||
+ (BOOL)overridePersistedFoldState {
|
||||
return overridePersistedFoldState;
|
||||
}
|
||||
|
||||
+ (void)setOverridePersistedFoldState:(BOOL)val {
|
||||
overridePersistedFoldState = val;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit {
|
||||
// 默认值
|
||||
_leadingTrailingPadding = 5;
|
||||
_separatorLinePadding = 40;
|
||||
_sectionTitle = @"Section";
|
||||
_isExpanded = YES;
|
||||
_expandable = true;
|
||||
_backgroundColor = [UIColor clearColor];
|
||||
_rootStackViewSpacing = [self isIPhone] ? 10 : 12;
|
||||
_subStackViews = [NSMutableArray array];
|
||||
_headerViewHeight = 37;
|
||||
_headerViewVerticalSpacing = 25;
|
||||
_titleOffset = 41.5;
|
||||
|
||||
// 设置视图
|
||||
self.layer.cornerRadius = 10.0;
|
||||
self.layer.masksToBounds = YES;
|
||||
self.backgroundColor = _backgroundColor;
|
||||
|
||||
// 头部视图(包含图标和标题)
|
||||
_headerView = [[UIView alloc] init];
|
||||
_headerView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_headerView];
|
||||
|
||||
// 图标视图
|
||||
_iconImageView = [[UIImageView alloc] init];
|
||||
_iconImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_iconImageView.tintColor = ThemeManager.textColor;
|
||||
[_headerView addSubview:_iconImageView];
|
||||
|
||||
// 标题标签
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = _sectionTitle;
|
||||
_titleLabel.accessibilityIdentifier = @"menuSectionTitleLabel";
|
||||
_titleLabel.font = [UIFont systemFontOfSize:19.5 weight:UIFontWeightMedium];
|
||||
_titleLabel.textColor = ThemeManager.textColor;
|
||||
_titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_headerView addSubview:_titleLabel];
|
||||
|
||||
// 切换按钮
|
||||
_toggleButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_toggleArea = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:_headerViewHeight*0.31 weight:UIImageSymbolWeightBold];
|
||||
[_toggleButton setImage:[UIImage systemImageNamed:@"chevron.right" withConfiguration:config] forState:UIControlStateNormal];
|
||||
} else {
|
||||
// [_toggleButton setTitle:@"<" forState:UIControlStateNormal];
|
||||
}
|
||||
[_toggleButton addTarget:self action:@selector(toggleFold) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_toggleArea addTarget:self action:@selector(toggleFold) forControlEvents:UIControlEventTouchUpInside];
|
||||
_toggleButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_toggleArea.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_toggleButton.contentEdgeInsets = UIEdgeInsetsMake(-10,-10,-10,-10); // 上下左右各扩展 20pt
|
||||
[_headerView addSubview:_toggleButton];
|
||||
[_headerView addSubview:_toggleArea];
|
||||
|
||||
// 根stackView
|
||||
_rootStackView = [[UIStackView alloc] init];
|
||||
_rootStackView.axis = UILayoutConstraintAxisVertical;
|
||||
_rootStackView.spacing = _rootStackViewSpacing;
|
||||
_rootStackView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[self addSubview:_rootStackView];
|
||||
|
||||
// 分隔线
|
||||
_separatorLine = [[UIView alloc] init];
|
||||
_separatorLine.backgroundColor = ThemeManager.separatorColor;
|
||||
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[self addSubview:_separatorLine];
|
||||
|
||||
// 高度约束
|
||||
_heightConstraint = [self.heightAnchor constraintEqualToConstant:44];
|
||||
_heightConstraint.active = YES;
|
||||
|
||||
// 布局
|
||||
[self setupConstraints];
|
||||
[self updateViewForFoldState];
|
||||
|
||||
self.lockedSectionHandler = ^{
|
||||
NSLog(@"Null execution of lockedSectionHandler");
|
||||
};
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
// 头部视图约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_headerView.topAnchor constraintEqualToAnchor:self.topAnchor],
|
||||
[_headerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
||||
[_headerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
||||
[_headerView.heightAnchor constraintEqualToConstant:_headerViewHeight]
|
||||
]];
|
||||
|
||||
// 标题约束
|
||||
if(@available(iOS 13.0, *)){
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_titleLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_titleOffset],
|
||||
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
|
||||
]];
|
||||
}
|
||||
else [NSLayoutConstraint activateConstraints:@[
|
||||
[_titleLabel.leadingAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:0],
|
||||
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
|
||||
]];
|
||||
|
||||
// 切换按钮约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_toggleButton.trailingAnchor constraintEqualToAnchor:_headerView.trailingAnchor constant:-_leadingTrailingPadding],
|
||||
[_toggleButton.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
[_toggleButton.widthAnchor constraintEqualToConstant:_headerViewHeight*0.8],
|
||||
[_toggleButton.heightAnchor constraintEqualToConstant:_headerViewHeight*0.8]
|
||||
]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_toggleArea.leadingAnchor constraintEqualToAnchor:_headerView.leadingAnchor],
|
||||
[_toggleArea.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
[_toggleArea.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor],
|
||||
//[_toggleArea.widthAnchor constraintEqualToConstant:_headerViewHeight],
|
||||
[_toggleArea.heightAnchor constraintEqualToConstant:_headerViewHeight+25]
|
||||
]];
|
||||
|
||||
[self layoutIfNeeded];
|
||||
|
||||
// 根stackView约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_rootStackView.topAnchor constraintEqualToAnchor:self.topAnchor constant:_headerViewHeight+_headerViewVerticalSpacing],
|
||||
[_rootStackView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_leadingTrailingPadding],
|
||||
[_rootStackView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_leadingTrailingPadding],
|
||||
[_rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-1] // 为分隔线留出空间
|
||||
]];
|
||||
|
||||
// 分隔线约束
|
||||
self.clipsToBounds = false;//mark: settingMenuLayout
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
//[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
||||
//[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
||||
[_separatorLine.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
||||
[_separatorLine.widthAnchor constraintEqualToAnchor:self.widthAnchor constant:-5], // //mark: settingMenuLayout
|
||||
[_separatorLine.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
||||
[_separatorLine.heightAnchor constraintEqualToConstant:GenericUtils.menuSectionSeparatorWidth]
|
||||
]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.headerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
|
||||
[self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
|
||||
]];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size sizeConstraint:(CGFloat)constant{
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config;
|
||||
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:UIImageSymbolWeightBold];
|
||||
else{
|
||||
config = [UIImageSymbolConfiguration configurationWithPointSize:size];
|
||||
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
_iconImageView.image = [icon imageWithConfiguration:config];
|
||||
} else {
|
||||
_iconImageView.image = icon;
|
||||
}
|
||||
_iconImageView.tintColor = ThemeManager.textColor;
|
||||
_iconImageView.hidden = (icon == nil);
|
||||
|
||||
// 图标约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
|
||||
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor constant:-0],
|
||||
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight+constant],
|
||||
[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight+constant]
|
||||
]];
|
||||
|
||||
[self updateLayout];
|
||||
}
|
||||
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size weight:(UIImageSymbolWeight)weight sizeConstraint:(CGFloat)constant API_AVAILABLE(ios(13.0)){
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config;
|
||||
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:weight];
|
||||
else{
|
||||
config = [UIImageSymbolConfiguration configurationWithPointSize:size];
|
||||
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
_iconImageView.image = [icon imageWithConfiguration:config];
|
||||
} else {
|
||||
_iconImageView.image = icon;
|
||||
}
|
||||
_iconImageView.tintColor = ThemeManager.textColor;
|
||||
_iconImageView.hidden = (icon == nil);
|
||||
|
||||
// 图标约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
|
||||
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
// [_iconImageView.topAnchor constraintEqualToAnchor:_headerView.topAnchor],
|
||||
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight+constant],
|
||||
[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight+constant]
|
||||
]];
|
||||
|
||||
[self updateLayout];
|
||||
}
|
||||
|
||||
- (void)setLeadingTrailingPadding:(CGFloat)leadingTrailingPadding {
|
||||
_leadingTrailingPadding = leadingTrailingPadding;
|
||||
[self updateLayout];
|
||||
}
|
||||
|
||||
- (void)setSectionTitle:(NSString *)sectionTitle {
|
||||
_sectionTitle = sectionTitle;
|
||||
_titleLabel.text = sectionTitle;
|
||||
}
|
||||
|
||||
- (void)setExpanded:(BOOL)isExpanded {
|
||||
self.isExpanded = !_expandable ? false : isExpanded;
|
||||
[self updateViewForFoldState];
|
||||
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
_backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setRootStackViewSpacing:(CGFloat)rootStackViewSpacing {
|
||||
_rootStackViewSpacing = rootStackViewSpacing;
|
||||
_rootStackView.spacing = rootStackViewSpacing;
|
||||
[self updateLayout];
|
||||
}
|
||||
|
||||
- (void)addSubStackView:(UIStackView *)stackView {
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
[UIView performWithoutAnimation:^{
|
||||
if (![_subStackViews containsObject:stackView]) {
|
||||
[_subStackViews addObject:stackView];
|
||||
[_rootStackView addArrangedSubview:stackView];
|
||||
//[_rootStackView layoutIfNeeded];
|
||||
//[self updateLayout];
|
||||
}
|
||||
}];
|
||||
[CATransaction commit];
|
||||
// NSLog(@"rootStack height2: %f", _rootStackView.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)addToParentStack:(UIStackView *)parentStack {
|
||||
[parentStack insertArrangedSubview:self atIndex:parentStack.arrangedSubviews.count];
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.leadingAnchor constraintEqualToAnchor:parentStack.leadingAnchor constant:0],
|
||||
[self.trailingAnchor constraintEqualToAnchor:parentStack.trailingAnchor constant:0],
|
||||
]];
|
||||
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
BOOL persistedFoldState = [defaults objectForKey:self.identifier] ? [defaults boolForKey:self.identifier] : YES;
|
||||
|
||||
[self setExpanded: MenuSectionView.overridePersistedFoldState ? _expandable : persistedFoldState];
|
||||
}
|
||||
|
||||
- (void)removeSubStackView:(UIStackView *)stackView {
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
if ([_subStackViews containsObject:stackView]) {
|
||||
[_subStackViews removeObject:stackView];
|
||||
[_rootStackView removeArrangedSubview:stackView];
|
||||
[stackView removeFromSuperview];
|
||||
[self updateLayout];
|
||||
}
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
|
||||
- (void)updateLayout {
|
||||
// 更新边距约束
|
||||
for (NSLayoutConstraint *constraint in self.constraints) {
|
||||
if (constraint.firstItem == _rootStackView && constraint.firstAttribute == NSLayoutAttributeLeading) {
|
||||
constraint.constant = _leadingTrailingPadding;
|
||||
} else if (constraint.firstItem == _rootStackView && constraint.firstAttribute == NSLayoutAttributeTrailing) {
|
||||
constraint.constant = -_leadingTrailingPadding;
|
||||
}
|
||||
}
|
||||
|
||||
for (NSLayoutConstraint *constraint in _headerView.constraints) {
|
||||
if (constraint.firstItem == _iconImageView && constraint.firstAttribute == NSLayoutAttributeLeading) {
|
||||
constraint.constant = _leadingTrailingPadding;
|
||||
} else if (constraint.firstItem == _toggleButton && constraint.firstAttribute == NSLayoutAttributeTrailing) {
|
||||
constraint.constant = -_leadingTrailingPadding;
|
||||
}
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
[self layoutIfNeeded];
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (BOOL)isIPhone {
|
||||
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone;
|
||||
}
|
||||
|
||||
- (void)toggleFold {
|
||||
self.isExpanded = !_expandable ? false : !self.isExpanded;
|
||||
if(!_expandable && self.lockedSectionHandler) self.lockedSectionHandler();
|
||||
[self updateViewForFoldState];
|
||||
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (SettingsMenuMode)getSettingsMenuMode {
|
||||
if ([self.delegate respondsToSelector:@selector(getSettingsMenuMode)]) {
|
||||
return [self.delegate getSettingsMenuMode];
|
||||
} else return AllSettings;
|
||||
}
|
||||
|
||||
|
||||
- (void)updateViewForFoldState {
|
||||
// [self setupConstraints];
|
||||
// don't run this repeatedly
|
||||
|
||||
NSInteger visibleCount = 0;
|
||||
for (UIView *subview in _rootStackView.arrangedSubviews) {
|
||||
if (!subview.isHidden) {
|
||||
visibleCount++;
|
||||
}
|
||||
}
|
||||
self.hidden = visibleCount == 0;
|
||||
if (_isExpanded) {
|
||||
_rootStackView.hidden = NO;
|
||||
_separatorLine.hidden = NO;
|
||||
CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
//CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingExpandedSize];
|
||||
CGFloat rootStackViewHeight = fittingSize.height;
|
||||
fittingSize = [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
CGFloat headerHeight = fittingSize.height;
|
||||
|
||||
self.heightConstraint.constant = headerHeight + _headerViewVerticalSpacing + rootStackViewHeight + _separatorLinePadding + 1;
|
||||
|
||||
[UIView animateWithDuration:0 animations:^{
|
||||
self.toggleButton.transform = CGAffineTransformMakeRotation(M_PI/2);
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.separatorLinePadding],
|
||||
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
|
||||
]];
|
||||
[self layoutIfNeeded];
|
||||
}];
|
||||
} else {
|
||||
// 折叠状态
|
||||
CGSize fittingSize = [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
_rootStackView.hidden = YES;
|
||||
_separatorLine.hidden = NO;
|
||||
_heightConstraint.constant = fittingSize.height + _headerViewVerticalSpacing;
|
||||
// [self updateLayout];
|
||||
[UIView animateWithDuration:0 animations:^{
|
||||
self.toggleButton.transform = CGAffineTransformIdentity;
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.headerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
|
||||
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
|
||||
]];
|
||||
[self layoutIfNeeded];
|
||||
}];
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(hideOverlappedDynamicLabels)]) {
|
||||
[self.delegate hideOverlappedDynamicLabels];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,371 @@
|
||||
//
|
||||
// MenuSectionView.m -> MenuSectionView.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025.5.18
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
protocol MenuSectionDelegate: NSObjectProtocol {
|
||||
func hideOverlappedDynamicLabels()
|
||||
func getSettingsMenuMode() -> SettingsMenuMode
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
class MenuSectionView: UIView {
|
||||
static var overridePersistedFoldState = true
|
||||
|
||||
var rootStackView: UIStackView!
|
||||
var leadingTrailingPadding: CGFloat = 5 {
|
||||
didSet {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
var separatorLinePadding: CGFloat = 40
|
||||
var titleLabel: UILabel!
|
||||
var sectionTitle: String = "Section" {
|
||||
didSet {
|
||||
titleLabel?.text = sectionTitle
|
||||
}
|
||||
}
|
||||
var identifier: String?
|
||||
var isExpanded = true
|
||||
var expandable = true
|
||||
var lockedSectionHandler: (() -> Void)?
|
||||
var rootStackViewSpacing: CGFloat = UIDevice.current.userInterfaceIdiom == .phone ? 10 : 12 {
|
||||
didSet {
|
||||
rootStackView?.spacing = rootStackViewSpacing
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
var headerViewHeight: CGFloat = 37
|
||||
var headerViewVerticalSpacing: CGFloat = 25
|
||||
var subStackViews = NSMutableArray()
|
||||
var iconImageView: UIImageView!
|
||||
var separatorLine: UIView!
|
||||
weak var delegate: MenuSectionDelegate?
|
||||
|
||||
private var toggleButton: UIButton!
|
||||
private var toggleArea: UIButton!
|
||||
private var heightConstraint: NSLayoutConstraint!
|
||||
private var headerView: UIView!
|
||||
private var titleOffset: CGFloat = 41.5
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
layer.cornerRadius = 10
|
||||
layer.masksToBounds = true
|
||||
backgroundColor = .clear
|
||||
|
||||
headerView = UIView()
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(headerView)
|
||||
|
||||
iconImageView = UIImageView()
|
||||
iconImageView.contentMode = .scaleAspectFit
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconImageView.tintColor = ThemeManager.textColor
|
||||
headerView.addSubview(iconImageView)
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.text = sectionTitle
|
||||
titleLabel.accessibilityIdentifier = "menuSectionTitleLabel"
|
||||
titleLabel.font = .systemFont(ofSize: 19.5, weight: .medium)
|
||||
titleLabel.textColor = ThemeManager.textColor
|
||||
titleLabel.textAlignment = .left
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
headerView.addSubview(titleLabel)
|
||||
|
||||
toggleButton = UIButton(type: .system)
|
||||
toggleArea = UIButton(type: .system)
|
||||
if #available(iOS 13.0, *) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: headerViewHeight * 0.31, weight: .bold)
|
||||
toggleButton.setImage(UIImage(systemName: "chevron.right", withConfiguration: config), for: .normal)
|
||||
}
|
||||
toggleButton.addTarget(self, action: #selector(toggleFold), for: .touchUpInside)
|
||||
toggleArea.addTarget(self, action: #selector(toggleFold), for: .touchUpInside)
|
||||
toggleButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
toggleArea.translatesAutoresizingMaskIntoConstraints = false
|
||||
toggleButton.contentEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||
headerView.addSubview(toggleButton)
|
||||
headerView.addSubview(toggleArea)
|
||||
|
||||
rootStackView = UIStackView()
|
||||
rootStackView.axis = .vertical
|
||||
rootStackView.spacing = rootStackViewSpacing
|
||||
rootStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(rootStackView)
|
||||
|
||||
separatorLine = UIView()
|
||||
separatorLine.backgroundColor = ThemeManager.separatorColor
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(separatorLine)
|
||||
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: 44)
|
||||
heightConstraint.isActive = true
|
||||
|
||||
setupConstraints()
|
||||
updateViewForFoldState()
|
||||
|
||||
lockedSectionHandler = {
|
||||
NSLog("Null execution of lockedSectionHandler")
|
||||
}
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
headerView.heightAnchor.constraint(equalToConstant: headerViewHeight),
|
||||
])
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
NSLayoutConstraint.activate([
|
||||
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: titleOffset),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: toggleButton.leadingAnchor, constant: -8),
|
||||
])
|
||||
} else {
|
||||
NSLayoutConstraint.activate([
|
||||
titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: toggleButton.leadingAnchor, constant: -8),
|
||||
])
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
toggleButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -leadingTrailingPadding),
|
||||
toggleButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
toggleButton.widthAnchor.constraint(equalToConstant: headerViewHeight * 0.8),
|
||||
toggleButton.heightAnchor.constraint(equalToConstant: headerViewHeight * 0.8),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
toggleArea.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
|
||||
toggleArea.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
toggleArea.trailingAnchor.constraint(equalTo: toggleButton.leadingAnchor),
|
||||
toggleArea.heightAnchor.constraint(equalToConstant: headerViewHeight + 25),
|
||||
])
|
||||
|
||||
layoutIfNeeded()
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
rootStackView.topAnchor.constraint(equalTo: topAnchor, constant: headerViewHeight + headerViewVerticalSpacing),
|
||||
rootStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: leadingTrailingPadding),
|
||||
rootStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -leadingTrailingPadding),
|
||||
rootStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1),
|
||||
])
|
||||
|
||||
clipsToBounds = false
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
separatorLine.widthAnchor.constraint(equalTo: widthAnchor, constant: -5),
|
||||
separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: GenericUtils.menuSectionSeparatorWidth),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
rootStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@objc(setSectionWithIcon:size:sizeConstraint:)
|
||||
func setSection(withIcon icon: UIImage?, size: CGFloat, sizeConstraint constant: CGFloat) {
|
||||
var displayIcon = icon
|
||||
if #available(iOS 13.0, *) {
|
||||
let config: UIImage.SymbolConfiguration
|
||||
if displayIcon?.isSymbolImage == true {
|
||||
config = UIImage.SymbolConfiguration(pointSize: size, weight: .bold)
|
||||
} else {
|
||||
config = UIImage.SymbolConfiguration(pointSize: size)
|
||||
displayIcon = displayIcon?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
iconImageView.image = displayIcon?.withConfiguration(config)
|
||||
} else {
|
||||
iconImageView.image = displayIcon
|
||||
}
|
||||
iconImageView.tintColor = ThemeManager.textColor
|
||||
iconImageView.isHidden = displayIcon == nil
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
iconImageView.centerXAnchor.constraint(equalTo: headerView.leadingAnchor, constant: leadingTrailingPadding + headerViewHeight / 2 - 3),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: headerViewHeight + constant),
|
||||
iconImageView.heightAnchor.constraint(equalToConstant: headerViewHeight + constant),
|
||||
])
|
||||
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
@objc(setSectionWithIcon:size:weight:sizeConstraint:)
|
||||
func setSection(withIcon icon: UIImage?, size: CGFloat, weight: UIImage.SymbolWeight, sizeConstraint constant: CGFloat) {
|
||||
var displayIcon = icon
|
||||
let config: UIImage.SymbolConfiguration
|
||||
if displayIcon?.isSymbolImage == true {
|
||||
config = UIImage.SymbolConfiguration(pointSize: size, weight: weight)
|
||||
} else {
|
||||
config = UIImage.SymbolConfiguration(pointSize: size)
|
||||
displayIcon = displayIcon?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
iconImageView.image = displayIcon?.withConfiguration(config)
|
||||
iconImageView.tintColor = ThemeManager.textColor
|
||||
iconImageView.isHidden = displayIcon == nil
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
iconImageView.centerXAnchor.constraint(equalTo: headerView.leadingAnchor, constant: leadingTrailingPadding + headerViewHeight / 2 - 3),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: headerViewHeight + constant),
|
||||
iconImageView.heightAnchor.constraint(equalToConstant: headerViewHeight + constant),
|
||||
])
|
||||
|
||||
updateLayout()
|
||||
}
|
||||
|
||||
@objc(setExpanded:)
|
||||
func setExpanded(_ isExpanded: Bool) {
|
||||
self.isExpanded = !expandable ? false : isExpanded
|
||||
updateViewForFoldState()
|
||||
if let identifier {
|
||||
UserDefaults.standard.set(self.isExpanded, forKey: identifier)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
func addSubStackView(_ stackView: UIStackView) {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
UIView.performWithoutAnimation {
|
||||
if !subStackViews.contains(stackView) {
|
||||
subStackViews.add(stackView)
|
||||
rootStackView.addArrangedSubview(stackView)
|
||||
}
|
||||
}
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
func add(toParentStack parentStack: UIStackView) {
|
||||
parentStack.insertArrangedSubview(self, at: parentStack.arrangedSubviews.count)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
leadingAnchor.constraint(equalTo: parentStack.leadingAnchor),
|
||||
trailingAnchor.constraint(equalTo: parentStack.trailingAnchor),
|
||||
])
|
||||
|
||||
let persistedFoldState: Bool
|
||||
if let identifier, UserDefaults.standard.object(forKey: identifier) != nil {
|
||||
persistedFoldState = UserDefaults.standard.bool(forKey: identifier)
|
||||
} else {
|
||||
persistedFoldState = true
|
||||
}
|
||||
setExpanded(MenuSectionView.overridePersistedFoldState ? expandable : persistedFoldState)
|
||||
}
|
||||
|
||||
func removeSubStackView(_ stackView: UIStackView) {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
if subStackViews.contains(stackView) {
|
||||
subStackViews.remove(stackView)
|
||||
rootStackView.removeArrangedSubview(stackView)
|
||||
stackView.removeFromSuperview()
|
||||
updateLayout()
|
||||
}
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
func updateLayout() {
|
||||
for constraint in constraints {
|
||||
if constraint.firstItem === rootStackView && constraint.firstAttribute == .leading {
|
||||
constraint.constant = leadingTrailingPadding
|
||||
} else if constraint.firstItem === rootStackView && constraint.firstAttribute == .trailing {
|
||||
constraint.constant = -leadingTrailingPadding
|
||||
}
|
||||
}
|
||||
|
||||
for constraint in headerView.constraints {
|
||||
if constraint.firstItem === iconImageView && constraint.firstAttribute == .leading {
|
||||
constraint.constant = leadingTrailingPadding
|
||||
} else if constraint.firstItem === toggleButton && constraint.firstAttribute == .trailing {
|
||||
constraint.constant = -leadingTrailingPadding
|
||||
}
|
||||
}
|
||||
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
@objc func toggleFold() {
|
||||
isExpanded = !expandable ? false : !isExpanded
|
||||
if !expandable {
|
||||
lockedSectionHandler?()
|
||||
}
|
||||
updateViewForFoldState()
|
||||
if let identifier {
|
||||
UserDefaults.standard.set(isExpanded, forKey: identifier)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
func getSettingsMenuMode() -> SettingsMenuMode {
|
||||
let allSettings = SettingsMenuMode(rawValue: 0)!
|
||||
if delegate?.responds(to: #selector(MenuSectionDelegate.getSettingsMenuMode)) == true {
|
||||
return delegate?.getSettingsMenuMode() ?? allSettings
|
||||
}
|
||||
return allSettings
|
||||
}
|
||||
|
||||
func updateViewForFoldState() {
|
||||
let visibleCount = rootStackView.arrangedSubviews.filter { !$0.isHidden }.count
|
||||
isHidden = visibleCount == 0
|
||||
|
||||
if isExpanded {
|
||||
rootStackView.isHidden = false
|
||||
separatorLine.isHidden = false
|
||||
var fittingSize = rootStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
let rootStackViewHeight = fittingSize.height
|
||||
fittingSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
let headerHeight = fittingSize.height
|
||||
|
||||
heightConstraint.constant = headerHeight + headerViewVerticalSpacing + rootStackViewHeight + separatorLinePadding + 1
|
||||
|
||||
UIView.animate(withDuration: 0) {
|
||||
self.toggleButton.transform = CGAffineTransform(rotationAngle: .pi / 2)
|
||||
NSLayoutConstraint.activate([
|
||||
self.rootStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -self.separatorLinePadding),
|
||||
])
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
} else {
|
||||
let fittingSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
rootStackView.isHidden = true
|
||||
separatorLine.isHidden = false
|
||||
heightConstraint.constant = fittingSize.height + headerViewVerticalSpacing
|
||||
|
||||
UIView.animate(withDuration: 0) {
|
||||
self.toggleButton.transform = .identity
|
||||
NSLayoutConstraint.activate([
|
||||
self.headerView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
])
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
if delegate?.responds(to: #selector(MenuSectionDelegate.hideOverlappedDynamicLabels)) == true {
|
||||
delegate?.hideOverlappedDynamicLabels()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@
|
||||
"Renderer: %@\n"
|
||||
"Frames dropped by network/pacing jitter: %.1f%% / %.1f%%\n"
|
||||
"Average network latency: %@\n"
|
||||
"Decode time: %.2f/%.2f/%.2f ms",
|
||||
"Decode time: %.2f/%.2f/%.2f ms %@",
|
||||
_config.width,
|
||||
_config.height,
|
||||
fps,
|
||||
@@ -270,7 +270,7 @@
|
||||
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
|
||||
stats.frameDropMetrics.nsamples > 0 ? (stats.frameDropMetrics.total / stats.frameDropMetrics.nsamples) * 100.0 : 0.0f,
|
||||
latencyString,
|
||||
stats.decodeMetrics.min, stats.decodeMetrics.max, stats.decodeMetrics.avg];
|
||||
stats.decodeMetrics.min, stats.decodeMetrics.max, stats.decodeMetrics.avg, PublicUtils.onScreenRuntimeStats];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ static UIImage* noImage;
|
||||
_artCache = cache;
|
||||
|
||||
self.layer.cornerRadius = 16;
|
||||
if (@available(iOS 13.0, *)) self.layer.cornerCurve = kCACornerCurveContinuous;
|
||||
self.clipsToBounds = YES;
|
||||
|
||||
// Cache the NoAppImage ourselves to avoid
|
||||
|
||||
@@ -69,6 +69,41 @@ import UIKit
|
||||
else {return PublicUtils.screenWidth > PublicUtils.screenHeight}
|
||||
}
|
||||
|
||||
@objc public static func disconnectSymbol() -> String {
|
||||
if #available(iOS 18.0, tvOS 18.0, *) {
|
||||
return "personalhotspot.slash"
|
||||
}
|
||||
else if #available(iOS 14.0, tvOS 14.0, *) {
|
||||
return "rectangle.slash"
|
||||
}
|
||||
else {
|
||||
return "nosign"
|
||||
}
|
||||
}
|
||||
|
||||
@objc public static func disconnectSymbolSize() -> CGFloat {
|
||||
if #available(iOS 26.0, tvOS 26.0, *) {
|
||||
return 15
|
||||
}
|
||||
if #available(iOS 18.0, tvOS 18.0, *) {
|
||||
return 19
|
||||
}
|
||||
else if #available(iOS 14.0, tvOS 14.0, *) {return 17.5}
|
||||
else {return 20.7}
|
||||
}
|
||||
|
||||
@objc public static func disconnectSymbolWeight() -> Int {
|
||||
if #available(iOS 26.0, tvOS 26.0, *) {
|
||||
return UIImage.SymbolWeight.bold.rawValue
|
||||
}
|
||||
if #available(iOS 18.0, tvOS 18.0, *) {
|
||||
return UIImage.SymbolWeight.semibold.rawValue
|
||||
}
|
||||
else if #available(iOS 13.0, tvOS 13.0, *) {return UIImage.SymbolWeight.semibold.rawValue}
|
||||
else {return 666}
|
||||
}
|
||||
|
||||
|
||||
@objc public static func toCGFloat(_ str: String) -> CGFloat {
|
||||
return CGFloat(Double(str) ?? 0)
|
||||
}
|
||||
@@ -135,12 +170,13 @@ import UIKit
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@objc static var onScreenRuntimeStats: String = ""
|
||||
}
|
||||
|
||||
+7
-6
@@ -188,7 +188,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
|
||||
@IBOutlet private weak var toolbarTopConstraintiPhone: NSLayoutConstraint!
|
||||
@IBOutlet private weak var toolbarTopConstraintiPad: NSLayoutConstraint!
|
||||
|
||||
private var oscProfilesTableViewController: OSCProfilesTableViewController?
|
||||
var oscProfilesTableViewController: OSCProfilesTableViewController?
|
||||
private var profilesManager: OSCProfilesManager!
|
||||
private var bulkEditEnabled: Bool = false
|
||||
private var selectedWidget: OnScreenWidgetView?
|
||||
@@ -541,15 +541,16 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
|
||||
}
|
||||
|
||||
@objc var profileTableLoadingMode: OSCProfilesTableViewLoadingMode = .selectProfile
|
||||
@objc(presentProfilesTableViewWithLoadingMode:)
|
||||
func presentProfilesTableView(with loadingMode: OSCProfilesTableViewLoadingMode) {
|
||||
@objc(presentProfilesTableViewWithLoadingMode: animated:)
|
||||
func presentProfilesTableView(with loadingMode: OSCProfilesTableViewLoadingMode, animated: Bool = false) {
|
||||
profileTableLoadingMode = loadingMode
|
||||
presentProfilesTableView(with: loadingMode, pickedProfileDataHandler: nil)
|
||||
presentProfilesTableView(with: loadingMode, pickedProfileDataHandler: nil, animated:animated)
|
||||
}
|
||||
|
||||
private func presentProfilesTableView(
|
||||
with loadingMode: OSCProfilesTableViewLoadingMode,
|
||||
pickedProfileDataHandler: ((OSCProfile) -> Void)? = nil
|
||||
pickedProfileDataHandler: ((OSCProfile) -> Void)? = nil,
|
||||
animated: Bool = false
|
||||
) {
|
||||
hideStickIndicators()
|
||||
if loadingMode != .pickProfileData {selectedWidget = nil}
|
||||
@@ -570,7 +571,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
|
||||
widgetPanelStack.isHidden = true
|
||||
oscProfilesTableViewController = controller
|
||||
if let controller {
|
||||
present(controller, animated: false)
|
||||
present(controller, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// HostCollectionViewController.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/5/28.
|
||||
// Copyright 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
// #import "HostCardView.h"
|
||||
|
||||
@class HostCardView;
|
||||
@class TemporaryHost;
|
||||
|
||||
@interface HostCollectionViewController : UICollectionViewController
|
||||
|
||||
/// 横向 cell 间距
|
||||
@property (nonatomic, assign) CGFloat interItemMinimumSpacing;
|
||||
|
||||
/// 纵向行间距
|
||||
@property (nonatomic, assign) CGFloat minimumLineSpacing;
|
||||
|
||||
/// 固定的 cell 尺寸
|
||||
@property (nonatomic, assign) CGSize cellSize;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSMutableArray<TemporaryHost *> *items;
|
||||
|
||||
/// 添加一个 item
|
||||
- (void)addHost:(TemporaryHost *)host;
|
||||
- (void)removeHost:(TemporaryHost *)host;
|
||||
- (void)updateTheme;
|
||||
|
||||
/// 移除最后一个 item(如果有)
|
||||
- (void)removeLastItem;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface HostCell : UICollectionViewCell
|
||||
@property (nonatomic, strong) HostCardView *cardView;
|
||||
|
||||
- (void)configureWithHost:(TemporaryHost *)host;
|
||||
@end
|
||||
@@ -1,227 +0,0 @@
|
||||
//
|
||||
// HostCollectionViewController.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/5/28.
|
||||
// Copyright 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HostCollectionViewController.h"
|
||||
#import "HostCardView.h"
|
||||
#import "TemporaryHost.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
static const CGFloat cellOffsetY = 20;
|
||||
|
||||
@implementation HostCell {
|
||||
UIViewController* parentVC;
|
||||
}
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
[self.cardView removeFromSuperview];
|
||||
self.cardView = nil;
|
||||
}
|
||||
|
||||
- (CGFloat)getHostCardSizeFactor{
|
||||
TemporaryHost* dummyHost = [[TemporaryHost alloc] init];
|
||||
HostCardView* dummyCard = [[HostCardView alloc] initWithHost:dummyHost];
|
||||
return self.contentView.bounds.size.height/dummyCard.size.height;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewController {
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIViewController class]]) {
|
||||
return (UIViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)assignDelegateForHostCard{
|
||||
parentVC = [self viewController].parentViewController;
|
||||
if(parentVC != nil){
|
||||
if([parentVC conformsToProtocol:@protocol(HostCardActionDelegate)]) self.cardView.delegate = (id<HostCardActionDelegate>) parentVC;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self assignDelegateForHostCard];
|
||||
}
|
||||
|
||||
- (void)configureWithHost:(TemporaryHost *)host {
|
||||
if (!self.cardView) {
|
||||
self.cardView = [[HostCardView alloc] initWithHost:host andSizeFactor:[self getHostCardSizeFactor]];
|
||||
[self assignDelegateForHostCard];
|
||||
if(!self.cardView.superview){
|
||||
[self.contentView addSubview:self.cardView];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.cardView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor],
|
||||
[self.cardView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:0],
|
||||
]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface HostCollectionViewController () <UICollectionViewDelegateFlowLayout>
|
||||
@property (nonatomic, strong, readwrite) NSMutableArray<TemporaryHost *> *items;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *collectionViewHeightConstraint;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *superViewBottomConstraint;
|
||||
@end
|
||||
|
||||
@implementation HostCollectionViewController{
|
||||
UICollectionViewFlowLayout *layout;
|
||||
CGFloat _horizontalPadding;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
layout = [[UICollectionViewFlowLayout alloc] init];
|
||||
switch ([UIDevice currentDevice].userInterfaceIdiom) {
|
||||
case UIUserInterfaceIdiomPhone:
|
||||
_horizontalPadding = 5;
|
||||
break;
|
||||
case UIUserInterfaceIdiomPad:
|
||||
default:
|
||||
_horizontalPadding = 75;
|
||||
break;
|
||||
}
|
||||
layout.sectionInset = UIEdgeInsetsMake(7, _horizontalPadding, 0, _horizontalPadding); // 上、左、下、右的间距
|
||||
if (self = [super initWithCollectionViewLayout:layout]) {
|
||||
_interItemMinimumSpacing = 10;
|
||||
_minimumLineSpacing = 10;
|
||||
// _cellSize = CGSizeMake(100, 100);
|
||||
_items = [NSMutableArray array];
|
||||
|
||||
_collectionViewHeightConstraint = [self.collectionView.heightAnchor constraintEqualToConstant:50];
|
||||
_collectionViewHeightConstraint.active = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateTheme {
|
||||
self.collectionView.backgroundColor = ThemeManager.hostViewBackgroundColor;
|
||||
for (HostCell *cell in [self.collectionView visibleCells]) {
|
||||
[cell.cardView updateTheme];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self.collectionView registerClass:[HostCell class] forCellWithReuseIdentifier:@"HostCell"];
|
||||
self.collectionView.alwaysBounceVertical = NO;
|
||||
self.collectionView.showsVerticalScrollIndicator = NO;
|
||||
[self updateTheme];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Data control
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
NSLog(@"View did appear");
|
||||
}
|
||||
|
||||
- (void)didMoveToParentViewController:(UIViewController *)parent {
|
||||
[super didMoveToParentViewController:parent];
|
||||
}
|
||||
|
||||
- (void)addHost:(TemporaryHost *)host {
|
||||
if(![self.items containsObject:host]){
|
||||
[self.items addObject:host];
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeHost:(TemporaryHost *)host {
|
||||
if([self.items containsObject:host]){
|
||||
[self.items removeObject:host];
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeLastItem {
|
||||
if (self.items.count > 0) {
|
||||
[self.items removeLastObject];
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRowsInCollectionView{
|
||||
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
|
||||
|
||||
if (itemCount == 0) return 0;
|
||||
|
||||
NSMutableSet<NSNumber *> *rowYs = [NSMutableSet set];
|
||||
for (NSInteger i = 0; i < itemCount; i++) {
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
|
||||
UICollectionViewLayoutAttributes *attr = [layout layoutAttributesForItemAtIndexPath:indexPath];
|
||||
if (attr) {
|
||||
CGFloat y = CGRectGetMinY(attr.frame);
|
||||
[rowYs addObject:@(round(y))]; // round 防止浮点误差
|
||||
}
|
||||
}
|
||||
|
||||
return rowYs.count;
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
CGFloat contentHeight = self.collectionView.collectionViewLayout.collectionViewContentSize.height;
|
||||
bool contentExceedsView = contentHeight > self.view.superview.bounds.size.height - self.view.frame.origin.y;
|
||||
if(contentExceedsView){
|
||||
if(!_superViewBottomConstraint){
|
||||
_superViewBottomConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.safeAreaLayoutGuide.bottomAnchor constant:0];
|
||||
_superViewBottomConstraint.active = YES;
|
||||
}
|
||||
}
|
||||
else{
|
||||
_collectionViewHeightConstraint.constant = contentHeight;
|
||||
}
|
||||
|
||||
if([self numberOfRowsInCollectionView] == 1) layout.sectionInset = UIEdgeInsetsMake(50, _horizontalPadding, 0, _horizontalPadding);
|
||||
else if([self numberOfRowsInCollectionView] == 2) layout.sectionInset = UIEdgeInsetsMake(17, _horizontalPadding, 0, _horizontalPadding);
|
||||
else layout.sectionInset = UIEdgeInsetsMake(10, _horizontalPadding, 0, _horizontalPadding);
|
||||
//if(contentExceedsView) layout.sectionInset = UIEdgeInsetsMake(7, _horizontalPadding, 0, _horizontalPadding);
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionView DataSource
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.items.count;
|
||||
}
|
||||
|
||||
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
HostCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"HostCell" forIndexPath:indexPath];
|
||||
TemporaryHost *host = self.items[indexPath.item];
|
||||
[cell configureWithHost:host];
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDelegateFlowLayout
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.cellSize;
|
||||
}
|
||||
|
||||
- (CGFloat)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
|
||||
return self.interItemMinimumSpacing;
|
||||
}
|
||||
|
||||
- (CGFloat)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
|
||||
return self.minimumLineSpacing;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// HostCollectionViewController.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/5/28.
|
||||
// Copyright 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
class HostCell: UICollectionViewCell {
|
||||
var cardView: HostCardView?
|
||||
private weak var parentVC: UIViewController?
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
cardView?.removeFromSuperview()
|
||||
cardView = nil
|
||||
}
|
||||
|
||||
private func getHostCardSizeFactor() -> CGFloat {
|
||||
let dummyHost = TemporaryHost()
|
||||
let dummyCard = HostCardView(host: dummyHost)
|
||||
return contentView.bounds.size.height / dummyCard.size.height
|
||||
}
|
||||
|
||||
private func viewController() -> UIViewController? {
|
||||
var responder: UIResponder? = self
|
||||
while let current = responder {
|
||||
if let viewController = current as? UIViewController {
|
||||
return viewController
|
||||
}
|
||||
responder = current.next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func assignDelegateForHostCard() {
|
||||
parentVC = viewController()?.parent
|
||||
if let parentVC, parentVC.conforms(to: HostCardActionDelegate.self) {
|
||||
cardView?.delegate = parentVC as? HostCardActionDelegate
|
||||
}
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
assignDelegateForHostCard()
|
||||
}
|
||||
|
||||
@objc(configureWithHost:)
|
||||
func configure(with host: TemporaryHost) {
|
||||
if cardView == nil {
|
||||
cardView = HostCardView(host: host, andSizeFactor: getHostCardSizeFactor())
|
||||
assignDelegateForHostCard()
|
||||
if let cardView, cardView.superview == nil {
|
||||
contentView.addSubview(cardView)
|
||||
NSLayoutConstraint.activate([
|
||||
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
cardView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
class HostCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
|
||||
var interItemMinimumSpacing: CGFloat = 10
|
||||
var minimumLineSpacing: CGFloat = 10
|
||||
var cellSize: CGSize = .zero
|
||||
private(set) var items = NSMutableArray()
|
||||
|
||||
private var collectionViewHeightConstraint: NSLayoutConstraint?
|
||||
private var superViewBottomConstraint: NSLayoutConstraint?
|
||||
private let flowLayout: UICollectionViewFlowLayout
|
||||
private var horizontalPadding: CGFloat
|
||||
|
||||
@objc init() {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
flowLayout = layout
|
||||
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
horizontalPadding = 5
|
||||
case .pad:
|
||||
fallthrough
|
||||
default:
|
||||
horizontalPadding = 75
|
||||
}
|
||||
|
||||
layout.sectionInset = UIEdgeInsets(top: 7, left: horizontalPadding, bottom: 0, right: horizontalPadding)
|
||||
super.init(collectionViewLayout: layout)
|
||||
|
||||
collectionViewHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 50)
|
||||
collectionViewHeightConstraint?.isActive = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
flowLayout = UICollectionViewFlowLayout()
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
horizontalPadding = 5
|
||||
case .pad:
|
||||
fallthrough
|
||||
default:
|
||||
horizontalPadding = 75
|
||||
}
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
func updateTheme() {
|
||||
collectionView.backgroundColor = ThemeManager.hostViewBackgroundColor
|
||||
for case let cell as HostCell in collectionView.visibleCells {
|
||||
cell.cardView?.updateTheme()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
collectionView.register(HostCell.self, forCellWithReuseIdentifier: "HostCell")
|
||||
collectionView.alwaysBounceVertical = false
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
updateTheme()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
NSLog("View did appear")
|
||||
}
|
||||
|
||||
override func didMove(toParent parent: UIViewController?) {
|
||||
super.didMove(toParent: parent)
|
||||
}
|
||||
|
||||
func addHost(_ host: TemporaryHost) {
|
||||
if !items.contains(host) {
|
||||
items.add(host)
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func removeHost(_ host: TemporaryHost) {
|
||||
if items.contains(host) {
|
||||
items.remove(host)
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func removeLastItem() {
|
||||
if items.count > 0 {
|
||||
items.removeLastObject()
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private func numberOfRowsInCollectionView() -> Int {
|
||||
let itemCount = collectionView.numberOfItems(inSection: 0)
|
||||
if itemCount == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var rowYs = Set<NSNumber>()
|
||||
for index in 0..<itemCount {
|
||||
let indexPath = IndexPath(item: index, section: 0)
|
||||
if let attr = flowLayout.layoutAttributesForItem(at: indexPath) {
|
||||
let y = attr.frame.minY
|
||||
rowYs.insert(NSNumber(value: Double(round(y))))
|
||||
}
|
||||
}
|
||||
|
||||
return rowYs.count
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let contentHeight = collectionView.collectionViewLayout.collectionViewContentSize.height
|
||||
if let superview = view.superview {
|
||||
let contentExceedsView = contentHeight > superview.bounds.size.height - view.frame.origin.y
|
||||
if contentExceedsView {
|
||||
if superViewBottomConstraint == nil {
|
||||
superViewBottomConstraint = view.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor)
|
||||
superViewBottomConstraint?.isActive = true
|
||||
}
|
||||
} else {
|
||||
collectionViewHeightConstraint?.constant = contentHeight
|
||||
}
|
||||
} else {
|
||||
collectionViewHeightConstraint?.constant = contentHeight
|
||||
}
|
||||
|
||||
switch numberOfRowsInCollectionView() {
|
||||
case 1:
|
||||
flowLayout.sectionInset = UIEdgeInsets(top: 50, left: horizontalPadding, bottom: 0, right: horizontalPadding)
|
||||
case 2:
|
||||
flowLayout.sectionInset = UIEdgeInsets(top: 17, left: horizontalPadding, bottom: 0, right: horizontalPadding)
|
||||
default:
|
||||
flowLayout.sectionInset = UIEdgeInsets(top: 10, left: horizontalPadding, bottom: 0, right: horizontalPadding)
|
||||
}
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
items.count
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HostCell", for: indexPath) as! HostCell
|
||||
if let host = items[indexPath.item] as? TemporaryHost {
|
||||
cell.configure(with: host)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
cellSize
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
interItemMinimumSpacing
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
minimumLineSpacing
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,13 @@
|
||||
#import "DiscoveryManager.h"
|
||||
#import "PairManager.h"
|
||||
#import "StreamConfiguration.h"
|
||||
#import "HostCardView.h"
|
||||
#import "UIAppView.h"
|
||||
#import "AppAssetManager.h"
|
||||
#import "SWRevealViewController.h"
|
||||
#import "HostCollectionViewController.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
|
||||
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate, HostCardActionDelegate, AppViewUpdateLoopDelegate, UITextFieldDelegate>
|
||||
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate, HostCardActionDelegate, AppViewUpdateLoopDelegate, UITextFieldDelegate, ControllerNavigatorRadialMenuDelegate>
|
||||
|
||||
@property (nonatomic, strong) IBOutlet UIBarButtonItem *settingsButton;
|
||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *profilesButton;
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#import "Plot.h"
|
||||
#import "CustomEdgeSlideGestureRecognizer.h"
|
||||
#import "DataManager.h"
|
||||
#import "FixedTintImageView.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@@ -44,6 +43,11 @@
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
|
||||
@interface MainFrameViewController()
|
||||
@property (weak, nonatomic) LayoutOnScreenControlsViewController* gameProfileSelectorVC;
|
||||
@end
|
||||
|
||||
@implementation MainFrameViewController {
|
||||
UILabel* waterMark;
|
||||
UIBarButtonItem* _addHostButton;
|
||||
@@ -1184,15 +1188,15 @@ static NSMutableSet* hostList;
|
||||
countdown:6
|
||||
action:^{}
|
||||
completion:^{
|
||||
[self openGameProfileSeletor];
|
||||
[self openGameProfileSeletor:false];
|
||||
}];
|
||||
}
|
||||
else [self openGameProfileSeletor];
|
||||
else [self openGameProfileSeletor:false];
|
||||
}
|
||||
|
||||
- (void)openGameProfileSeletor{
|
||||
- (void)openGameProfileSeletor:(bool)animated {
|
||||
if(settingsViewController){
|
||||
[settingsViewController mainFrameGameProfileButtonTapped];
|
||||
[settingsViewController mainFrameGameProfileButtonTapped:animated];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1214,8 +1218,10 @@ static NSMutableSet* hostList;
|
||||
layoutToolVC.profileTableLoadingMode = OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame;
|
||||
layoutToolVC.toolbarStackView.hidden = true;
|
||||
layoutToolVC.toolbarRootView.hidden = true;
|
||||
[self presentViewController:layoutToolVC animated:NO completion:^{
|
||||
[layoutToolVC presentProfilesTableViewWithLoadingMode:OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame];
|
||||
|
||||
self.gameProfileSelectorVC = layoutToolVC;
|
||||
[self presentViewController:layoutToolVC animated:false completion:^{
|
||||
[layoutToolVC presentProfilesTableViewWithLoadingMode:OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame animated:animated];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -1463,6 +1469,7 @@ static NSMutableSet* hostList;
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
button.backgroundColor = liquidGlassEnabled ? ThemeManager.appPrimaryColor : ThemeManager.appPrimaryColor; // #0A85FF
|
||||
button.layer.cornerRadius = buttonHeight/2;
|
||||
if (@available(iOS 13.0, *)) button.layer.cornerCurve = kCACornerCurveContinuous;
|
||||
button.clipsToBounds = !liquidGlassEnabled;
|
||||
|
||||
// 设置图标(SF Symbol)
|
||||
@@ -1623,7 +1630,7 @@ static NSMutableSet* hostList;
|
||||
UIImageSymbolConfiguration *config;
|
||||
UIImage *image;
|
||||
if(PublicUtils.iOS18Available){
|
||||
config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 20.5 : 23 weight:UIImageSymbolWeightRegular ];
|
||||
config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 20.5 : 22.5 weight:PublicUtils.liquidGlassEnabled ? UIImageSymbolWeightRegular : UIImageSymbolWeightRegular];
|
||||
image = [[UIImage systemImageNamed: @"gamecontroller.circle" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
[_profilesButton setImage:image];
|
||||
_profilesButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.55) : UIEdgeInsetsMake(10, 10, 0, 0);
|
||||
@@ -1882,7 +1889,7 @@ static NSMutableSet* hostList;
|
||||
|
||||
[IAPManager.shared fetchProducts];
|
||||
[GenericUtils handleAddOnProductPurchaseIntentFor:AddOnProductPencilProPack];
|
||||
|
||||
|
||||
/*
|
||||
if (@available(iOS 15.0, *)) {
|
||||
[IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) {
|
||||
@@ -2124,6 +2131,11 @@ static NSMutableSet* hostList;
|
||||
//[self simulateSettingsButtonPress];
|
||||
[self updateResolutionAccordingly];
|
||||
if([self needPopupAboutView])[self helpButtonTapped];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[ControllerNavigator startWithSwapABXY:false];
|
||||
[ControllerNavigator setRadialMenuDelegate:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated{
|
||||
@@ -2605,4 +2617,27 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)controllerNavigatorDidSelectSettings {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[self revealViewController] revealToggleAnimated:YES];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)controllerNavigatorDidSelectGameProfiles {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if(!self.gameProfileSelectorVC && !self->settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletor:true];
|
||||
else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}];
|
||||
else if (self->settingsViewController.layoutOnScreenControlsVC) [self->settingsViewController.layoutOnScreenControlsVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)controllerNavigatorDidSelectHostView {
|
||||
[self switchToHostView];
|
||||
}
|
||||
|
||||
- (void)controllerNavigatorDidSelectExit {
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1033,11 +1033,16 @@ const int FrontViewPositionNone = 0xff;
|
||||
[_disconnectButton setTitle:nil];
|
||||
UIImage *image;
|
||||
|
||||
/*
|
||||
if(PublicUtils.liquidGlassEnabled){
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:16 weight:UIImageSymbolWeightSemibold];
|
||||
image = [[UIImage systemImageNamed:@"rectangle.portrait.and.arrow.right" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
image = [[UIImage systemImageNamed:@"personalhotspot.slash" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
else image = [[UIImage imageNamed:@"disconnect.svg" ] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
*/
|
||||
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.disconnectSymbolSize weight:PublicUtils.disconnectSymbolWeight];
|
||||
image = [[UIImage systemImageNamed:PublicUtils.disconnectSymbol withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
|
||||
[_disconnectButton setImage:image];
|
||||
if(PublicUtils.liquidGlassEnabled) _disconnectButton.imageInsets = UIEdgeInsetsMake(5, 5, 5, 5);
|
||||
@@ -1061,12 +1066,15 @@ const int FrontViewPositionNone = 0xff;
|
||||
//[moreButton setAction:@selector(moreButtonTapped:)];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[_moreButton setTitle:nil];
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 18 : 23
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:PublicUtils.liquidGlassEnabled ? 18 : 22
|
||||
weight:PublicUtils.liquidGlassEnabled ? UIImageSymbolWeightMedium : UIImageSymbolWeightMedium];
|
||||
UIImage *image = [[UIImage systemImageNamed:@"ellipsis.circle" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
[_moreButton setImage:image];
|
||||
_moreButton.imageInsets = PublicUtils.liquidGlassEnabled ? UIEdgeInsetsMake(0, 0, 0, 0.75) : UIEdgeInsetsMake(20, 0, 0, -10);
|
||||
if(PublicUtils.liquidGlassEnabled) _moreButton.tintColor = ThemeManager.appPrimaryColor;
|
||||
if(PublicUtils.liquidGlassEnabled){
|
||||
_moreButton.tintColor = ThemeManager.appPrimaryColor;
|
||||
if(@available(iOS 26.0, *)) _moreButton.sharesBackground = false;
|
||||
}
|
||||
} else {
|
||||
[_moreButton setTitle:[LocalizationHelper localizedStringForKey:@"Options"]];
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#import "AppDelegate.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "CustomEdgeSlideGestureRecognizer.h"
|
||||
#import "MenuSectionView.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
@class LayoutOnScreenControlsViewController;
|
||||
@class MicHandler;
|
||||
@@ -102,7 +102,7 @@
|
||||
@property (strong, nonatomic) IBOutlet UISlider *mousePointerVelocityFactorSlider;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *mousePointerVelocityFactorUILabel;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *unlockDisplayOrientationSelector;
|
||||
@property (strong, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC;
|
||||
@property (weak, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC;
|
||||
@property (nonatomic, strong) MainFrameViewController *mainFrameViewController;
|
||||
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
|
||||
@@ -319,6 +319,6 @@
|
||||
- (void)hideDynamicLabelsWhenOverlapped:(UIView* )view;
|
||||
- (void)setHidden:(BOOL)hidden forStack:(UIStackView* )stack;
|
||||
- (void)updateCodecDependentSwitches;
|
||||
- (void)mainFrameGameProfileButtonTapped;
|
||||
- (void)mainFrameGameProfileButtonTapped:(bool)animated;
|
||||
|
||||
@end
|
||||
|
||||
@@ -302,13 +302,13 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
}
|
||||
|
||||
- (bool)isAirPlayEnabled{
|
||||
return [self.externalDisplayModeSelector selectedSegmentIndex] == 1;
|
||||
return self.externalDisplayModeSelector.selectedSegmentIndex == 1;
|
||||
}
|
||||
|
||||
- (void)updateResolutionTable{
|
||||
if(self.mainFrameViewController.settingsExpandedInStreamView) return;
|
||||
|
||||
NSInteger externalDisplayMode = [self.externalDisplayModeSelector selectedSegmentIndex];
|
||||
NSInteger externalDisplayMode = self.externalDisplayModeSelector.selectedSegmentIndex;
|
||||
// 调用主界面方法统一填充 resolutionTable
|
||||
[self.mainFrameViewController fillResolutionTable:resolutionTable externalDisplayMode:externalDisplayMode];
|
||||
|
||||
@@ -356,21 +356,20 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
// this part will enable/disable oscSelector & the asyncNativeTouchPriority selector
|
||||
uint8_t touchModeSelectorIndex = oscProfile.touchMode == NativeTouchOnly ? NativeTouch : oscProfile.touchMode;
|
||||
[self.touchModeSelector1 setSelectedSegmentIndex:touchModeSelectorIndex]; //Load old touchMode setting
|
||||
self.touchModeSelector1.selectedSegmentIndex = touchModeSelectorIndex; //Load old touchMode setting
|
||||
[self touchModeChanged:self.touchModeSelector1];
|
||||
|
||||
[self.touchModeSelector2 addTarget:self action:@selector(touchMode2Changed:) forControlEvents:UIControlEventValueChanged];
|
||||
self.touchModeSelector2.selectedSegmentIndex = self.touchModeSelector1.selectedSegmentIndex;
|
||||
|
||||
[self.enableOswForNativeTouchSwitch setOn:oscProfile.touchMode != NativeTouchOnly];
|
||||
[self enableOswForNativeTouchSwitchFlipped:self.enableOswForNativeTouchSwitch];
|
||||
|
||||
[self.enableOswForNativeTouchSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.pointerVelocityModeDividerSlider setValue: (uint8_t)(oscProfile.pointerVelocityModeDivider * 100) animated:NO]; // Load old setting.
|
||||
[self pointerVelocityModeDividerSliderMoved:self.pointerVelocityModeDividerSlider];
|
||||
[self.pointerVelocityModeDividerSlider sendActionsForControlEvents:UIControlEventValueChanged]; // Load old setting.
|
||||
|
||||
[self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: oscProfile.touchPointerVelocityFactor] animated:NO]; // Load old setting.
|
||||
[self touchPointerVelocityFactorSliderMoved:self.touchPointerVelocityFactorSlider];
|
||||
[self.touchPointerVelocityFactorSlider sendActionsForControlEvents:UIControlEventValueChanged]; // Load old setting.
|
||||
|
||||
self.controllerGyroSwitchButtonSetter.selectedSegmentIndex = oscProfile.controllerGyroSwitchMode;
|
||||
if(self.controllerGyroSwitchButtonSetter.selectedSegmentIndex != ControllerGyroSwitchDisabled){
|
||||
@@ -381,36 +380,40 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
[self.reverseHoldButtonSwitch setOn:oscProfile.reverseGyroHoldButton];
|
||||
|
||||
[self.gyroSourceSelector setSelectedSegmentIndex:oscProfile.useBuiltinGyro ? 0 : 1];
|
||||
[self gyroSourceChanged:self.gyroSourceSelector];
|
||||
self.gyroSourceSelector.selectedSegmentIndex = oscProfile.useBuiltinGyro ? 0 : 1;
|
||||
[self.gyroSourceSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.swapYawAndRollSwitch setOn:oscProfile.swapYawAndRoll];
|
||||
|
||||
[self.mapGyroToSelector setSelectedSegmentIndex:oscProfile.mapGyroTo];
|
||||
[self mapGyroToChanged:self.mapGyroToSelector];
|
||||
|
||||
self.mapGyroToSelector.selectedSegmentIndex = oscProfile.mapGyroTo;
|
||||
[self.mapGyroToSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.yawPitchToRightStickSwitch setOn:oscProfile.yawPitchToRightStick];
|
||||
[self.rollToLeftStickSwitch setOn:oscProfile.rollToLeftStick];
|
||||
|
||||
if(self.mapGyroToSelector.selectedSegmentIndex == mapGyroToControllerStick){
|
||||
[self yawPitchToRightStickSwitchFlipped:self.yawPitchToRightStickSwitch];
|
||||
[self rollToLeftStickSwitchFlipped:self.rollToLeftStickSwitch];
|
||||
[self.yawPitchToRightStickSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
[self.rollToLeftStickSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
[self.yawSensitivitySlider setValue:[self map_SliderValue_fromVelocFactor:oscProfile.gyroSensitivityYaw]];
|
||||
[self yawSensitivitySliderMoved:self.yawSensitivitySlider];
|
||||
[self.yawSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.pitchSensitivitySlider setValue:[self map_SliderValue_fromVelocFactor:oscProfile.gyroSensitivityPitch]];
|
||||
[self pitchSensitivitySliderMoved:self.pitchSensitivitySlider];
|
||||
[self.pitchSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.rollSensitivitySlider setValue:[self map_SliderValue_fromVelocFactor:oscProfile.gyroSensitivityRoll]];
|
||||
[self rollSensitivitySliderMoved:self.rollSensitivitySlider];
|
||||
[self.rollSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.gyroToStickMinOffsetSlider setValue:(uint16_t)oscProfile.gyroToStickMinOffset];
|
||||
[self gyroMinStickOffsetSliderMoved:self.gyroToStickMinOffsetSlider];
|
||||
|
||||
[self.gyroToStickMinOffsetSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.leftStickMinOffsetSlider setValue:(uint16_t)oscProfile.physicalLeftStickMinOffset];
|
||||
[self leftStickMinOffsetSliderMoved:self.leftStickMinOffsetSlider];
|
||||
[self.leftStickMinOffsetSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.rightStickMinOffsetSlider setValue:(uint16_t)oscProfile.physicalRightStickMinOffset];
|
||||
[self rightStickMinOffsetSliderMoved:self.rightStickMinOffsetSlider];
|
||||
|
||||
[self.rightStickMinOffsetSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.synthPhysicalInputSwitch setOn:oscProfile.synthesizePhysicalStick];
|
||||
|
||||
[self.pressureCurveSwitch setOn:oscProfile.pressureCurveEnabled];
|
||||
@@ -579,7 +582,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
action:^{}
|
||||
completion:^{
|
||||
if(!AlertControllerUtil.actionCancelled){
|
||||
[weakSelf.onScreenWidgetSelector setSelectedSegmentIndex:OnScreenControlsLevelCustom];
|
||||
weakSelf.onScreenWidgetSelector.selectedSegmentIndex = OnScreenControlsLevelCustom;
|
||||
if(weakSelf.touchModeSelector1.selectedSegmentIndex == NativeTouch){
|
||||
[weakSelf.enableOswForNativeTouchSwitch setOn:true];
|
||||
[weakSelf enableOswForNativeTouchSwitchFlipped:weakSelf.enableOswForNativeTouchSwitch];
|
||||
@@ -2130,20 +2133,20 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
switch (self->tempSettings.preferredCodec) {
|
||||
case CODEC_PREF_AUTO:
|
||||
[self.codecSelector setSelectedSegmentIndex:VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) ? CODEC_PREF_HEVC-1 : CODEC_PREF_H264-1];
|
||||
[self codecSelectorChanged:self.codecSelector];
|
||||
self.codecSelector.selectedSegmentIndex = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) ? CODEC_PREF_HEVC-1 : CODEC_PREF_H264-1;
|
||||
[self.codecSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
break;
|
||||
|
||||
case CODEC_PREF_AV1:
|
||||
[self.codecSelector setSelectedSegmentIndex:2];
|
||||
self.codecSelector.selectedSegmentIndex = 2;
|
||||
break;
|
||||
|
||||
case CODEC_PREF_HEVC:
|
||||
[self.codecSelector setSelectedSegmentIndex:1];
|
||||
self.codecSelector.selectedSegmentIndex = 1;
|
||||
break;
|
||||
|
||||
case CODEC_PREF_H264:
|
||||
[self.codecSelector setSelectedSegmentIndex:0];
|
||||
self.codecSelector.selectedSegmentIndex = 0;
|
||||
break;
|
||||
}
|
||||
}];
|
||||
@@ -2161,7 +2164,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self.yuv444Switch setOn:self->tempSettings.enableYUV444];
|
||||
[self.fullColorRangeSwitch setOn:self->tempSettings.fullColorRange];
|
||||
[self.codecSelector addTarget:self action:@selector(codecSelectorChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self codecSelectorChanged:self.codecSelector];
|
||||
[self.codecSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.pipSwitch setOn:self->tempSettings.enablePIP];
|
||||
if(@available(iOS 15.0, *)) {
|
||||
@@ -2171,18 +2174,18 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self.pipSwitch setEnabled:false];
|
||||
}
|
||||
|
||||
[self.statsOverlaySelector setSelectedSegmentIndex:self->tempSettings.statsOverlayLevel.intValue];
|
||||
self.statsOverlaySelector.selectedSegmentIndex = self->tempSettings.statsOverlayLevel.intValue;
|
||||
|
||||
NSInteger renderingBackend = [self->tempSettings.renderingBackend integerValue];
|
||||
[self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend];
|
||||
self.renderingBackendSelector.selectedSegmentIndex = renderingBackend;
|
||||
[self.renderingBackendSelector addTarget:self action:@selector(renderingBackendChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP and frame pacing state based on current selection
|
||||
[self.renderingBackendSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
NSInteger framePacingMode = [self->tempSettings.framePacingMode integerValue];
|
||||
[self.framePacingModeSelector setSelectedSegmentIndex:framePacingMode];
|
||||
self.framePacingModeSelector.selectedSegmentIndex = framePacingMode;
|
||||
[self.framePacingModeSelector addTarget:self action:@selector(framePacingModeChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self framePacingModeChanged:self.framePacingModeSelector];
|
||||
|
||||
[self.framePacingModeSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
// [self.frameTimebaseSwitch setOn:self->tempSettings.enableFrameTimebase];
|
||||
[self.asyncFrameDequeueSwitch setOn:self->tempSettings.asyncFrameDequeue];
|
||||
[self.sdrPerformanceWorkaroundSwitch setOn:self->tempSettings.sdrPerformanceWorkaround];
|
||||
@@ -2201,7 +2204,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
[self.delayLeftClickSwitch setOn:self->tempSettings.delayLeftClick];
|
||||
|
||||
[self.hapticEngineSelector setSelectedSegmentIndex:self->tempSettings.hapticEngine.intValue];
|
||||
self.hapticEngineSelector.selectedSegmentIndex = self->tempSettings.hapticEngine.intValue;
|
||||
bool hideHapticEngineStack = false;
|
||||
if(@available(iOS 13.0, tvOS 13.0, *)) hideHapticEngineStack = false;
|
||||
else hideHapticEngineStack = true;
|
||||
@@ -2213,11 +2216,11 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self.hapticEngineSelector setEnabled:!disableControllerRumble forSegmentAtIndex:HapticEngineAuto];
|
||||
[self.hapticEngineSelector setEnabled:[self isIPhone] forSegmentAtIndex:RumbleDevice];
|
||||
|
||||
[self.gyroModeSelector setSelectedSegmentIndex:self->tempSettings.gyroMode.intValue];
|
||||
self.gyroModeSelector.selectedSegmentIndex = self->tempSettings.gyroMode.intValue;
|
||||
[self.gyroSensitivitySlider setValue: (uint16_t)(self->tempSettings.gyroSensitivity.floatValue * 100) animated:NO]; // Load old setting.
|
||||
[self.gyroSensitivitySlider addTarget:self action:@selector(gyroSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self gyroSensitivitySliderMoved:self.gyroSensitivitySlider];
|
||||
|
||||
[self.gyroSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged]; // Load old setting.
|
||||
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) nil;
|
||||
else{
|
||||
[self.gyroModeSelector setEnabled:false forSegmentAtIndex:1];
|
||||
@@ -2226,10 +2229,10 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
|
||||
[self.gyroModeSelector setEnabled:[motionManager isGyroAvailable] forSegmentAtIndex:2];
|
||||
|
||||
[self.emulatedControllerTypeSelector setSelectedSegmentIndex:[self controllerTypeToSegmentIndex:self->tempSettings.emulatedControllerType.intValue]];
|
||||
self.emulatedControllerTypeSelector.selectedSegmentIndex = [self controllerTypeToSegmentIndex:self->tempSettings.emulatedControllerType.intValue];
|
||||
[self.emulatedControllerTypeSelector addTarget:self action:@selector(emulatedControllerTypeChanged:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self emulatedControllerTypeChanged:self.emulatedControllerTypeSelector];
|
||||
|
||||
[self.emulatedControllerTypeSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.leftStickMinOffsetSlider addTarget:self action:@selector(leftStickMinOffsetSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.rightStickMinOffsetSlider addTarget:self action:@selector(rightStickMinOffsetSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
@@ -2237,8 +2240,8 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
[self.localVolumeSlider setValue:self->tempSettings.localVolume.floatValue*100];
|
||||
[self.localVolumeSlider addTarget:self action:@selector(localVolumeSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self localVolumeSliderMoved:self.localVolumeSlider];
|
||||
|
||||
[self.localVolumeSlider sendActionsForControlEvents:UIControlEventValueChanged]; // Load old setting.
|
||||
|
||||
[self.duckOtherAppSwitch setOn:self->tempSettings.duckOtherApps];
|
||||
|
||||
[self.muteInBackgroundSwitch setOn:self->tempSettings.muteInBackground];
|
||||
@@ -2246,43 +2249,43 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
[self.micVolumeSlider setValue:self->tempSettings.micVolume.floatValue*100];
|
||||
[self.micVolumeSlider addTarget:self action:@selector(micVolumeSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self micVolumeSliderMoved:self.self.micVolumeSlider];
|
||||
|
||||
[self.micVolumeSlider sendActionsForControlEvents:UIControlEventValueChanged]; // Load old setting.
|
||||
|
||||
[self.redirectMicSwitch setOn:self->tempSettings.redirectMic];
|
||||
[self.redirectMicSwitch addTarget:self action:@selector(redirectMicSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
|
||||
[self redirectMicSwitchFlipped:self.redirectMicSwitch];
|
||||
|
||||
[self.redirectMicSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.useBuiltinMicSwitch setOn:self->tempSettings.useBuiltinMic];
|
||||
|
||||
self->_lastSelectedResolutionIndex = resolution;
|
||||
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
||||
self.resolutionSelector.selectedSegmentIndex = resolution;
|
||||
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||||
self.framerateSelector.selectedSegmentIndex = framerate;
|
||||
[self.framerateSelector addTarget:self action:@selector(framerateChanged) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.bitrateSlider setMinimumValue:0];
|
||||
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:self->_bitrate] animated:NO];
|
||||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||
[self updateBitrateText];
|
||||
[self updateResolutionDisplayLabel];
|
||||
[self.bitrateSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.frameQueueSizeSlider setMinimumValue:0];
|
||||
[self.frameQueueSizeSlider setMaximumValue:5];
|
||||
[self.frameQueueSizeSlider setValue:self->tempSettings.frameQueueSize.intValue];
|
||||
[self.frameQueueSizeSlider addTarget:self action:@selector(frameQueueSizeSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self frameQueueSizeSliderMoved:self.frameQueueSizeSlider];
|
||||
[self.frameQueueSizeSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.enableGraphsSwitch setOn:self->tempSettings.enableGraphs animated:NO]; // Add this line
|
||||
[self.enableGraphsSwitch addTarget:self action:@selector(enableGraphsChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self enableGraphsChanged:self.enableGraphsSwitch];
|
||||
[self.enableGraphsSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.graphOpacityStepper setMinimumValue:0];
|
||||
[self.graphOpacityStepper setMaximumValue:100];
|
||||
[self.graphOpacityStepper setValue:(int)self->tempSettings.graphOpacity.intValue];
|
||||
[self.graphOpacityStepper addTarget:self action:@selector(graphOpacityStepperTapped:) forControlEvents:UIControlEventValueChanged];
|
||||
[self graphOpacityStepperTapped:self.graphOpacityStepper];
|
||||
|
||||
[self.graphOpacityStepper sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
self.audioEngineSelector.selectedSegmentIndex = self->tempSettings.audioEngine.intValue;
|
||||
|
||||
if (@available(iOS 18.0, tvOS 18.0, *)) {}else{
|
||||
@@ -2294,16 +2297,16 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
}
|
||||
switch ([self->tempSettings.audioConfig integerValue]) {
|
||||
case 2:
|
||||
[self.audioConfigSelector setSelectedSegmentIndex:0];
|
||||
self.audioConfigSelector.selectedSegmentIndex = 0;
|
||||
break;
|
||||
case 3:
|
||||
[self.audioConfigSelector setSelectedSegmentIndex:1];
|
||||
self.audioConfigSelector.selectedSegmentIndex = 1;
|
||||
break;
|
||||
case 6:
|
||||
[self.audioConfigSelector setSelectedSegmentIndex:2];
|
||||
self.audioConfigSelector.selectedSegmentIndex = 2;
|
||||
break;
|
||||
case 8:
|
||||
[self.audioConfigSelector setSelectedSegmentIndex:3];
|
||||
self.audioConfigSelector.selectedSegmentIndex = 3;
|
||||
break;
|
||||
}
|
||||
// 2 - stereo (system)
|
||||
@@ -2313,24 +2316,24 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
// Unlock Display Orientation setting
|
||||
bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired] || [self isIPhone];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation working
|
||||
if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:self->tempSettings.unlockDisplayOrientation ? 1 : 0];
|
||||
else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked
|
||||
if(unlockDisplayOrientationSelectorEnabled) self.unlockDisplayOrientationSelector.selectedSegmentIndex = self->tempSettings.unlockDisplayOrientation ? 1 : 0;
|
||||
else self.unlockDisplayOrientationSelector.selectedSegmentIndex = 1; // can't lock screen orientation in this mode = Display Orientation always unlocked
|
||||
[self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled];
|
||||
|
||||
[self.backgroundSessionTimerSlider setValue:(uint32_t)self->tempSettings.backgroundSessionTimer.floatValue];
|
||||
[self.backgroundSessionTimerSlider addTarget:self action:@selector(backgroundSessionTimerSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self backgroundSessionTimerSliderMoved:self.backgroundSessionTimerSlider];
|
||||
|
||||
[self.appThemeSelector setSelectedSegmentIndex:self->tempSettings.appTheme.intValue];
|
||||
[self.backgroundSessionTimerSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
self.appThemeSelector.selectedSegmentIndex = self->tempSettings.appTheme.intValue;
|
||||
[self.appThemeSelector addTarget:self action:@selector(appThemeChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
if (@available(iOS 13.0, *)) nil;
|
||||
else{
|
||||
[self.appThemeSelector setSelectedSegmentIndex:UIUserInterfaceStyleDark];
|
||||
self.appThemeSelector.selectedSegmentIndex = UIUserInterfaceStyleDark;
|
||||
[self.appThemeSelector setEnabled:false];
|
||||
}
|
||||
|
||||
// lift streamview setting
|
||||
[self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:self->tempSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting
|
||||
self.liftStreamViewForKeyboardSelector.selectedSegmentIndex = self->tempSettings.liftStreamViewForKeyboard ? 1 : 0;// Load old setting
|
||||
|
||||
// showkeyboard toolbar setting
|
||||
[self.softKeyboardToolbarSwitch setOn:self->tempSettings.showKeyboardToolbar];// Load old setting
|
||||
@@ -2341,22 +2344,22 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
|
||||
// reverse mouse wheel direction setting
|
||||
[self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:self->tempSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting
|
||||
self.reverseMouseWheelDirectionSelector.selectedSegmentIndex = self->tempSettings.reverseMouseWheelDirection ? 1 : 0;// Load old setting
|
||||
|
||||
// slide to menu settings
|
||||
[self.slideToSettingsScreenEdgeSelector setSelectedSegmentIndex:[self getSelectorIndexFromScreenEdge:(uint32_t)self->tempSettings.slideToSettingsScreenEdge.integerValue]];
|
||||
self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex = [self getSelectorIndexFromScreenEdge:(uint32_t)self->tempSettings.slideToSettingsScreenEdge.integerValue];
|
||||
// Load old setting
|
||||
[self.slideToToolboxScreenEdgeSelector setEnabled:false];
|
||||
[self.slideToSettingsScreenEdgeSelector addTarget:self action:@selector(slideToSettingsScreenEdgeChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self slideToSettingsScreenEdgeChanged];
|
||||
[self.slideToSettingsScreenEdgeSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.slideToMenuDistanceSlider setValue:self->tempSettings.slideToSettingsDistance.floatValue];
|
||||
[self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self slideToMenuDistanceSliderMoved:self.slideToMenuDistanceSlider];
|
||||
[self.slideToMenuDistanceSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.edgeSlidingSensitivitySlider setValue:self->tempSettings.edgeSlidingSensitivity.floatValue];
|
||||
[self.edgeSlidingSensitivitySlider addTarget:self action:@selector(edgeSlidingSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self edgeSlidingSensitivitySliderMoved:self.edgeSlidingSensitivitySlider];
|
||||
[self.edgeSlidingSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
//TouchMode & OSC Related Settings:
|
||||
|
||||
@@ -2367,14 +2370,14 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
|
||||
// async native touch event
|
||||
// [self.asyncNativeTouchPrioritySelector setSelectedSegmentIndex:currentSettings.asyncNativeTouchPriority.intValue]; // load old setting of asyncNativeTouchPriority
|
||||
// self.asyncNativeTouchPrioritySelector.selectedSegmentIndex = currentSettings.asyncNativeTouchPriority.intValue; // load old setting of asyncNativeTouchPriority
|
||||
// [self.asyncNativeTouchPrioritySelector addTarget:self action:@selector(asyncNativeTouchPriorityChanged) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
// init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode
|
||||
[self.mousePointerVelocityFactorSlider setValue:[self map_SliderValue_fromVelocFactor: self->tempSettings.mousePointerVelocityFactor.floatValue] animated:NO]; // Load old setting.
|
||||
[self.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self mousePointerVelocityFactorSliderMoved:self.mousePointerVelocityFactorSlider];
|
||||
|
||||
[self.mousePointerVelocityFactorSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.passthroughGesturesSwitch setOn:self->tempSettings.passthroughGestures];
|
||||
[self.passthroughGesturesSwitch addTarget:self action:@selector(passthroughGesturesSwitchFlipped:) forControlEvents:(UIControlEventValueChanged)];
|
||||
|
||||
@@ -2385,34 +2388,34 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
[self.scrollSensitivitySlider setValue:self->tempSettings.scrollSensitivity.floatValue animated:NO];
|
||||
[self.scrollSensitivitySlider addTarget:self action:@selector(scrollSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)];
|
||||
[self scrollSensitivitySliderMoved:self.scrollSensitivitySlider];
|
||||
|
||||
[self.scrollSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.pinchSensitivitySlider setValue:self->tempSettings.pinchSensitivity.floatValue animated:NO];
|
||||
[self.pinchSensitivitySlider addTarget:self action:@selector(pinchSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)];
|
||||
[self pinchSensitivitySliderMoved:self.pinchSensitivitySlider];
|
||||
[self.pinchSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.singleTapSensitivitySlider setValue:self->tempSettings.singleTapSensitivity.doubleValue animated:NO];
|
||||
[self.singleTapSensitivitySlider addTarget:self action:@selector(singleTapSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)];
|
||||
[self singleTapSensitivitySliderMoved:self.singleTapSensitivitySlider];
|
||||
|
||||
[self.singleTapSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.relativeTouchSlideThresholdSlider setValue:self->tempSettings.relativeTouchSlideThreshold.floatValue animated:NO];
|
||||
[self.relativeTouchSlideThresholdSlider addTarget:self action:@selector(relativeTouchSlideThresholdSliderMoved:) forControlEvents:(UIControlEventValueChanged)];
|
||||
[self relativeTouchSlideThresholdSliderMoved:self.relativeTouchSlideThresholdSlider];
|
||||
[self.relativeTouchSlideThresholdSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
// these settings will be affected by onscreenControl & touchMode, must be loaded before them.
|
||||
// NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue);
|
||||
self->oswLayoutFingers = (uint16_t)self->tempSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers
|
||||
uint8_t keyboardToggleFingers = self->tempSettings.keyboardToggleFingers.intValue;
|
||||
|
||||
[self.softKeyboardGestureSelector setSelectedSegmentIndex:keyboardToggleFingers>=6 ? 3 : keyboardToggleFingers-3];
|
||||
self.softKeyboardGestureSelector.selectedSegmentIndex = keyboardToggleFingers>=6 ? 3 : keyboardToggleFingers-3;
|
||||
|
||||
|
||||
|
||||
// this setting will be affected by touchMode, must be loaded before them.
|
||||
NSInteger onscreenControlsLevel = [self->tempSettings.onscreenControls integerValue];
|
||||
[self.onScreenWidgetSelector setSelectedSegmentIndex:MIN(onscreenControlsLevel,OnScreenControlsLevelCustom)];
|
||||
self.onScreenWidgetSelector.selectedSegmentIndex = MIN(onscreenControlsLevel,OnScreenControlsLevelCustom);
|
||||
[self.onScreenWidgetSelector addTarget:self action:@selector(onScreenWidgetChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self onScreenWidgetChanged];
|
||||
[self.onScreenWidgetSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
/*
|
||||
[self.touchMoveEventIntervalSlider setValue:self->tempSettings.touchMoveEventInterval.intValue animated:NO]; // Load old setting.
|
||||
@@ -2429,24 +2432,24 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self.touchModeSelector1 addTarget:self action:@selector(touchMode1Changed:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.enableOswForNativeTouchSwitch addTarget:self action:@selector(enableOswForNativeTouchSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.externalDisplayModeSelector setSelectedSegmentIndex:self->tempSettings.externalDisplayMode.integerValue];
|
||||
[self.localMousePointerModeSelector setSelectedSegmentIndex:self->tempSettings.localMousePointerMode.integerValue];
|
||||
self.externalDisplayModeSelector.selectedSegmentIndex = self->tempSettings.externalDisplayMode.integerValue;
|
||||
self.localMousePointerModeSelector.selectedSegmentIndex = self->tempSettings.localMousePointerMode.integerValue;
|
||||
|
||||
[self.sendDummyEventSwitch setOn:self->tempSettings.sendDummyEvent];// Load old setting
|
||||
|
||||
[self.controllerToMouseSwitch setOn:self->tempSettings.mapControllerToMouse];
|
||||
[self.controllerToMouseSwitch addTarget:self action:@selector(controllerToMouseSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
|
||||
[self controllerToMouseSwitchFlipped:self.controllerToMouseSwitch];
|
||||
|
||||
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.controllerGyroSwitchButtonSetter addTarget:self action:@selector(controllerGyroSwitchModeChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.controllerMouseVelocitySlider setValue:self->tempSettings.controllerMousePointerVelocity.floatValue];
|
||||
[self.controllerMouseVelocitySlider addTarget:self action:@selector(controllerMouseVelocitySliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self controllerMouseVelocitySliderMoved:self.controllerMouseVelocitySlider];
|
||||
|
||||
[self.controllerMouseVelocitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.controllerMouseExpoSlider setValue:self->tempSettings.controllerMouseExpo.floatValue];
|
||||
[self.controllerMouseExpoSlider addTarget:self action:@selector(controllerMouseExpoSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self controllerMouseExpoSliderMoved:self.controllerMouseExpoSlider];
|
||||
[self.controllerMouseExpoSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.gyroSourceSelector addTarget:self action:@selector(gyroSourceChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.mapGyroToSelector addTarget:self action:@selector(mapGyroToChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
@@ -2471,8 +2474,8 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
}
|
||||
|
||||
- (void)slideToSettingsScreenEdgeChanged{
|
||||
if([self.slideToSettingsScreenEdgeSelector selectedSegmentIndex] == 0) [self.slideToToolboxScreenEdgeSelector setSelectedSegmentIndex:1];
|
||||
else [self.slideToToolboxScreenEdgeSelector setSelectedSegmentIndex:0];
|
||||
if(self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex == 0) self.slideToToolboxScreenEdgeSelector.selectedSegmentIndex = 1;
|
||||
else self.slideToToolboxScreenEdgeSelector.selectedSegmentIndex = 0;
|
||||
}
|
||||
|
||||
- (void)showCustomOswTip {
|
||||
@@ -2549,7 +2552,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
- (void)handleOswGestureChange{
|
||||
if(settingsViewJustLoaded) return;
|
||||
if([self isCustomOswEnabled] && oswLayoutFingers == self.softKeyboardGestureSelector.selectedSegmentIndex + 3 && oswLayoutFingers < 6){
|
||||
[_softKeyboardGestureSelector setSelectedSegmentIndex:_softKeyboardGestureSelector.selectedSegmentIndex-1];
|
||||
_softKeyboardGestureSelector.selectedSegmentIndex = _softKeyboardGestureSelector.selectedSegmentIndex-1;
|
||||
}
|
||||
for (NSInteger i = 0; i < _softKeyboardGestureSelector.numberOfSegments; i++) {
|
||||
[_softKeyboardGestureSelector setEnabled:![self isCustomOswEnabled] || i == 3 ? true : i+3 != oswLayoutFingers forSegmentAtIndex:i]; // 或 NO 来禁用
|
||||
@@ -2563,7 +2566,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self.pipSwitch setOn:NO animated:YES];
|
||||
[self.pipSwitch setEnabled:NO];
|
||||
// Set pacing method to Queue and disable selector
|
||||
[self.framePacingModeSelector setSelectedSegmentIndex:FramePacingModeQueue];
|
||||
self.framePacingModeSelector.selectedSegmentIndex = FramePacingModeQueue;
|
||||
[self.framePacingModeSelector setEnabled:NO];
|
||||
} else {
|
||||
// Balanced mode (AVSB renderer) - enable PiP toggle if iOS 15+
|
||||
@@ -2604,7 +2607,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
self.renderingBackendSelector.selectedSegmentIndex = 0;
|
||||
[self renderingBackendChanged:self.renderingBackendSelector];
|
||||
[self.renderingBackendSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
NSURL *url = [NSURL URLWithString:[LocalizationHelper localizedStringForKey:@"betterPerformanceLink"]];
|
||||
if ([[UIApplication sharedApplication] canOpenURL:url]) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
@@ -2688,8 +2691,8 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self setHidden:!mapGyroToControllerStickEnabled forStack:self.synthPhysicalInputStack];
|
||||
|
||||
if(mapGyroToControllerStickEnabled){
|
||||
[self yawPitchToRightStickSwitchFlipped:self.yawPitchToRightStickSwitch];
|
||||
[self rollToLeftStickSwitchFlipped:self.rollToLeftStickSwitch];
|
||||
[self.yawPitchToRightStickSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
[self.rollToLeftStickSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
if(sender.selectedSegmentIndex == driftCorrection){
|
||||
@@ -2703,7 +2706,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
action:^{}
|
||||
completion:^{
|
||||
if(AlertControllerUtil.actionCancelled){
|
||||
[self.mapGyroToSelector setSelectedSegmentIndex:self->oscProfile.mapGyroTo];
|
||||
self.mapGyroToSelector.selectedSegmentIndex = self->oscProfile.mapGyroTo;
|
||||
[self mapGyroToChanged:self.mapGyroToSelector];
|
||||
}
|
||||
else{
|
||||
@@ -2730,8 +2733,8 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
countdown:6
|
||||
action:^{}
|
||||
completion:^{
|
||||
[self.mapGyroToSelector setSelectedSegmentIndex:self->oscProfile.mapGyroTo];
|
||||
[self mapGyroToChanged:self.mapGyroToSelector];
|
||||
self.mapGyroToSelector.selectedSegmentIndex = self->oscProfile.mapGyroTo;
|
||||
[self.mapGyroToSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}];
|
||||
}
|
||||
}];
|
||||
@@ -2797,7 +2800,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
return;
|
||||
}
|
||||
[AlertControllerUtil.alertController dismissViewControllerAnimated:NO completion:^{}];
|
||||
[self controllerGyroSwitchModeChanged:sender];
|
||||
[self.controllerGyroSwitchButtonSetter sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
@@ -2840,6 +2843,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
__block bool stickCaptured = false;
|
||||
__block bool leftButtonCaptured = false;
|
||||
__block bool rightButtonCaptured = false;
|
||||
__block NSMutableSet *capturedButtons = [NSMutableSet new];
|
||||
Settings* currentSettings = [dataMan retrieveSettings];
|
||||
[AlertControllerUtil showAlertIn:self
|
||||
title:[LocalizationHelper localizedStringForKey:@"Map controller to mouse"]
|
||||
@@ -2865,8 +2869,9 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
if(!switchButtonCaptured){
|
||||
for(NSNumber* buttonFlagId in buttonDict){
|
||||
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId];
|
||||
if(button.isPressed){
|
||||
if(button.isPressed && ![capturedButtons containsObject:buttonFlagId]){
|
||||
currentSettings.controllerMouseSwitch = buttonFlagId;
|
||||
[capturedButtons addObject:buttonFlagId];
|
||||
switchButtonCaptured = true;
|
||||
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Move the stick you wish to use for mouse control. Another stick will be used for vertical & horizontal scroll."];
|
||||
return;
|
||||
@@ -2889,8 +2894,9 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
if(!leftButtonCaptured && stickCaptured){
|
||||
for(NSNumber* buttonFlagId in buttonDict){
|
||||
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId];
|
||||
if(button.isPressed){
|
||||
if(button.isPressed && ![capturedButtons containsObject:buttonFlagId]){
|
||||
currentSettings.controllerMouseLeftButton = buttonFlagId;
|
||||
[capturedButtons addObject:buttonFlagId];
|
||||
leftButtonCaptured = true;
|
||||
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the button for mouse right button"];
|
||||
return;
|
||||
@@ -2901,8 +2907,9 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
if(!rightButtonCaptured && leftButtonCaptured){
|
||||
for(NSNumber* buttonFlagId in buttonDict){
|
||||
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId];
|
||||
if(button.isPressed){
|
||||
if(button.isPressed && ![capturedButtons containsObject:buttonFlagId]){
|
||||
currentSettings.controllerMouseRightButton = buttonFlagId;
|
||||
[capturedButtons addObject:buttonFlagId];
|
||||
rightButtonCaptured = true;
|
||||
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Finished"];
|
||||
gamepad.valueChangedHandler = nil;
|
||||
@@ -2926,11 +2933,11 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
completion:^{
|
||||
if(AlertControllerUtil.actionCancelled){
|
||||
[self.controllerToMouseSwitch setOn:NO];
|
||||
[self controllerToMouseSwitchFlipped:self.controllerToMouseSwitch];
|
||||
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
return;
|
||||
}
|
||||
[AlertControllerUtil.alertController dismissViewControllerAnimated:NO completion:^{}];
|
||||
[self controllerToMouseSwitchFlipped:self.controllerToMouseSwitch];
|
||||
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
@@ -2939,7 +2946,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
if(AlertControllerUtil.actionCancelled){
|
||||
if(!rightButtonCaptured){
|
||||
[self.controllerToMouseSwitch setOn:rightButtonCaptured];
|
||||
[self controllerToMouseSwitchFlipped:self.controllerToMouseSwitch];
|
||||
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
if(self->capturedController && self->capturedController.extendedGamepad) self->capturedController.extendedGamepad.valueChangedHandler = nil;
|
||||
}
|
||||
@@ -2957,7 +2964,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
- (void)yawSensitivitySliderMoved:(UISlider* )sender{
|
||||
[self findDynamicLabelFromStack:_yawSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (int16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]];
|
||||
[_pitchSensitivitySlider setValue:sender.value];
|
||||
[self pitchSensitivitySliderMoved:self.pitchSensitivitySlider];
|
||||
[_pitchSensitivitySlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
- (void)pitchSensitivitySliderMoved:(UISlider* )sender{
|
||||
@@ -3003,7 +3010,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
[self presentViewController:self.layoutOnScreenControlsVC animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)mainFrameGameProfileButtonTapped{
|
||||
- (void)mainFrameGameProfileButtonTapped:(bool)animated{
|
||||
self.definesPresentationContext = NO;
|
||||
|
||||
[self saveGameProfileConfigs];
|
||||
@@ -3018,7 +3025,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationOverCurrentContext;
|
||||
self.layoutOnScreenControlsVC.profileTableLoadingMode = OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame;
|
||||
[self presentViewController:self.layoutOnScreenControlsVC animated:NO completion:^{
|
||||
[self->_layoutOnScreenControlsVC presentProfilesTableViewWithLoadingMode:OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame];
|
||||
[self->_layoutOnScreenControlsVC presentProfilesTableViewWithLoadingMode:OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame animated:animated];
|
||||
}];
|
||||
_layoutOnScreenControlsVC.toolbarStackView.hidden = true;
|
||||
_layoutOnScreenControlsVC.toolbarRootView.hidden = true;
|
||||
@@ -3169,15 +3176,15 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
/*
|
||||
- (void) asyncNativeTouchPriorityChanged {
|
||||
bool isNativeTouch = [self.touchModeSelector selectedSegmentIndex] == NativeTouchOnly || [self.touchModeSelector selectedSegmentIndex] == NativeTouch;
|
||||
bool asyncNativeTouchEnabled = [self.asyncNativeTouchPrioritySelector selectedSegmentIndex] != AsyncNativeTouchOff;
|
||||
bool isNativeTouch = self.touchModeSelector.selectedSegmentIndex == NativeTouchOnly || self.touchModeSelector.selectedSegmentIndex == NativeTouch;
|
||||
bool asyncNativeTouchEnabled = self.asyncNativeTouchPrioritySelector.selectedSegmentIndex != AsyncNativeTouchOff;
|
||||
[self widget:self.touchMoveEventIntervalSlider setEnabled:isNativeTouch && asyncNativeTouchEnabled];
|
||||
}
|
||||
*/
|
||||
|
||||
- (void)touchMode2Changed:(UISegmentedControl* )sender {
|
||||
// [UIView animateWithDuration:0 animations:^{
|
||||
[self.touchModeSelector1 setSelectedSegmentIndex: sender.selectedSegmentIndex];
|
||||
self.touchModeSelector1.selectedSegmentIndex = sender.selectedSegmentIndex;
|
||||
// } completion:^(BOOL finished) {
|
||||
// 动画完成时执行的代码
|
||||
[self touchModeChanged:sender];
|
||||
@@ -3192,7 +3199,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
- (void)touchModeChanged:(UISegmentedControl* )sender {
|
||||
// Disable On-Screen Controls & Widgets in non-relative touch mode
|
||||
// bool customOscEnabled = [self isOswEnabled] && [self.onScreenWidgetSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
// bool customOscEnabled = [self isOswEnabled] && self.onScreenWidgetSelector.selectedSegmentIndex == OnScreenControlsLevelCustom;
|
||||
|
||||
bool isNativeTouch = sender.selectedSegmentIndex == NativeTouch;
|
||||
// bool isEgmerging = self.enableOswSwitchStack.hidden != !isNativeTouch && isNativeTouch;
|
||||
@@ -3219,7 +3226,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
UISwitch* dummySwitch = [[UISwitch alloc] init];
|
||||
[dummySwitch setOn:(sender.selectedSegmentIndex==RelativeTouch
|
||||
|| (sender.selectedSegmentIndex==AbsoluteTouch && _passthroughGesturesSwitch.isOn))];
|
||||
[self passthroughGesturesSwitchFlipped:dummySwitch];
|
||||
[self.passthroughGesturesSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
/*
|
||||
[self setHidden:((sender.selectedSegmentIndex!=RelativeTouch
|
||||
@@ -3285,8 +3292,8 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
- (void)passthroughGesturesSwitchFlipped:(UISwitch* )sender{
|
||||
[self setHidden:!sender.isOn forStack:_pinchGestureStack];
|
||||
[self setHidden:!sender.isOn forStack:_scrollSensitivityStack];
|
||||
if(!sender.isOn) [self pinchGestureSwitchFlipped:sender];
|
||||
else [self pinchGestureSwitchFlipped:_pinchGestureSwitch];
|
||||
if(!sender.isOn) [self.pinchGestureSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
else [_pinchGestureSwitch sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
- (void)softKeyboardHeightSwitchFlipped:(UISwitch* )sender{
|
||||
@@ -3474,7 +3481,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
- (void) newResolutionChosen {
|
||||
[self updateBitrate];
|
||||
[self updateResolutionDisplayLabel];
|
||||
_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
||||
_lastSelectedResolutionIndex = self.resolutionSelector.selectedSegmentIndex;
|
||||
[self updateResolutionTable];
|
||||
}
|
||||
|
||||
@@ -3527,7 +3534,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
long height = [heightField.text integerValue];
|
||||
if (width <= 0 || height <= 0) {
|
||||
// Restore the previous selection
|
||||
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
|
||||
self.resolutionSelector.selectedSegmentIndex = self->_lastSelectedResolutionIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3604,6 +3611,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
|
||||
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
|
||||
[self updateBitrateText];
|
||||
[self updateResolutionDisplayLabel];
|
||||
}
|
||||
|
||||
- (bool)hdrSupported{
|
||||
@@ -3617,7 +3625,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenFrameRate {
|
||||
switch ([self.framerateSelector selectedSegmentIndex]) {
|
||||
switch (self.framerateSelector.selectedSegmentIndex) {
|
||||
case 0:
|
||||
return 30;
|
||||
case 1:
|
||||
@@ -3649,7 +3657,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height;
|
||||
}
|
||||
|
||||
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].height;
|
||||
return resolutionTable[self.resolutionSelector.selectedSegmentIndex].height;
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenStreamWidth {
|
||||
@@ -3658,7 +3666,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
|
||||
}
|
||||
|
||||
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width;
|
||||
return resolutionTable[self.resolutionSelector.selectedSegmentIndex].width;
|
||||
}
|
||||
|
||||
- (UIStackView *)findFlatStackViewFrom:(UIView *)view {
|
||||
@@ -3821,7 +3829,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
- (void)pencilProPurchaseAborted:(NSNotification *)notification{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.pencilTickSelector.selectedSegmentIndex = PencilTickDisabled;
|
||||
[self pencilTickModeChanged:self.pencilTickSelector];
|
||||
[self.pencilTickSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
[self.pressureCurveSwitch setOn:false];
|
||||
[self.doubleTapShortcutSwitch setOn:false];
|
||||
[self.squeezeShortcutSwitch setOn:false];
|
||||
@@ -3955,11 +3963,11 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
if([PublicUtils isIPad]){
|
||||
self.pencilTickSelector.selectedSegmentIndex = tempSettings.pencilTickMode.intValue;
|
||||
[self.pencilTickSelector addTarget:self action:@selector(pencilTickModeChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self pencilTickModeChanged:self.pencilTickSelector];
|
||||
[self.pencilTickSelector sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.pencilTickIntervalSlider setValue:tempSettings.pencilTickIntervalUs.floatValue];
|
||||
[self.pencilTickIntervalSlider addTarget:self action:@selector(pencilTickIntervalSliderMoved:) forControlEvents:UIControlEventValueChanged];
|
||||
[self pencilTickIntervalSliderMoved:self.pencilTickIntervalSlider];
|
||||
[self.pencilTickIntervalSlider sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self.pressureCurveSwitch addTarget:self action:@selector(pressureCurveSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.doubleTapShortcutSwitch addTarget:self action:@selector(doubleTapShortcutSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
|
||||
@@ -3995,15 +4003,15 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
NSInteger framerate = [self getChosenFrameRate];
|
||||
|
||||
NSInteger audioConfig = [@[@2, @3, @6, @8][[self.audioConfigSelector selectedSegmentIndex]] integerValue];
|
||||
NSInteger audioConfig = [@[@2, @3, @6, @8][self.audioConfigSelector.selectedSegmentIndex] integerValue];
|
||||
// 2 - stereo (system)
|
||||
// 3 - stereo (SDL)
|
||||
// 6 - 5.1 (SDL)
|
||||
// 8 - 7.1 (SDL)
|
||||
|
||||
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
|
||||
NSInteger framePacingMode = [self.framePacingModeSelector selectedSegmentIndex];
|
||||
NSInteger onscreenControls = [self.onScreenWidgetSelector selectedSegmentIndex];
|
||||
NSInteger renderingBackend = self.renderingBackendSelector.selectedSegmentIndex;
|
||||
NSInteger framePacingMode = self.framePacingModeSelector.selectedSegmentIndex;
|
||||
NSInteger onscreenControls = self.onScreenWidgetSelector.selectedSegmentIndex;
|
||||
NSInteger keyboardToggleFingers = self.softKeyboardGestureSelector.selectedSegmentIndex == 3 ? 20 : self.softKeyboardGestureSelector.selectedSegmentIndex+3;
|
||||
NSInteger oscLayoutToolFingers = (uint16_t)self->oswLayoutFingers;
|
||||
|
||||
@@ -4019,9 +4027,9 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
|
||||
uint16_t touchMoveEventInterval = 0;
|
||||
|
||||
BOOL reverseMouseWheelDirection = [self.reverseMouseWheelDirectionSelector selectedSegmentIndex] == 1;
|
||||
BOOL reverseMouseWheelDirection = self.reverseMouseWheelDirectionSelector.selectedSegmentIndex == 1;
|
||||
NSInteger asyncNativeTouchPriority = 1;
|
||||
//BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
|
||||
//BOOL liftStreamViewForKeyboard = self.liftStreamViewForKeyboardSelector.selectedSegmentIndex == 1;
|
||||
BOOL liftStreamViewForKeyboard = YES; // enable and hide this option
|
||||
BOOL showKeyboardToolbar = self.softKeyboardToolbarSwitch.isOn;
|
||||
BOOL optimizeGames = self.optimizeGamesSwitch.isOn;
|
||||
@@ -4041,19 +4049,19 @@ BOOL isCustomResolution(int resolutionSelected) {
|
||||
BOOL fullColorRange = self.fullColorRangeSwitch.isOn;
|
||||
BOOL btMouseSupport = self.citrixX1MouseSwitch.isOn;
|
||||
NSInteger touchMode = [self isNotNativeTouchOnly] ? self.touchModeSelector1.selectedSegmentIndex : NativeTouchOnly;
|
||||
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
|
||||
NSInteger statsOverlayLevel = self.statsOverlaySelector.selectedSegmentIndex;
|
||||
BOOL statsOverlayEnabled = statsOverlayLevel != 0;
|
||||
BOOL enableHdr = self.hdrSwitch.isOn;
|
||||
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
|
||||
BOOL unlockDisplayOrientation = self.unlockDisplayOrientationSelector.selectedSegmentIndex == 1;
|
||||
BOOL enableGraphs = self.enableGraphsSwitch.isOn;
|
||||
int graphOpacity = (int)self.graphOpacityStepper.value;
|
||||
int frameQueueSize = (int)self.frameQueueSizeSlider.value;
|
||||
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
||||
NSInteger resolutionSelected = self.resolutionSelector.selectedSegmentIndex;
|
||||
if (self.customResolutionSwitch.isOn) {
|
||||
resolutionSelected = RESOLUTION_TABLE_CUSTOM_INDEX;
|
||||
}
|
||||
NSInteger externalDisplayMode = [self.externalDisplayModeSelector selectedSegmentIndex];
|
||||
NSInteger localMousePointerMode = [self.localMousePointerModeSelector selectedSegmentIndex];
|
||||
NSInteger externalDisplayMode = self.externalDisplayModeSelector.selectedSegmentIndex;
|
||||
NSInteger localMousePointerMode = self.localMousePointerModeSelector.selectedSegmentIndex;
|
||||
BOOL sendDummyEvent = self.sendDummyEventSwitch.isOn;
|
||||
BOOL rememberFoldState = self.rememberFoldStateSwitch.isOn;
|
||||
CGFloat singleTapSensitivity = self.singleTapSensitivitySlider.value;
|
||||
|
||||
@@ -1005,6 +1005,8 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
|
||||
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
OnScreenWidgetView.enableFolderAnimation = true;
|
||||
});
|
||||
|
||||
[ControllerNavigator stop];
|
||||
}
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification{
|
||||
@@ -1050,7 +1052,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
|
||||
_layoutOnScreenControlsVC.toolbarRootView.hidden = true;
|
||||
OSCProfilesTableViewLoadingMode loadingMode = pickProfile ? OSCProfilesTableViewLoadingModePickProfile : OSCProfilesTableViewLoadingModeSelectProfileFromStreamView;
|
||||
[self presentViewController:_layoutOnScreenControlsVC animated:NO completion:^{
|
||||
[self->_layoutOnScreenControlsVC presentProfilesTableViewWithLoadingMode:loadingMode];
|
||||
[self->_layoutOnScreenControlsVC presentProfilesTableViewWithLoadingMode:loadingMode animated:false];
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,26 @@ import SwiftUI
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
public enum RadialMenuItem {
|
||||
case settings
|
||||
case hostView
|
||||
case gameProfiles
|
||||
case exit
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct RadialMenuItem: Identifiable, Equatable {
|
||||
struct RadialMenuSector: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let systemImageName: String
|
||||
let item: RadialMenuItem
|
||||
|
||||
init(title: String, subtitle: String, systemImageName: String) {
|
||||
init(title: String, subtitle: String, systemImageName: String, item: RadialMenuItem) {
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.systemImageName = systemImageName
|
||||
self.item = item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +94,9 @@ final class RadialMenuSelectionState: ObservableObject {
|
||||
return nil
|
||||
}
|
||||
|
||||
let clockwiseDegreesFromTop = positiveRemainder(atan2(Double(x), Double(y)) * 180 / .pi, 360)
|
||||
let clockwiseDegreesFromWest = positiveRemainder(atan2(Double(y), -Double(x)) * 180 / .pi, 360)
|
||||
let segmentDegrees = 360 / Double(itemCount)
|
||||
let centeredDegrees = positiveRemainder(clockwiseDegreesFromTop + segmentDegrees / 2, 360)
|
||||
let centeredDegrees = positiveRemainder(clockwiseDegreesFromWest + segmentDegrees / 2, 360)
|
||||
return min(Int(centeredDegrees / segmentDegrees), itemCount - 1)
|
||||
}
|
||||
|
||||
@@ -99,7 +108,7 @@ final class RadialMenuSelectionState: ObservableObject {
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct RadialMenuView: View {
|
||||
let items: [RadialMenuItem]
|
||||
let sectors: [RadialMenuSector]
|
||||
var selectedIndex: Int?
|
||||
var isTouchSelectionEnabled: Bool
|
||||
var style: RadialMenuStyle
|
||||
@@ -119,31 +128,31 @@ struct RadialMenuView: View {
|
||||
return selectedIndex
|
||||
}
|
||||
|
||||
private var selectedItem: RadialMenuItem? {
|
||||
private var selectedItem: RadialMenuSector? {
|
||||
guard let selectedIndex = effectiveSelectedIndex,
|
||||
items.indices.contains(selectedIndex) else {
|
||||
return items.first
|
||||
sectors.indices.contains(selectedIndex) else {
|
||||
return sectors.first
|
||||
}
|
||||
return items[selectedIndex]
|
||||
return sectors[selectedIndex]
|
||||
}
|
||||
|
||||
init(
|
||||
items: [RadialMenuItem],
|
||||
sectors: [RadialMenuSector],
|
||||
selectedIndex: Int? = nil,
|
||||
isTouchSelectionEnabled: Bool = true,
|
||||
style: RadialMenuStyle = RadialMenuStyle(),
|
||||
selectionState: RadialMenuSelectionState = RadialMenuSelectionState()
|
||||
) {
|
||||
self.items = items
|
||||
self.sectors = sectors
|
||||
self.selectedIndex = selectedIndex
|
||||
self.isTouchSelectionEnabled = isTouchSelectionEnabled
|
||||
self.style = style
|
||||
self.joystickSelectionState = selectionState
|
||||
self.joystickSelectionState.configureItemCount(items.count)
|
||||
self.joystickSelectionState.configureItemCount(sectors.count)
|
||||
}
|
||||
|
||||
func updateSelection(xOffset: Float, yOffset: Float) {
|
||||
joystickSelectionState.configureItemCount(items.count)
|
||||
joystickSelectionState.configureItemCount(sectors.count)
|
||||
joystickSelectionState.updateSelection(xOffset: xOffset, yOffset: yOffset)
|
||||
}
|
||||
|
||||
@@ -155,10 +164,10 @@ struct RadialMenuView: View {
|
||||
let innerRadius = outerRadius * (1 - style.ringWidthRatio)
|
||||
|
||||
ZStack {
|
||||
ForEach(items.indices, id: \.self) { index in
|
||||
ForEach(sectors.indices, id: \.self) { index in
|
||||
RadialMenuSegmentShape(
|
||||
index: index,
|
||||
count: items.count,
|
||||
count: sectors.count,
|
||||
innerRadiusRatio: 1 - style.ringWidthRatio,
|
||||
gapWidth: style.segmentGapWidth
|
||||
)
|
||||
@@ -166,7 +175,7 @@ struct RadialMenuView: View {
|
||||
.overlay(
|
||||
RadialMenuSegmentShape(
|
||||
index: index,
|
||||
count: items.count,
|
||||
count: sectors.count,
|
||||
innerRadiusRatio: 1 - style.ringWidthRatio,
|
||||
gapWidth: style.segmentGapWidth
|
||||
)
|
||||
@@ -181,13 +190,13 @@ struct RadialMenuView: View {
|
||||
.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]
|
||||
ForEach(sectors.indices, id: \.self) { index in
|
||||
let item = sectors[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))
|
||||
.position(iconPosition(index: index, count: sectors.count, center: center, radius: (outerRadius + innerRadius) / 2))
|
||||
.animation(.spring(response: 0.24, dampingFraction: 0.72), value: effectiveSelectedIndex)
|
||||
}
|
||||
|
||||
@@ -218,7 +227,7 @@ struct RadialMenuView: View {
|
||||
}
|
||||
|
||||
private func index(for location: CGPoint, center: CGPoint, innerRadius: CGFloat, outerRadius: CGFloat) -> Int? {
|
||||
guard !items.isEmpty else { return nil }
|
||||
guard !sectors.isEmpty else { return nil }
|
||||
|
||||
let dx = location.x - center.x
|
||||
let dy = location.y - center.y
|
||||
@@ -227,17 +236,16 @@ struct RadialMenuView: View {
|
||||
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)
|
||||
let clockwiseDegreesFromWest = positiveRemainder(atan2(Double(-dy), Double(-dx)) * 180 / Double.pi, 360)
|
||||
let segmentDegrees = 360 / Double(sectors.count)
|
||||
let centeredDegrees = positiveRemainder(clockwiseDegreesFromWest + segmentDegrees / 2, 360)
|
||||
return min(Int(centeredDegrees / segmentDegrees), sectors.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 angleDegrees = 180 + Double(index) * segmentDegrees
|
||||
let radians = angleDegrees * .pi / 180
|
||||
return CGPoint(
|
||||
x: center.x + CGFloat(cos(radians)) * radius,
|
||||
@@ -253,7 +261,7 @@ struct RadialMenuView: View {
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private struct RadialMenuCenterView: View {
|
||||
let item: RadialMenuItem?
|
||||
let item: RadialMenuSector?
|
||||
let style: RadialMenuStyle
|
||||
let diameter: CGFloat
|
||||
|
||||
@@ -298,7 +306,7 @@ private struct RadialMenuSegmentShape: Shape {
|
||||
let outerRadius = min(rect.width, rect.height) / 2
|
||||
let innerRadius = outerRadius * innerRadiusRatio
|
||||
let segmentDegrees = 360 / Double(count)
|
||||
let centerDegrees = -90 + Double(index) * segmentDegrees
|
||||
let centerDegrees = 180 + Double(index) * segmentDegrees
|
||||
let startBoundaryDegrees = centerDegrees - segmentDegrees / 2
|
||||
let endBoundaryDegrees = centerDegrees + segmentDegrees / 2
|
||||
let halfGap = min(gapWidth / 2, max(innerRadius - 1, 0))
|
||||
@@ -379,19 +387,20 @@ struct RadialMenuDemoView: View {
|
||||
@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"),
|
||||
RadialMenuSector(title: "Settings Menu".localized, subtitle: "", systemImageName: "sidebar.left", item: .settings),
|
||||
RadialMenuSector(title: "Host View".localized, subtitle: "", systemImageName: PublicUtils.liquidGlassEnabled ? "macwindow.on.rectangle" : "tv", item: .hostView),
|
||||
RadialMenuSector(title: "Game Profiles".localized, subtitle: "", systemImageName: "gamecontroller.circle", item: .gameProfiles),
|
||||
RadialMenuSector(title: "Exit/Disconnect".localized, subtitle: "", systemImageName: "personalhotspot.slash", item: .gameProfiles),
|
||||
]
|
||||
|
||||
private var visibleItems: [RadialMenuItem] {
|
||||
private var visibleItems: [RadialMenuSector] {
|
||||
Array(Self.menuItems.prefix(itemCount))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 28) {
|
||||
RadialMenuView(
|
||||
items: visibleItems,
|
||||
sectors: visibleItems,
|
||||
selectedIndex: isTouchSelectionEnabled ? nil : 2,
|
||||
isTouchSelectionEnabled: isTouchSelectionEnabled
|
||||
)
|
||||
@@ -421,7 +430,7 @@ struct RadialMenuView_Previews: PreviewProvider {
|
||||
.previewLayout(.sizeThatFits)
|
||||
|
||||
RadialMenuView(
|
||||
items: Array(RadialMenuDemoView.menuItems.prefix(6)),
|
||||
sectors: Array(RadialMenuDemoView.menuItems.prefix(6)),
|
||||
selectedIndex: 1,
|
||||
isTouchSelectionEnabled: false
|
||||
)
|
||||
|
||||
@@ -73,6 +73,7 @@ struct Key: Identifiable {
|
||||
let visualStyle: KeyVisualStyle
|
||||
let label: String
|
||||
let width: CGFloat
|
||||
let labelInsets: EdgeInsets
|
||||
let sfSymbolName: String?
|
||||
let winCmdString: String?
|
||||
let macCmdString: String?
|
||||
@@ -85,6 +86,7 @@ struct Key: Identifiable {
|
||||
identity: String? = nil,
|
||||
visualStyle: KeyVisualStyle = .standard,
|
||||
width: CGFloat,
|
||||
labelInsets: EdgeInsets = EdgeInsets(top: 3, leading: 6, bottom: 3, trailing: 6),
|
||||
sfSymbolName: String? = nil,
|
||||
winCmdString: String? = nil,
|
||||
macCmdString: String? = nil,
|
||||
@@ -94,6 +96,7 @@ struct Key: Identifiable {
|
||||
self.label = label
|
||||
self.identity = identity
|
||||
self.width = width
|
||||
self.labelInsets = labelInsets
|
||||
self.visualStyle = visualStyle
|
||||
self.sfSymbolName = sfSymbolName
|
||||
self.winCmdString = winCmdString
|
||||
@@ -1041,8 +1044,8 @@ struct VirtualKeyboardView: View {
|
||||
Key(
|
||||
label: LocalizationHelper.localizedString(forKey: "Wheel\nUp"),
|
||||
width: unit,
|
||||
winCmdString: "WHEELDOWN",
|
||||
macCmdString: "WHEELDOWN",
|
||||
winCmdString: "WHEELUP",
|
||||
macCmdString: "WHEELUP",
|
||||
winKeyCode: 0xFF,
|
||||
macKeyCode: 0xFF
|
||||
)
|
||||
@@ -1052,8 +1055,8 @@ struct VirtualKeyboardView: View {
|
||||
Key(
|
||||
label: LocalizationHelper.localizedString(forKey: "Wheel\nDown"),
|
||||
width: unit,
|
||||
winCmdString: "WHEELUP",
|
||||
macCmdString: "WHEELUP",
|
||||
winCmdString: "WHEELDOWN",
|
||||
macCmdString: "WHEELDOWN",
|
||||
winKeyCode: 0xFF,
|
||||
macKeyCode: 0xFF
|
||||
)
|
||||
@@ -1065,8 +1068,8 @@ struct VirtualKeyboardView: View {
|
||||
Key(
|
||||
label: LocalizationHelper.localizedString(forKey: "Wheel\nUp"),
|
||||
width: unit,
|
||||
winCmdString: "WHEELDOWN",
|
||||
macCmdString: "WHEELDOWN",
|
||||
winCmdString: "WHEELUP",
|
||||
macCmdString: "WHEELUP",
|
||||
winKeyCode: 0xFF,
|
||||
macKeyCode: 0xFF
|
||||
)
|
||||
@@ -1076,8 +1079,8 @@ struct VirtualKeyboardView: View {
|
||||
Key(
|
||||
label: LocalizationHelper.localizedString(forKey: "Wheel\nDown"),
|
||||
width: unit,
|
||||
winCmdString: "WHEELUP",
|
||||
macCmdString: "WHEELUP",
|
||||
winCmdString: "WHEELDOWN",
|
||||
macCmdString: "WHEELDOWN",
|
||||
winKeyCode: 0xFF,
|
||||
macKeyCode: 0xFF
|
||||
)
|
||||
@@ -1364,11 +1367,15 @@ struct KeyView: View {
|
||||
key.keyCode(for: keyboardType)
|
||||
}
|
||||
|
||||
private var keyCornerRadius: CGFloat {
|
||||
11.0
|
||||
}
|
||||
|
||||
private var typingHighlightFill: LinearGradient {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.78, green: 0.84, blue: 0.92).opacity(0.95),
|
||||
Color(red: 0.66, green: 0.74, blue: 0.85).opacity(0.88)
|
||||
Color(red: 0.92, green: 0.945, blue: 0.98),
|
||||
Color(red: 0.76, green: 0.82, blue: 0.90)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
@@ -1376,31 +1383,64 @@ struct KeyView: View {
|
||||
}
|
||||
|
||||
private var typingHighlightStroke: Color {
|
||||
Color(red: 0.42, green: 0.52, blue: 0.66).opacity(0.78)
|
||||
Color(red: 0.47, green: 0.58, blue: 0.73).opacity(0.62)
|
||||
}
|
||||
|
||||
private var keyFillColor: Color {
|
||||
private var keyFillGradient: LinearGradient {
|
||||
switch key.visualStyle {
|
||||
case .standard:
|
||||
return Color.gray.opacity(0.2)
|
||||
return LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.990, green: 0.988, blue: 0.984),
|
||||
Color(red: 0.920, green: 0.920, blue: 0.910)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
case .directionPad:
|
||||
return usesDirectionPadBaseStyle
|
||||
? Color(red: 0.16, green: 0.72, blue: 0.66).opacity(0.2)
|
||||
: Color.gray.opacity(0.2)
|
||||
if usesDirectionPadBaseStyle {
|
||||
return LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.88, green: 0.98, blue: 0.96),
|
||||
Color(red: 0.73, green: 0.91, blue: 0.88)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
}
|
||||
return LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.990, green: 0.988, blue: 0.984),
|
||||
Color(red: 0.920, green: 0.920, blue: 0.910)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var keyStrokeColor: Color {
|
||||
switch key.visualStyle {
|
||||
case .standard:
|
||||
return Color.gray.opacity(0.2)
|
||||
return Color(red: 0.66, green: 0.66, blue: 0.64).opacity(0.36)
|
||||
case .directionPad:
|
||||
return usesDirectionPadBaseStyle
|
||||
? Color(red: 0.08, green: 0.49, blue: 0.45).opacity(0.55)
|
||||
: Color.gray.opacity(0.2)
|
||||
? Color(red: 0.18, green: 0.56, blue: 0.52).opacity(0.52)
|
||||
: Color(red: 0.66, green: 0.66, blue: 0.64).opacity(0.36)
|
||||
}
|
||||
}
|
||||
|
||||
private var pressedFillGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.84, green: 0.87, blue: 0.91),
|
||||
Color(red: 0.72, green: 0.76, blue: 0.82)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
}
|
||||
|
||||
private var foregroundColor: Color {
|
||||
if mode == .typing && (highlightStyle != .none || isPressed) {
|
||||
return Color.black.opacity(0.72)
|
||||
@@ -1427,56 +1467,54 @@ struct KeyView: View {
|
||||
Image(systemName: name)
|
||||
.font(.system(size: fontSize))
|
||||
.foregroundColor(.black)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
Text(key.label)
|
||||
.font(.system(size: fontSize, weight: .regular, design: .rounded))
|
||||
.foregroundColor(.black)
|
||||
.lineLimit(key.label.contains("\n") ? 2 : 1)
|
||||
.minimumScaleFactor(0.45)
|
||||
.allowsTightening(true)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
if highlightStyle != .none {
|
||||
return Color.clear
|
||||
}
|
||||
|
||||
if isPressed || isActive {
|
||||
return Color.gray.opacity(0.4)
|
||||
}
|
||||
return keyFillColor
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var keyBackground: some View {
|
||||
if mode == .typing, (highlightStyle != .none || isPressed) {
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.fill(typingHighlightFill)
|
||||
} else if isPressed || isActive {
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.fill(pressedFillGradient)
|
||||
} else if highlightStyle == .pad {
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 0.82, green: 0.84, blue: 1.00).opacity(0.92),
|
||||
Color(red: 0.56, green: 0.60, blue: 0.94).opacity(0.84)
|
||||
Color(red: 0.89, green: 0.90, blue: 1.00),
|
||||
Color(red: 0.64, green: 0.68, blue: 0.96)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
} else if highlightStyle == .single {
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color(red: 1.00, green: 0.96, blue: 0.74),
|
||||
Color(red: 0.95, green: 0.78, blue: 0.32)
|
||||
Color(red: 1.00, green: 0.98, blue: 0.78),
|
||||
Color(red: 0.96, green: 0.80, blue: 0.36)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.fill(backgroundColor)
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.fill(keyFillGradient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1493,15 +1531,33 @@ struct KeyView: View {
|
||||
return keyStrokeColor
|
||||
}
|
||||
|
||||
private var keyShadowColor: Color {
|
||||
if isPressed || isActive {
|
||||
return Color.black.opacity(0.08)
|
||||
}
|
||||
if highlightStyle != .none {
|
||||
return Color.black.opacity(0.08)
|
||||
}
|
||||
return Color.black.opacity(PublicUtils.isIPhone ? 0.13 : 0.15)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
keyContent
|
||||
.padding(key.labelInsets)
|
||||
.foregroundColor(foregroundColor)
|
||||
.frame(width: key.width, height: height)
|
||||
.background(keyBackground)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(borderColor, lineWidth: highlightStyle == .none ? (key.visualStyle == .standard ? 1 : 1.5) : 1.5)
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.stroke(Color.white.opacity(isPressed || isActive ? 0.22 : 0.45), lineWidth: 0.76)
|
||||
.padding(1)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous)
|
||||
.stroke(borderColor, lineWidth: highlightStyle == .none ? (key.visualStyle == .standard ? (PublicUtils.isIPhone ? 1.15 : 0.7) : 1.1) : 1.25)
|
||||
|
||||
)
|
||||
.shadow(color: keyShadowColor, radius: isPressed || isActive ? 1 : 3, x: 0, y: isPressed || isActive ? 1 : 2)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
|
||||
@@ -694,6 +694,36 @@ struct WidgetPickerView: View {
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: nil
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=absoluteTouchDrag",
|
||||
cmd: "ABSTCHDRAG",
|
||||
tip: LocalizationHelper.localizedString(forKey: "Replace mouse button action in single point touch mode with another button temporarily"),
|
||||
allowsKeyboardCombination: true,
|
||||
allowsGamepadCombination: true,
|
||||
allowsSkillCombo: true,
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: .skill
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=magnifier",
|
||||
cmd: "MAGNIFIER",
|
||||
tip: LocalizationHelper.localizedString(forKey: "magnifierTip"),
|
||||
allowsKeyboardCombination: false,
|
||||
allowsGamepadCombination: false,
|
||||
allowsSkillCombo: false,
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: .skill
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=dummyPad",
|
||||
cmd: "DUMMYPAD",
|
||||
tip: LocalizationHelper.localizedString(forKey: "dummyPadTip"),
|
||||
allowsKeyboardCombination: false,
|
||||
allowsGamepadCombination: false,
|
||||
allowsSkillCombo: false,
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: .skill
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=pressureCurve",
|
||||
cmd: "PRESSURECURVE",
|
||||
@@ -745,34 +775,14 @@ struct WidgetPickerView: View {
|
||||
forcedComboMode: nil
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=absoluteTouchDrag",
|
||||
cmd: "ABSTCHDRAG",
|
||||
tip: LocalizationHelper.localizedString(forKey: "Replace mouse button action in single point touch mode with another button temporarily"),
|
||||
allowsKeyboardCombination: true,
|
||||
allowsGamepadCombination: true,
|
||||
allowsSkillCombo: true,
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: .skill
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=magnifier",
|
||||
cmd: "MAGNIFIER",
|
||||
tip: LocalizationHelper.localizedString(forKey: "magnifierTip"),
|
||||
localizationKey: "=disableTilt",
|
||||
cmd: "DISABLETILT",
|
||||
tip: LocalizationHelper.localizedString(forKey: "Disable tilt for apple pencil"),
|
||||
allowsKeyboardCombination: false,
|
||||
allowsGamepadCombination: false,
|
||||
allowsSkillCombo: false,
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: .skill
|
||||
),
|
||||
FunctionalButtonOption(
|
||||
localizationKey: "=dummyPad",
|
||||
cmd: "DUMMYPAD",
|
||||
tip: LocalizationHelper.localizedString(forKey: "dummyPadTip"),
|
||||
allowsKeyboardCombination: false,
|
||||
allowsGamepadCombination: false,
|
||||
allowsSkillCombo: false,
|
||||
allowsShortcutCombo: false,
|
||||
forcedComboMode: .skill
|
||||
forcedComboMode: nil
|
||||
),
|
||||
]
|
||||
|
||||
@@ -3739,6 +3749,8 @@ struct FunctionalCollectionButton: View {
|
||||
UIDevice.current.userInterfaceIdiom == .phone ? 8 : 12
|
||||
}
|
||||
|
||||
private let cornerRadius: CGFloat = PublicUtils.isIPhone ? 16 : 24
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Text(LocalizationHelper.localizedString(forKey: item.localizationKey))
|
||||
@@ -3757,7 +3769,7 @@ struct FunctionalCollectionButton: View {
|
||||
}
|
||||
|
||||
private var backgroundFill: some View {
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: isSelected
|
||||
@@ -3777,7 +3789,7 @@ struct FunctionalCollectionButton: View {
|
||||
}
|
||||
|
||||
private var borderOverlay: some View {
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.stroke(
|
||||
isSelected ? Color.orange.opacity(0.75) : Color.white.opacity(0.76),
|
||||
lineWidth: 1.4
|
||||
|
||||
Reference in New Issue
Block a user