- 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:
True Zhuanjia
2026-07-07 16:13:46 +08:00
parent 54c106e01e
commit c968a1df2a
34 changed files with 2048 additions and 1758 deletions
+20 -18
View File
@@ -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 */,
+1 -1
View File
@@ -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">
-33
View File
@@ -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
-644
View File
@@ -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
+593
View File
@@ -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
}
}
}
+3 -1
View File
@@ -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"]
+171
View File
@@ -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()
}
}
}
+7 -7
View File
@@ -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);
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+21 -8
View File
@@ -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()
}
+3 -1
View File
@@ -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
+105
View File
@@ -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
}
}
+84 -26
View File
@@ -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"
}
}
-54
View File
@@ -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
-425
View File
@@ -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
+371
View File
@@ -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()
}
}
}
+2 -2
View File
@@ -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];
}
}
}
+1
View File
@@ -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
+38 -2
View File
@@ -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 = ""
}
@@ -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
+145 -137
View File
@@ -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];
}];
}
+43 -34
View File
@@ -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
)
+97 -41
View File
@@ -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
+38 -26
View File
@@ -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