further renaming

This commit is contained in:
TrueZhuanjia
2025-06-26 22:15:18 +08:00
parent 9e41537554
commit ee03f6b866
353 changed files with 14 additions and 15 deletions
@@ -0,0 +1,22 @@
//
// CustomEdgeSlideGestureRecognizer.h
// VoidLink
//
// Created by True砖家 on 2024/4/30.
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
//
#ifndef CustomEdgeSlideGestureRecognizer_h
#define CustomEdgeSlideGestureRecognizer_h
// CustomEdgeSlideGestureRecognizer.h
#import <UIKit/UIKit.h>
@interface CustomEdgeSlideGestureRecognizer : UIGestureRecognizer
@property (nonatomic, assign) UIRectEdge edges; // Specify the edge(s) you want to recognize the swipe gesture on
@property (nonatomic, assign) CGFloat normalizedThresholdDistance; // Distance from the edge to start recognizing the gesture
@property (nonatomic, assign) bool immediateTriggering;
@property (nonatomic, assign) CGFloat EDGE_TOLERANCE;
@end
#endif /* CustomEdgeSlideGestureRecognizer_h */
@@ -0,0 +1,77 @@
//
// CustomEdgeSlideGestureRecognizer.m
// VoidLink
//
// Created by True砖家 on 2024/4/30.
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
//
// #import <Foundation/Foundation.h>
#import "CustomEdgeSlideGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
@implementation CustomEdgeSlideGestureRecognizer
UITouch* capturedUITouch;
CGFloat startPointX;
static CGFloat screenWidthInPoints;
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action {
self = [super initWithTarget:target action:action];
screenWidthInPoints = CGRectGetWidth([UIApplication.sharedApplication.windows.firstObject.screen bounds]); // Get the screen's bounds (in points)
_immediateTriggering = false;
_EDGE_TOLERANCE = 15.0f;
return self;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
capturedUITouch = touch;
startPointX = [capturedUITouch locationInView:self.view].x;
if(_immediateTriggering){
if(self.edges & UIRectEdgeLeft){
if(startPointX < _EDGE_TOLERANCE){
self.state = UIGestureRecognizerStateEnded;
}
// NSLog(@"startPointX %f , normalizedGestureDeltaX %f", startPointX, normalizedGestureDistance);
}
if(self.edges & UIRectEdgeRight){
if(startPointX > screenWidthInPoints - _EDGE_TOLERANCE){
self.state = UIGestureRecognizerStateEnded;
}
// NSLog(@"startPointX %f , normalizedGestureDeltaX %f", startPointX, normalizedGestureDistance);
}
}
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [super touchesEnded:touches withEvent:event];
if([touches containsObject:capturedUITouch]){
CGFloat _endPointX = [capturedUITouch locationInView:self.view].x;
CGFloat normalizedGestureDistance = fabs(_endPointX - startPointX)/screenWidthInPoints;
if(self.edges & UIRectEdgeLeft){
if(startPointX < _EDGE_TOLERANCE && normalizedGestureDistance > _normalizedThresholdDistance){
self.state = UIGestureRecognizerStateEnded;
}
// NSLog(@"startPointX %f , normalizedGestureDeltaX %f", startPointX, normalizedGestureDistance);
}
if(self.edges & UIRectEdgeRight){
if((startPointX > (screenWidthInPoints - _EDGE_TOLERANCE)) && normalizedGestureDistance > _normalizedThresholdDistance){
self.state = UIGestureRecognizerStateEnded;
}
// NSLog(@"startPointX %f , normalizedGestureDeltaX %f", startPointX, normalizedGestureDistance);
}
}
}
@end
@@ -0,0 +1,19 @@
//
// ProfileTableViewCell.h
// Moonlight
//
// Created by Long Le on 12/11/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ProfileTableViewCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *name;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,21 @@
//
// ProfileTableViewCell.m
// Moonlight
//
// Created by Long Le on 12/11/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import "ProfileTableViewCell.h"
@implementation ProfileTableViewCell
- (void)awakeFromNib {
[super awakeFromNib];
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
}
@end
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="Cell" id="KGk-i7-Jjw" customClass="ProfileTableViewCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="280" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Profile Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="b1d-b4-TCK">
<rect key="frame" x="16.000000000000007" y="11.666666666666664" width="97.666666666666686" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.95294117647058818" green="0.97647058823529409" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.1215686275" green="0.12941176469999999" blue="0.14117647059999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="b1d-b4-TCK" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="pxI-vz-oR6"/>
<constraint firstItem="b1d-b4-TCK" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="xCR-Og-ne7"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="aW0-zy-SZf"/>
<color key="backgroundColor" red="0.1215686275" green="0.12941176469999999" blue="0.14117647059999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="0.6705882353" green="0.61568627450000002" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<outlet property="name" destination="b1d-b4-TCK" id="CX8-IC-jlM"/>
</connections>
<point key="canvasLocation" x="108" y="39"/>
</tableViewCell>
</objects>
</document>
@@ -0,0 +1,94 @@
//
// LayoutOnScreenControlsViewController.h
// Moonlight
//
// Created by Long Le on 9/27/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 since 2024.6.24
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "LayoutOnScreenControls.h"
#import "ToolBarContainerView.h"
#import "OSCProfilesManager.h"
#import "OSCProfilesTableViewController.h"
#import "Moonlight-Swift.h"
NS_ASSUME_NONNULL_BEGIN
/**
This view controller provides the user interface which allows the user to position on screen controller buttons anywhere they'd like on the screen. It also provides the user with the abilities to undo a change, save the on screen controller layout for later retrieval, and load previously saved controller layouts
*/
@interface LayoutOnScreenControlsViewController : UIViewController <OnScreenWidgetGuidelineUpdateDelegate>
- (void)profileRefresh;
- (void)reloadOnScreenWidgetViews;
- (void)presentProfilesTableView;
@property LayoutOnScreenControls *layoutOSC; // object that contains a view which contains the on screen controller buttons that allows the user to drag and positions each button on the screen using touch
@property (nonatomic) NSMutableSet* OnScreenWidgetViews;
@property int OSCSegmentSelected;
@property (nonatomic, assign) bool quickSwitchEnabled;
@property (weak, nonatomic) IBOutlet UIButton *trashCanButton;
@property (weak, nonatomic) IBOutlet UIButton *undoButton;
@property (weak, nonatomic) IBOutlet ToolBarContainerView *toolbarRootView;
@property (weak, nonatomic) IBOutlet UIView *chevronView;
@property (weak, nonatomic) IBOutlet UIImageView *chevronImageView;
@property (weak, nonatomic) IBOutlet UIStackView *toolbarStackView;
@property (strong, nonatomic) OSCProfilesTableViewController *oscProfilesTableViewController;
@property (nonatomic, assign) NSString *currentProfileName;
@property (strong, nonatomic) IBOutlet UILabel *currentProfileLabel;
@property (weak, nonatomic) IBOutlet UILabel *widgetSizeLabel;
@property (weak, nonatomic) IBOutlet UISlider *widgetSizeSlider;
@property (weak, nonatomic) IBOutlet UIStackView *widgetSizeStack;
@property (weak, nonatomic) IBOutlet UILabel *widgetHeightLabel;
@property (weak, nonatomic) IBOutlet UISlider *widgetHeightSlider;
@property (weak, nonatomic) IBOutlet UIStackView *widgetHeightStack;
@property (weak, nonatomic) IBOutlet UILabel *widgetBorderWidthLabel;
@property (weak, nonatomic) IBOutlet UISlider *widgetBorderWidthSlider;
@property (weak, nonatomic) IBOutlet UILabel *widgetAlphaLabel;
@property (weak, nonatomic) IBOutlet UISlider *widgetAlphaSlider;
@property (weak, nonatomic) IBOutlet UIStackView *borderWidthAlphaStack;
@property (weak, nonatomic) IBOutlet UIButton *saveButton;
@property (weak, nonatomic) IBOutlet UIButton *exitButton;
@property (weak, nonatomic) IBOutlet UIButton *loadButton;
@property (weak, nonatomic) IBOutlet UIButton *addButton;
@property (weak, nonatomic) IBOutlet UIButton *editButton;
@property (weak, nonatomic) IBOutlet UILabel *stickIndicatorOffsetLabel;
@property (weak, nonatomic) IBOutlet UISlider *stickIndicatorOffsetSlider;
@property (weak, nonatomic) IBOutlet UIStackView *stickIndicatorOffsetStack;
@property (weak, nonatomic) IBOutlet UILabel *sensitivityLabel;
@property (weak, nonatomic) IBOutlet UISlider *sensitivitySlider;
@property (weak, nonatomic) IBOutlet UIStackView *sensitivityStack;
@property (strong, nonatomic) IBOutlet UISegmentedControl *vibrationStyleSelector;
@property (strong, nonatomic) IBOutlet UIStackView *vibrationStyleStack;
@property (strong, nonatomic) IBOutlet UILabel *loadConfigTipLabel;
@property (strong, nonatomic) IBOutlet UIStackView *mouseDownButtonStack;
@property (strong, nonatomic) IBOutlet UISegmentedControl *mouseButtonDownSelector;
@property (weak, nonatomic) IBOutlet UIStackView *widgetPanelStack;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,38 @@
//
// OSCProfilesTableViewController.h
// Moonlight
//
// Created by Long Le on 11/28/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 since 2024.6.24
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
This view displays a list of on screen controller profiles and gives the user the ability to select any of the profiles to be the 'Selected' profile whose on screen controller layout configuration will be shown on the game stream view, or in the on screen controller layout view. This view also allows the user to swipe and delete any of the listed profiles.
*/
@interface OSCProfilesTableViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, UIDocumentPickerDelegate>
- (void)profileViewRefresh;
typedef NS_ENUM(NSUInteger, FileOperation) {
Import,
Export
};
@property (nonatomic, assign) FileOperation currentFileOperation;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, copy) void (^needToUpdateOscLayoutTVC)(void);
@property (nonatomic, assign) NSMutableArray *currentOSCButtonLayers;
@property (nonatomic, assign) CGRect streamViewBounds;
@property (weak, nonatomic) IBOutlet UINavigationBar *profileTableViewNavigationBar;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,445 @@
//
// OSCProfilesTableViewController.m
// Moonlight
//
// Created by Long Le on 11/28/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 since 2024.6.24
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
//
#import "OSCProfilesTableViewController.h"
#import "LayoutOnScreenControlsViewController.h"
#import "ProfileTableViewCell.h"
#import "OSCProfile.h"
#import "OnScreenButtonState.h"
#import "OSCProfilesManager.h"
#import "LocalizationHelper.h"
const double NAV_BAR_HEIGHT = 50;
@interface OSCProfilesTableViewController ()
@end
@implementation OSCProfilesTableViewController {
OSCProfilesManager *profilesManager;
}
@synthesize tableView;
- (UIInterfaceOrientationMask)getCurrentOrientation{
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
//lock the orientation accordingly after streaming is started
if(screenWidthInPoints > screenHeightInPoints) return UIInterfaceOrientationMaskLandscape;
else return UIInterfaceOrientationMaskPortrait|UIInterfaceOrientationMaskPortraitUpsideDown;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
// Return the supported interface orientations acoordingly
return [self getCurrentOrientation]; // 90 Degree rotation not allowed in streaming or app view
}
- (void) viewDidDisappear:(BOOL)animated{
[[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutTableViewCloseNotification" object:self]; // notify other view that oscLayoutManager is closing
[super viewDidDisappear:animated];
}
- (void) viewDidLayoutSubviews {
CGRect bounds = self.profileTableViewNavigationBar.bounds;
CGSize cornerSize = CGSizeMake(15, 15);
// 创建一个 UIBezierPath,设置顶部圆角
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight) cornerRadii:cornerSize];
//byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight) // 只设置上半部分圆角
// cornerRadius:10]; // 圆角半径
// 创建一个 CAShapeLayer 并将圆角路径应用到该图层
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.path = path.CGPath;
self.profileTableViewNavigationBar.layer.masksToBounds = true; // 使圆角生效
self.profileTableViewNavigationBar.layer.mask = maskLayer;
self.tableView.layer.cornerRadius = 15; // 设置圆角半径
self.tableView.layer.masksToBounds = true; // 使圆角生效
}
- (void) viewDidLoad {
[super viewDidLoad];
// self.profileTableViewNavigationBar.layer.cornerRadius = 15; // 设置圆角半径
profilesManager = [OSCProfilesManager sharedManager:_streamViewBounds];
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, NAV_BAR_HEIGHT)];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self.tableView registerNib:[UINib nibWithNibName:@"ProfileTableViewCell" bundle:nil]
forCellReuseIdentifier:@"Cell"]; // Register the custom cell nib file with the table view
self.tableView.alpha = 0.7;
//self.tableView.backgroundColor = [[UIColor colorWithRed:0.5 green:0.7 blue:1.0 alpha:1.0] colorWithAlphaComponent:0.2]; // set background color & transparency
self.tableView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.1]; // set background color & transparency
// self.tableView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2]; // set background color & transparency
}
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear: animated];
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, NAV_BAR_HEIGHT)];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self.tableView registerNib:[UINib nibWithNibName:@"ProfileTableViewCell" bundle:nil]
forCellReuseIdentifier:@"Cell"]; // Register the custom cell nib file with the table view
if ([[profilesManager getAllProfiles] count] > 0) { // scroll to selected profile if user has any saved profiles
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[profilesManager getIndexOfSelectedProfile] inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
}
#pragma mark - UIButton Actions
/* Loads the OSC profile that user selected, dismisses this VC, then tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions */
- (IBAction) duplicateTapped:(id)sender {
UIAlertController * inputNameAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Enter the name you want to save this profile as"] message: @"" preferredStyle:UIAlertControllerStyleAlert];
[inputNameAlertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { // pop up notification with text field where user can enter the text they wish to name their OSC layout profile
textField.placeholder = [LocalizationHelper localizedStringForKey:@"name"];
textField.textColor = [UIColor lightGrayColor];
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
textField.borderStyle = UITextBorderStyleNone;
}];
[inputNameAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // adds a button that allows user to decline the option to save the controller layout they currently see on screen
[inputNameAlertController dismissViewControllerAnimated:NO completion:nil];
}]];
[inputNameAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Save"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // add save button to allow user to save the on screen controller configuration
NSArray *textFields = inputNameAlertController.textFields;
UITextField *nameField = textFields[0];
NSString *enteredProfileName = nameField.text;
if ([enteredProfileName isEqualToString:@"Default"]) { // don't let user user overwrite the 'Default' profile
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Saving over the 'Default' profile is not allowed"] preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[alertController dismissViewControllerAnimated:NO completion:^{
[self presentViewController:inputNameAlertController animated:YES completion:nil];
}];
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
else if ([enteredProfileName length] == 0) { // if user entered no text and taps the 'Save' button let them know they can't do that
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile name cannot be blank!"] preferredStyle:UIAlertControllerStyleAlert];
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // show pop up notification letting user know they must enter a name in the text field if they wish to save the controller profile
[savedAlertController dismissViewControllerAnimated:NO completion:^{
[self presentViewController:inputNameAlertController animated:YES completion:nil];
}];
}]];
[self presentViewController:savedAlertController animated:YES completion:nil];
}
else if ([self->profilesManager profileNameAlreadyExist:enteredProfileName] == YES) { // if the entered profile name already
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile name already exists"] preferredStyle:UIAlertControllerStyleAlert];
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
//[savedAlertController dismissViewControllerAnimated:NO completion:nil];
//[self profileViewRefresh]; //refresh profile view after saving new profile;
}]];
[self presentViewController:savedAlertController animated:YES completion:nil];
}
else { // if user entered a valid name that doesn't already exist then save the profile to persistent storage
[self->profilesManager saveProfileWithName: enteredProfileName andButtonLayers:self.currentOSCButtonLayers]; // the OSC layout here is passed from parent LayoutOSCViewController;
[self->profilesManager setProfileToSelected: enteredProfileName];
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile %@ duplicated from current layout", enteredProfileName] preferredStyle:UIAlertControllerStyleAlert]; // Let user know this profile has been duplicated & saved
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[savedAlertController dismissViewControllerAnimated:NO completion:nil];
[self profileViewRefresh]; //refresh profile view after saving new profile;
}]];
[self presentViewController:savedAlertController animated:YES completion:nil];
}
}]];
[self presentViewController:inputNameAlertController animated:YES completion:nil];
}
/* basically the same with loadTapped */
- (void) profileViewRefresh{
//[self dismissViewControllerAnimated:YES completion:nil];
//[selfparentLayoutOSCViewController]
[self.tableView reloadData]; // table view will be refreshed by calling reloadData
if (self.needToUpdateOscLayoutTVC) { // tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions
self.needToUpdateOscLayoutTVC();
}
[[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutProfileSelctedInTableView" object:self]; // notify other view that oscLayoutManager is closing
}
- (IBAction) deleteTapped:(id)sender{// delete can be executed simply by calling this 2 methods.
[profilesManager deleteCurrentSelectedProfile];
[self profileViewRefresh];
}
- (IBAction) exportDataTapped:(id)sender {
// 创建临时占位文件(仅用于提供文件名)
self.currentFileOperation = Export;
NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"profiles.bin"];
[[NSData new] writeToFile:tempPath atomically:YES]; // 空文件
// 初始化选择器
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithURL:[NSURL fileURLWithPath:tempPath] inMode:UIDocumentPickerModeExportToService];
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
}
- (IBAction) importDataTapped:(id)sender {
self.currentFileOperation = Import;
// 2. 创建文件选择器
// UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:supportedTypes inMode:UIDocumentPickerModeImport];
// UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.data"] inMode:UIDocumentPickerModeOpen];
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
documentPicker.delegate = self;
documentPicker.allowsMultipleSelection = NO; // 只允许选择一个文件
[self presentViewController:documentPicker animated:YES completion:nil];
}
- (IBAction) exitTapped:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
NSURL* url = urls.firstObject;
switch(self.currentFileOperation){
case Export:
[self profilesToFile:url];break;
case Import:
[self fileToProfiles:url];break;
default:break;
}
}
- (void)profilesToFile:(NSURL* )destinationURL{
// NSArray *profiles = [profilesManager getAllProfiles];
// 1. 序列化数据
NSError *error;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[profilesManager getEncodedProfiles] requiringSecureCoding:YES error:&error];
if (!data) {
NSLog(@"序列化失败: %@", error);
return;
}
// 2. 安全写入
[destinationURL startAccessingSecurityScopedResource];
BOOL success = [data writeToURL:destinationURL options:NSDataWritingAtomic error:&error];
[destinationURL stopAccessingSecurityScopedResource];
if (!success) {
NSLog(@"写入失败: %@", error);
}
}
- (void)fileToProfiles:(NSURL* )sourceURL{
bool restoreFailed = false;
if (![sourceURL startAccessingSecurityScopedResource]) {
restoreFailed = true;
}
NSError* error;
NSData* fileData = [NSData dataWithContentsOfURL:sourceURL options:0 error:&error];// 读取数据
[sourceURL stopAccessingSecurityScopedResource]; // 立即释放权限
if (!fileData) {
restoreFailed = true;
}
else NSLog(@"profile file read: %d", (uint32_t)fileData.length);
// 定义需要解码的类集
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
NSMutableArray *profilesEncoded;
// 解码原始的NSArray对象,得到包含编码对象的数组
error = nil;
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
restoreFailed = error != nil;
UIAlertController *restoredAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@""] message: [LocalizationHelper localizedStringForKey:@"Pofiles imported"] preferredStyle:UIAlertControllerStyleAlert];
UIAlertController *failedAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@""] message: [LocalizationHelper localizedStringForKey:@"Failed to import profiles"] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
style:UIAlertActionStyleDefault
handler:nil];
if(restoreFailed){
[failedAlertController addAction:okAction];
[self presentViewController:failedAlertController animated:YES completion:nil];
}
else{
[profilesManager importEncodedProfiles:profilesEncoded];
[restoredAlertController addAction:okAction];
[self presentViewController:restoredAlertController animated:YES completion:nil];
}
[self profileViewRefresh];
NSLog(@"profile test: %d", (uint32_t)profilesEncoded.count);
if (error) {
NSLog(@"解码失败: %@", error);
return;
}
}
#pragma mark - TableView DataSource
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[profilesManager getAllProfiles] count];
}
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ProfileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
OSCProfile *profile = [[profilesManager getAllProfiles] objectAtIndex:indexPath.row];
cell.name.text = profile.name;
cell.name.backgroundColor = [UIColor clearColor];
cell.name.alpha = 1.0;
cell.name.textColor = [UIColor whiteColor]; // Opaque white text
cell.name.font =[UIFont systemFontOfSize:20 weight:UIFontWeightMedium];
cell.name.shadowColor = [UIColor blackColor]; // Black shadow
// Set the shadow offset
cell.name.shadowOffset = CGSizeMake(1.0, 1.5);
// Set cell and contentView background colors
cell.backgroundColor = [UIColor clearColor];
cell.contentView.backgroundColor = [UIColor clearColor];
// Configure the checkmark accessory
if ([profile.name isEqualToString:[profilesManager getSelectedProfile].name]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
// Remove existing custom separators to avoid duplicates
UIView *existingSeparator = [cell.contentView viewWithTag:100];
if (existingSeparator) {
[existingSeparator removeFromSuperview];
}
// Add custom separator with increased height (thickness)
CGFloat separatorHeight = 1.0; // Adjust this value for thicker line
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(0, cell.contentView.frame.size.height - separatorHeight, cell.contentView.frame.size.width, separatorHeight)];
separatorView.backgroundColor = [UIColor whiteColor]; // Set your desired color
separatorView.tag = 100; // Assign a tag to identify the separator view
separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
[cell.contentView addSubview:separatorView];
[cell.contentView bringSubviewToFront:separatorView];
// Replace the default checkmark with a UILabel displaying a checkmark character
if ([profile.name isEqualToString:[profilesManager getSelectedProfile].name]) {
UILabel *checkmarkLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; // Adjust size as needed
checkmarkLabel.text = @"✓"; // The checkmark character
checkmarkLabel.font = [UIFont systemFontOfSize:25]; // Adjust font size as needed
checkmarkLabel.textAlignment = NSTextAlignmentCenter;
checkmarkLabel.textColor = [UIColor greenColor]; // Set the color of the checkmark
cell.accessoryView = checkmarkLabel;
checkmarkLabel.layer.zPosition = 0; // Push checkmark to the back
} else {
cell.accessoryView = nil; // Remove checkmark if not selected
}
[cell.contentView bringSubviewToFront:separatorView];
separatorView.layer.zPosition = 1; // Bring separator to the top layer
return cell;
}
- (BOOL) tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 60.0; // Set your desired cell height here
}
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
NSMutableArray *profiles = [profilesManager getAllProfiles];
if ([[[profiles objectAtIndex:indexPath.row] name] isEqualToString:@"Default"]) { // if user is attempting to delete the 'Default' profile then show a pop up telling user they can't do that and return out of this method
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: @"Deleting the 'Default' profile is not allowed" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[alertController dismissViewControllerAnimated:NO completion:nil];
}]];
[self presentViewController:alertController animated:YES completion:nil];
return;
}
if (editingStyle == UITableViewCellEditingStyleDelete) {
OSCProfile *profile = [profiles objectAtIndex:indexPath.row];
if (profile.isSelected) { // if user is deleting the currently selected OSC profile then make the profile at its previous index the currently selected profile
if (indexPath.row > 0) { // check that row is greater than zero to avoid an out of bounds crash, although that should not be possible right now since the 'Default' profile is always at row 0 and they're not allowed to delete it
OSCProfile *profile = [profiles objectAtIndex:indexPath.row - 1];
profile.isSelected = YES;
}
}
[profiles removeObjectAtIndex:indexPath.row];
/* save OSC profiles array to persistent storage */
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
for (OSCProfile *profileDecoded in profiles) { // encode each OSC profile object and add them to an array
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profileDecoded requiringSecureCoding:YES error:nil];
[profilesEncoded addObject:profileEncoded];
}
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded
requiringSecureCoding:YES error:nil]; // encode the array itself, NOT the objects in the array
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
[tableView reloadData];
}
}
#pragma mark - TableView Delegate
/* When user taps a cell it moves the checkmark to that cell indicating to the user the profile associated with that cell is now the selected profile. It also sets that cell's associated OSCProfile object's 'isSelected' property to YES */
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath *selectedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
NSIndexPath *lastSelectedIndexPath = [NSIndexPath indexPathForRow:[profilesManager getIndexOfSelectedProfile] inSection:0];
if (selectedIndexPath != lastSelectedIndexPath) {
/* Place checkmark on selected cell and set profile associated with cell as selected profile */
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath: selectedIndexPath];
selectedCell.accessoryType = UITableViewCellAccessoryCheckmark; // add checkmark to the cell the user tapped
selectedCell.accessoryView.tintColor = [[UIColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:0.85] colorWithAlphaComponent:1.0];
OSCProfile *profile = [[profilesManager getAllProfiles] objectAtIndex:indexPath.row];
[profilesManager setProfileToSelected: profile.name]; // set the profile associated with this cell's 'isSelected' property to YES
/* Remove checkmark on the previously selected cell */
UITableViewCell *lastSelectedCell = [tableView cellForRowAtIndexPath: lastSelectedIndexPath];
lastSelectedCell.accessoryType = UITableViewCellAccessoryNone;
[tableView deselectRowAtIndexPath:lastSelectedIndexPath animated:YES];
}
[self profileViewRefresh]; // update OSC layout when table view option is changed
}
@end
@@ -0,0 +1,18 @@
//
// ToolBarContainerView.h
// Moonlight
//
// Created by Long Le on 12/10/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/* Allows the ToolBar in the 'LayoutOnScreenControlsViewController' to accept touches on the small UIView 'tab' protruding from its bottom center*/
@interface ToolBarContainerView : UIView
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,24 @@
//
// ToolBarContainerView.m
// Moonlight
//
// Created by Long Le on 12/10/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import "ToolBarContainerView.h"
@implementation ToolBarContainerView
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *view in self.subviews) {
if (!view.hidden && view.alpha > 0 &&
view.userInteractionEnabled &&
[view pointInside:[self convertPoint:point toView:view] withEvent:event])
return YES;
}
return NO;
}
@end
@@ -0,0 +1,14 @@
//
// WidgetPanelStackView.h
// VoidLink
//
// Created by True砖家 on 2025/6/19.
// Copyright © True砖家 on Bilibili. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface WidgetPanelStackView : UIStackView
@end
@@ -0,0 +1,26 @@
//
// WidgetPanelStackView.h
// VoidLink
//
// Created by True砖家 on 2025/6/19.
// Copyright © True砖家 on Bilibili. All rights reserved.
//
#import "WidgetPanelStackView.h"
@implementation WidgetPanelStackView
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPhone) return [super pointInside:point withEvent:event];
BOOL touchedWithinMask;
CAShapeLayer *mask = (CAShapeLayer *)self.layer.mask;
if (mask && mask.path) {
// 注意:mask 的坐标系是以 view.layer 为基准的
touchedWithinMask = CGPathContainsPoint(mask.path, NULL, point, NO);
}
else touchedWithinMask = YES;
return touchedWithinMask;
}
@end
@@ -0,0 +1,42 @@
//
// 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;
/// 移除最后一个 item(如果有)
- (void)removeLastItem;
@end
@interface HostCell : UICollectionViewCell
@property (nonatomic, strong) HostCardView *cardView;
- (void)configureWithHost:(TemporaryHost *)host;
@end
@@ -0,0 +1,217 @@
//
// 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 "ThemeManager.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];
[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;
@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)viewDidLoad {
[super viewDidLoad];
[self.collectionView registerClass:[HostCell class] forCellWithReuseIdentifier:@"HostCell"];
self.collectionView.alwaysBounceVertical = NO;
self.collectionView.showsVerticalScrollIndicator = NO;
self.collectionView.backgroundColor = [ThemeManager appBackgroundColor];
}
#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){
[NSLayoutConstraint activateConstraints:@[
[self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.safeAreaLayoutGuide.bottomAnchor constant:0]
]];
}
else{
_collectionViewHeightConstraint.constant = contentHeight;
}
if([self numberOfRowsInCollectionView]<=2 && !contentExceedsView) layout.sectionInset = UIEdgeInsetsMake(50, _horizontalPadding, 0, _horizontalPadding);
if([self numberOfRowsInCollectionView]>2 && !contentExceedsView) layout.sectionInset = UIEdgeInsetsMake(25, _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,20 @@
//
// LoadingFrameViewController.h
// Moonlight
//
// Created by Diego Waxemberg on 2/24/15.
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface LoadingFrameViewController : UIViewController
- (void)showLoadingFrame:(void (^)(void))completion;
- (void)dismissLoadingFrame:(void (^)(void))completion;
- (BOOL)isShown;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingSpinner;
@end
@@ -0,0 +1,83 @@
//
// LoadingFrameViewController.m
// Moonlight
//
// Created by Diego Waxemberg on 2/24/15.
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import "LoadingFrameViewController.h"
@implementation LoadingFrameViewController {
BOOL presented;
};
- (void)viewDidLoad {
[super viewDidLoad];
// center the loading spinner
self.loadingSpinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
}
- (UIViewController*) activeViewController {
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
- (void)showLoadingFrame:(void (^)(void))completion {
if (!presented) {
Log(LOG_I, @"Loading frame presenting start");
presented = YES;
[[self activeViewController] presentViewController:self animated:NO completion:^{
Log(LOG_I, @"Loading frame presenting complete");
if (completion) {
completion();
}
}];
}
else if (completion) {
Log(LOG_E, @"Loading frame already shown!");
completion();
}
}
- (void)dismissLoadingFrame:(void (^)(void))completion {
if (presented) {
Log(LOG_I, @"Loading frame hiding start");
[self dismissViewControllerAnimated:NO completion:^{
Log(LOG_I, @"Loading frame hiding complete");
// Since presented is set to NO here rather than
// immediately in dismissLoadingFrame, we may
// falsely avoid displaying the loading frame if
// a dismiss is in progress while attempting to show
// the frame. That's preferable to crashing due to
// displaying the same VC twice though.
//
// This scenario can happen if the app is suspended
// while the dismiss is in progress then on resume
// it attempts to display it again before the dismiss
// completes. It can be reproduced by rapidly pressing
// Home and switching back to Moonlight while in the app grid.
// It reproduces more easily if the VC transitions are animated.
self->presented = NO;
if (completion) {
completion();
}
}];
}
else if (completion) {
completion();
}
}
- (BOOL)isShown {
return presented;
}
@end
@@ -0,0 +1,39 @@
//
// MainFrameViewController.h
// Moonlight
//
// Created by Diego Waxemberg on 1/17/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
// Modified by True砖家 since 2024.7.7
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "DiscoveryManager.h"
#import "PairManager.h"
#import "StreamConfiguration.h"
#import "UIComputerView.h"
#import "HostCardView.h"
#import "UIAppView.h"
#import "AppAssetManager.h"
#import "SWRevealViewController.h"
#import "HostCollectionViewController.h"
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate, HostCardActionDelegate>
@property (nonatomic, strong) IBOutlet UIBarButtonItem *settingsButton;
#if !TARGET_OS_TV
@property (nonatomic, assign) bool settingsExpandedInStreamView;
@property (nonatomic, strong) HostCollectionViewController *hostCollectionVC;
@property (weak, nonatomic) IBOutlet UINavigationItem *navigationItem;
-(void)expandSettingsView;
- (void)closeSettingViewAnimated:(BOOL)anaimated;
-(void)reloadStreamConfig;
-(bool)isIPhonePortrait;
#endif
@end
File diff suppressed because it is too large Load Diff
+455
View File
@@ -0,0 +1,455 @@
// Modified by True砖家 since 2024.7.13
// Copyright © 2024 True砖家. All rights reserved.
/*
Copyright (c) 2013 Joan Lluch <joan.lluch@sweetwilliamsl.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Early code inspired on a similar class by Philip Kluz (Philip.Kluz@zuui.org)
*/
/*
RELEASE NOTES
Version 2.3.0 (Current Version)
- StoryBoard initializing bug fix
- Minor Code refactoring
Version 2.2.0
- State Restoration support.
- Reverted panGestureRecognizer implementation to before v2.1.0 (works better).
- New properties 'toggleAnimationType', 'springDampingRatio'. Default reveal animation is 'Spring'
- New property 'frontViewShadowColor'
- New properties 'clipsViewsToBounds' and '_extendedPointInsideHit'
- New delegate methods for finer control of front view location in the overdraw area, as long as deprecation note on former delegate methods
- Other minor changes that should not affect current implementations
Version 2.1.0
- Removed SWDirectionPanGestureRecognizer. Horizontal panning is filtered on the shouldBegin delegate. This is cleaner, I hope it does not break previous funcionality
- Took a cleaner approach to storyboard support. SWRevealViewControllerSegue is now deprecated and you should use SWRevealViewControllerSegueSetController and SWRevealViewControllerSeguePushController instead.
- A minor change on the autoresizingMask of the internal views to fix a glitch on iOS8. This should not affect iOS7
Version 2.0.2
- Added new delegates for better control of gesture recognizers
Version 2.0.1
- Fix: draggableBorderWidth now correctly handles the cases where one of the rear controllers is not provided
- Fix: the shadow related properties are now granted at any time after view load, not just after initialization.
Version 2.0.0
- Dropped support for iOS6 and earlier. This version will only work on iOS7
- The method setFrontViewController:animated: does not longer perform a full reveal animation. Instead it just replaces the frontViewController in
its current position. Use the new pushFrontViewController:animated: method to perform a replacement of the front controlles with reveal animation
as in the previous version
IMPORTANT: You must replace all calls to setFrontViewController:animated by calls to pushFrontViewController:animated to prevent breaking
functionality on existing projects.
- Added support for animated replacement of child controllers: setRearViewController, setFrontViewController, setRightViewController now have animated versions.
- The new 'replaceViewAnimationDuration' property sets the default duration of child viewController replacement.
- Added the following new delegate methods
revealController:willAddViewController:forOperation:animated:
revealController:didAddViewController:forOperation:animated:
- The class also supports custom UIViewControllerAnimatedTransitioning related with the replacement of child viewControllers.
You can implement the following new delegate method: revealController:animationControllerForOperation:fromViewController:toViewController:
and provide an object conforming to UIViewControllerAnimatedTransitioning to implement custom animations.
Version 1.1.3
- Reverted the supportedInterfaceOrientations to the default behavior. This is consistent with Apple provided controllers
- The presentFrontViewHierarchically now dynamically takes into account the smaller header height of bars on iPhone landscape orientation
Version 1.1.2
- The status bar style and appearance are now handled in sync with the class animations.
You can implement the methods preferredStatusBarStyle and prefersStatusBarHidden on your child controllers to define the desired appearance
- The loadView method now calls a method, loadStoryboardControllers, just for the purpose of loading child controllers from a storyboard.
You can override this method and remove the @try @catch statements if you want the debugger not to stop at them in case you have set an exception breakpoint.
Version 1.1.1
- You can now get a tapGestureRecognizer from the class. See the tapGestureRecognizer method for more information.
- Both the panGestureRecognizer and the tapGestureRecognizer are now attached to the revealViewController's front content view
by default, so they will start working just by calling their access methods even if you do not attach them to any of your views.
This enables you to dissable interactions on your views -for example based on position- without breaking normal gesture behavior.
- Corrected a bug that caused a crash on iOS6 and earlier.
Version 1.1.0
- The method setFrontViewController:animated now performs the correct animations both for left and right controllers.
- The class now automatically handles the status bar appearance depending on the currently shown child controller.
Version 1.0.8
- Support for constant width frontView by setting a negative value to reveal widths. See properties rearViewRevealWidth and rightViewRevealWidth
- Support for draggableBorderWidth. See property of the same name.
- The Pan gesture recongnizer can be disabled by implementing the following delegate method and returning NO
revealControllerPanGestureShouldBegin:
- Added the ability to track pan gesture reveal progress through the following new delegate methods
revealController:panGestureBeganFromLocation:progress:
revealController:panGestureMovedToLocation:progress:
revealController:panGestureEndedToLocation:progress:
Previous Versions
- No release notes were updated for previous versions.
*/
#import <UIKit/UIKit.h>
#import "DataManager.h"
@class SWRevealViewController;
@protocol SWRevealViewControllerDelegate;
@protocol RearNavigationBarMenuDelegate;
#pragma mark - SWRevealViewController Class
// Enum values for setFrontViewPosition:animated:
typedef NS_ENUM( NSInteger, FrontViewPosition)
{
// Front controller is removed from view. Animated transitioning from this state will cause the same
// effect than animating from FrontViewPositionLeftSideMost. Use this instead of FrontViewPositionLeftSideMost when
// you want to remove the front view controller view from the view hierarchy.
FrontViewPositionLeftSideMostRemoved,
// Left most position, front view is presented left-offseted by rightViewRevealWidth+rigthViewRevealOverdraw
FrontViewPositionLeftSideMost,
// Left position, front view is presented left-offseted by rightViewRevealWidth
FrontViewPositionLeftSide,
// Center position, rear view is hidden behind front controller
FrontViewPositionLeft,
// Right possition, front view is presented right-offseted by rearViewRevealWidth
FrontViewPositionRight,
// Right most possition, front view is presented right-offseted by rearViewRevealWidth+rearViewRevealOverdraw
FrontViewPositionRightMost,
// Front controller is removed from view. Animated transitioning from this state will cause the same
// effect than animating from FrontViewPositionRightMost. Use this instead of FrontViewPositionRightMost when
// you intent to remove the front controller view from the view hierarchy.
FrontViewPositionRightMostRemoved,
};
// Enum values for toggleAnimationType
typedef NS_ENUM(NSInteger, SWRevealToggleAnimationType)
{
SWRevealToggleAnimationTypeSpring, // <- produces a spring based animation
SWRevealToggleAnimationTypeEaseOut, // <- produces an ease out curve animation
};
@interface SWRevealViewController : UIViewController
@property (nonatomic, strong) UINavigationBar *dockedNavBar;
- (void)setPrimaryViewController:(UIViewController*)viewController;
/* Basic API */
// Object instance init and rear view setting
- (id)initWithRearViewController:(UIViewController *)rearViewController frontViewController:(UIViewController *)frontViewController;
// Rear view controller, can be nil if not used
@property (nonatomic) UIViewController *rearViewController;
- (void)setRearViewController:(UIViewController *)rearViewController animated:(BOOL)animated;
// Optional right view controller, can be nil if not used
@property (nonatomic) UIViewController *rightViewController;
- (void)setRightViewController:(UIViewController *)rightViewController animated:(BOOL)animated;
// Front view controller, can be nil on initialization but must be supplied by the time the view is loaded
@property (nonatomic) UIViewController *frontViewController;
- (void)setFrontViewController:(UIViewController *)frontViewController animated:(BOOL)animated;
// Sets the frontViewController using a default set of chained animations consisting on moving the
// presented frontViewController to the right most possition, replacing it, and moving it back to the left position
- (void)pushFrontViewController:(UIViewController *)frontViewController animated:(BOOL)animated;
// Sets the frontViewController position. You can call the animated version several times with different
// positions to obtain a set of animations that will be performed in order one after the other.
@property (nonatomic) FrontViewPosition frontViewPosition;
- (void)setFrontViewPosition:(FrontViewPosition)frontViewPosition animated:(BOOL)animated;
// The following methods are meant to be directly connected to the action method of a button
// to perform user triggered postion change of the controller views. This is ussually added to a
// button on top left or right of the frontViewController
- (IBAction)revealToggle:(id)sender;
- (IBAction)rightRevealToggle:(id)sender; // <-- simetric implementation of the above for the rightViewController
// Toogles the current state of the front controller between Left or Right and fully visible
// Use setFrontViewPosition to set a particular position
- (void)revealToggleAnimated:(BOOL)animated;
- (void)rightRevealToggleAnimated:(BOOL)animated; // <-- simetric implementation of the above for the rightViewController
// The following method will provide a panGestureRecognizer suitable to be added to any view
// in order to perform usual drag and swipe gestures to reveal the rear views. This is usually added to the top bar
// of a front controller, but it can be added to your frontViewController view or to the reveal controller view to provide full screen panning.
// By default, the panGestureRecognizer is added to the view containing the front controller view. To keep this default behavior
// you still need to call this method, just don't add it to any of your views. The default setup allows you to dissable
// user interactions on your controller views without affecting the recognizer.
- (UIPanGestureRecognizer*)panGestureRecognizer;
// The following method will provide a tapGestureRecognizer suitable to be added to any view on the frontController
// for concealing the rear views. By default no tap recognizer is created or added to any view, however if you call this method after
// the controller's view has been loaded the recognizer is added to the reveal controller's front container view.
// Thus, you can disable user interactions on your frontViewController view without affecting the tap recognizer.
- (UITapGestureRecognizer*)tapGestureRecognizer;
/* The following properties are provided for further customization, they are set to default values on initialization,
you do not generally have to set them */
@property (nonatomic, strong) UIBarButtonItem* disconnectButton;
@property (nonatomic, strong) UIBarButtonItem* moreButton;
// Defines how much of the rear or right view is shown, default is 260.
// Negative values indicate that the reveal width should be computed by substracting the full front view width,
// so the revealed frontView width is kept constant when bounds change as opposed to the rear or right width.
@property (nonatomic) CGFloat rearViewRevealWidth;
@property (nonatomic) CGFloat rightViewRevealWidth; // <-- simetric implementation of the above for the rightViewController
// Defines how much of an overdraw can occur when dragging further than 'rearViewRevealWidth', default is 60.
@property (nonatomic) CGFloat rearViewRevealOverdraw;
@property (nonatomic) CGFloat rightViewRevealOverdraw; // <-- simetric implementation of the above for the rightViewController
// Defines how much displacement is applied to the rear view when animating or dragging the front view, default is 40.
@property (nonatomic) CGFloat rearViewRevealDisplacement;
@property (nonatomic) CGFloat rightViewRevealDisplacement; // <-- simetric implementation of the above for the rightViewController
// Defines a width on the border of the view attached to the panGesturRecognizer where the gesture is allowed,
// default is 0 which means no restriction.
@property (nonatomic) CGFloat draggableBorderWidth;
// If YES (the default) the controller will bounce to the Left position when dragging further than 'rearViewRevealWidth'
@property (nonatomic) BOOL bounceBackOnOverdraw;
@property (nonatomic) BOOL bounceBackOnLeftOverdraw; // <-- simetric implementation of the above for the rightViewController
// If YES (default is NO) the controller will allow permanent dragging up to the rightMostPosition
@property (nonatomic) BOOL stableDragOnOverdraw;
@property (nonatomic) BOOL stableDragOnLeftOverdraw; // <-- simetric implementation of the above for the rightViewController
// If YES (default is NO) the front view controller will be ofsseted vertically by the height of a navigation bar.
// Use this on iOS7 when you add an instance of RevealViewController as a child of a UINavigationController (or another SWRevealViewController)
// and you want the front view controller to be presented below the navigation bar of its UINavigationController grand parent.
// The rearViewController will still appear full size and blurred behind the navigation bar of its UINavigationController grand parent
@property (nonatomic) BOOL presentFrontViewHierarchically;
// Velocity required for the controller to toggle its state based on a swipe movement, default is 250
@property (nonatomic) CGFloat quickFlickVelocity;
// Duration for the revealToggle animation, default is 0.25
@property (nonatomic) NSTimeInterval toggleAnimationDuration;
// Animation type, default is SWRevealToggleAnimationTypeSpring
@property (nonatomic) SWRevealToggleAnimationType toggleAnimationType;
// When animation type is SWRevealToggleAnimationTypeSpring determines the damping ratio, default is 1
@property (nonatomic) CGFloat springDampingRatio;
// Duration for animated replacement of view controllers
@property (nonatomic) NSTimeInterval replaceViewAnimationDuration;
// Defines the radius of the front view's shadow, default is 2.5f
@property (nonatomic) CGFloat frontViewShadowRadius;
// Defines the radius of the front view's shadow offset default is {0.0f,2.5f}
@property (nonatomic) CGSize frontViewShadowOffset;
// Defines the front view's shadow opacity, default is 1.0f
@property (nonatomic) CGFloat frontViewShadowOpacity;
// Defines the front view's shadow color, default is blackColor
@property (nonatomic) UIColor *frontViewShadowColor;
// Defines whether the controller should clip subviews to its view bounds. Default is NO.
// Set this to YES when you are presenting this controller as a non full-screen child of a
// custom container controller which does not explicitly clips its subviews.
@property (nonatomic) BOOL clipsViewsToBounds;
// Defines whether your views clicable area extends beyond the bounds of this controller. Default is NO.
// Set this to YES if you are presenting this controller as a non full-screen child of a custom container and you are not
// clipping your front view to this controller bounds.
@property (nonatomic) BOOL extendsPointInsideHit;
/* The class properly handles all the relevant calls to appearance methods on the contained controllers.
Moreover you can assign a delegate to let the class inform you on positions and animation activity */
// Delegate
@property (nonatomic,weak) id<SWRevealViewControllerDelegate> delegate;
@property (nonatomic,weak) id<RearNavigationBarMenuDelegate> navBarMenuDelegate;
@property (nonatomic,assign) bool isStreaming;
@property (nonatomic,assign) bool mainFrameIsInHostView;
- (void)buttonsInStreaming;
- (void)buttonsNotInStreaming;
- (void)setupNavigationBar;
@end
#pragma mark - SWRevealViewControllerDelegate Protocol
typedef enum
{
SWRevealControllerOperationNone,
SWRevealControllerOperationReplaceRearController,
SWRevealControllerOperationReplaceFrontController,
SWRevealControllerOperationReplaceRightController,
} SWRevealControllerOperation;
#pragma mark - RearNavigationBarMenuDelegate Protocol
@protocol RearNavigationBarMenuDelegate<NSObject>
@optional
- (void)switchToFavoriteSettings;
- (void)switchToAllSettings;
- (SettingsMenuMode)getSettingsMenuMode;
- (void)enterRemoveSettingItemMode;
- (void)doneRemoveSettingItem;
- (void)layoutSettingsView;
@end
@protocol SWRevealViewControllerDelegate<NSObject>
@optional
// The following delegate methods will be called before and after the front view moves to a position
- (void)revealController:(SWRevealViewController *)revealController willMoveToPosition:(FrontViewPosition)position;
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position;
// This will be called inside the reveal animation, thus you can use it to place your own code that will be animated in sync
- (void)revealController:(SWRevealViewController *)revealController animateToPosition:(FrontViewPosition)position;
// Implement this to return NO when you want the pan gesture recognizer to be ignored
- (BOOL)revealControllerPanGestureShouldBegin:(SWRevealViewController *)revealController;
// Implement this to return NO when you want the tap gesture recognizer to be ignored
- (BOOL)revealControllerTapGestureShouldBegin:(SWRevealViewController *)revealController;
// Implement this to return YES if you want other gesture recognizer to share touch events with the pan gesture
- (BOOL)revealController:(SWRevealViewController *)revealController
panGestureRecognizerShouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
// Implement this to return YES if you want other gesture recognizer to share touch events with the tap gesture
- (BOOL)revealController:(SWRevealViewController *)revealController
tapGestureRecognizerShouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
// Called when the gestureRecognizer began and ended
- (void)revealControllerPanGestureBegan:(SWRevealViewController *)revealController;
- (void)revealControllerPanGestureEnded:(SWRevealViewController *)revealController;
// The following methods provide a means to track the evolution of the gesture recognizer.
// The 'location' parameter is the X origin coordinate of the front view as the user drags it
// The 'progress' parameter is a number ranging from 0 to 1 indicating the front view location relative to the
// rearRevealWidth or rightRevealWidth. 1 is fully revealed, dragging ocurring in the overDraw region will result in values above 1.
// The 'overProgress' parameter is a number ranging from 0 to 1 indicating the front view location relative to the
// overdraw region. 0 is fully revealed, 1 is fully overdrawn. Negative values occur inside the normal reveal region
- (void)revealController:(SWRevealViewController *)revealController panGestureBeganFromLocation:(CGFloat)location progress:(CGFloat)progress overProgress:(CGFloat)overProgress;
- (void)revealController:(SWRevealViewController *)revealController panGestureMovedToLocation:(CGFloat)location progress:(CGFloat)progress overProgress:(CGFloat)overProgress;
- (void)revealController:(SWRevealViewController *)revealController panGestureEndedToLocation:(CGFloat)location progress:(CGFloat)progress overProgress:(CGFloat)overProgress;
// Notification of child controller replacement
- (void)revealController:(SWRevealViewController *)revealController willAddViewController:(UIViewController *)viewController
forOperation:(SWRevealControllerOperation)operation animated:(BOOL)animated;
- (void)revealController:(SWRevealViewController *)revealController didAddViewController:(UIViewController *)viewController
forOperation:(SWRevealControllerOperation)operation animated:(BOOL)animated;
// Support for custom transition animations while replacing child controllers. If implemented, it will be fired in response
// to calls to 'setXXViewController' methods
- (id<UIViewControllerAnimatedTransitioning>)revealController:(SWRevealViewController *)revealController
animationControllerForOperation:(SWRevealControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC;
// DEPRECATED - The following delegate methods will be removed some time in the future
- (void)revealController:(SWRevealViewController *)revealController panGestureBeganFromLocation:(CGFloat)location progress:(CGFloat)progress; // (DEPRECATED)
- (void)revealController:(SWRevealViewController *)revealController panGestureMovedToLocation:(CGFloat)location progress:(CGFloat)progress; // (DEPRECATED)
- (void)revealController:(SWRevealViewController *)revealController panGestureEndedToLocation:(CGFloat)location progress:(CGFloat)progress; // (DEPRECATED)
@end
#pragma mark - UIViewController(SWRevealViewController) Category
// A category of UIViewController to let childViewControllers easily access their parent SWRevealViewController
@interface UIViewController(SWRevealViewController)
- (SWRevealViewController*)revealViewController;
@end
#pragma mark - StoryBoard support Classes
/* StoryBoard support */
// String identifiers to be applied to segues on a storyboard
extern NSString* const SWSegueRearIdentifier; // this is @"sw_rear"
extern NSString* const SWSegueFrontIdentifier; // this is @"sw_front"
extern NSString* const SWSegueRightIdentifier; // this is @"sw_right"
/* This will allow the class to be defined on a storyboard */
// Use this along with one of the above segue identifiers to segue to the initial state
@interface SWRevealViewControllerSegueSetController : UIStoryboardSegue
@end
// Use this to push a view controller
@interface SWRevealViewControllerSeguePushController : UIStoryboardSegue
@end
//#pragma mark - SWRevealViewControllerSegue (DEPRECATED)
//
//@interface SWRevealViewControllerSegue : UIStoryboardSegue // DEPRECATED: USE SWRevealViewControllerSegueSetController instead
//@property (nonatomic, strong) void(^performBlock)( SWRevealViewControllerSegue* segue, UIViewController* svc, UIViewController* dvc );
//@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
//
// SettingsViewController.h
// Moonlight
//
// Created by Diego Waxemberg on 10/27/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
// Modified by True砖家 since 2024.6.1
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "CustomOSCViewControl/LayoutOnScreenControlsViewController.h"
#import "MainFrameViewController.h"
#import "CustomEdgeSlideGestureRecognizer.h"
#import "MenuSectionView.h"
@interface SettingsViewController : UIViewController <RearNavigationBarMenuDelegate, MenuSectionDelegate>
@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar;
@property (strong, nonatomic) UIStackView *parentStack;
@property (strong, nonatomic) IBOutlet UIStackView *resolutionStack;
@property (strong, nonatomic) IBOutlet UIStackView *fpsStack;
@property (strong, nonatomic) IBOutlet UIStackView *bitrateStack;
@property (strong, nonatomic) IBOutlet UIStackView *touchModeStack;
@property (strong, nonatomic) IBOutlet UIStackView *enableOswSwitchStack;
//@property (strong, nonatomic) IBOutlet UIStackView *asyncTouchStack;
@property (strong, nonatomic) IBOutlet UISwitch *optimizeGamesSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *pointerVelocityDividerStack;
@property (strong, nonatomic) IBOutlet UIStackView *pointerVelocityFactorStack;
@property (strong, nonatomic) IBOutlet UIStackView *touchMoveEventIntervalStack;
@property (strong, nonatomic) IBOutlet UIStackView *mousePointerVelocityStack;
@property (strong, nonatomic) IBOutlet UIStackView *onScreenWidgetStack;
@property (strong, nonatomic) IBOutlet UIStackView *softKeyboardGestureStack;
@property (strong, nonatomic) IBOutlet UISegmentedControl *softKeyboardGestureSelector;
@property (strong, nonatomic) IBOutlet UIStackView *liftStreamViewForKeyboardStack;
@property (strong, nonatomic) IBOutlet UIStackView *softKeyboardToolbarStack;
@property (strong, nonatomic) IBOutlet UISwitch *softKeyboardToolbarSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *slideToSettingsScreenEdgeStack;
@property (strong, nonatomic) IBOutlet UIStackView *slideToToolboxScreenEdgeStack;
@property (strong, nonatomic) IBOutlet UIStackView *slideToSettingsDistanceStack;
@property (strong, nonatomic) IBOutlet UIStackView *optimizeGamesStack;
@property (strong, nonatomic) IBOutlet UIStackView *multiControllerStack;
@property (strong, nonatomic) IBOutlet UISwitch *multiControllerSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *swapAbaxyStack;
@property (strong, nonatomic) IBOutlet UIStackView *audioOnPcStack;
@property (strong, nonatomic) IBOutlet UISwitch *audioOnPcSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *codecStack;
@property (strong, nonatomic) IBOutlet UIStackView *yuv444Stack;
@property (strong, nonatomic) IBOutlet UIStackView *HdrStack;
@property (strong, nonatomic) IBOutlet UIStackView *framepacingStack;
@property (strong, nonatomic) IBOutlet UIStackView *reverseMouseWheelDirectionStack;
@property (strong, nonatomic) IBOutlet UIStackView *citrixX1MouseStack;
@property (strong, nonatomic) IBOutlet UISwitch *citrixX1MouseSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *statsOverlayStack;
@property (strong, nonatomic) IBOutlet UIStackView *unlockDisplayOrientationStack;
@property (strong, nonatomic) IBOutlet UIStackView *externalDisplayModeStack;
@property (strong, nonatomic) IBOutlet UIStackView *localMousePointerModeStack;
@property (strong, nonatomic) IBOutlet UILabel *bitrateLabel;
@property (strong, nonatomic) IBOutlet UISlider *bitrateSlider;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
@property (strong, nonatomic) IBOutlet UISwitch *customResolutionSwitch;
@property (strong, nonatomic) IBOutlet UILabel *touchModeLabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
@property (strong, nonatomic) IBOutlet UISwitch *enableOswForNativeTouchSwitch;
@property (strong, nonatomic) IBOutlet UILabel *onscreenControllerLabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *onScreenWidgetSelector;
//@property (strong, nonatomic) IBOutlet UISegmentedControl *asyncNativeTouchPrioritySelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *swapABXYButtonsSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
@property (strong, nonatomic) IBOutlet UISwitch *hdrSwitch;
@property (strong, nonatomic) IBOutlet UISwitch *yuv444Switch;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
@property (strong, nonatomic) IBOutlet UISegmentedControl *liftStreamViewForKeyboardSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *slideToSettingsScreenEdgeSelector;
@property (strong, nonatomic) IBOutlet UILabel *touchMoveEventIntervalLabel;
@property (strong, nonatomic) IBOutlet UISlider *touchMoveEventIntervalSlider;
@property (strong, nonatomic) IBOutlet UILabel *slideToSettingsScreenEdgeUILabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *slideToToolboxScreenEdgeSelector;
@property (strong, nonatomic) IBOutlet UILabel *slideToSettingsDistanceUILabel;
@property (strong, nonatomic) IBOutlet UISlider *slideToMenuDistanceSlider;
@property (strong, nonatomic) IBOutlet UISlider *pointerVelocityModeDividerSlider;
@property (strong, nonatomic) IBOutlet UILabel *pointerVelocityModeDividerUILabel;
@property (strong, nonatomic) IBOutlet UISlider *touchPointerVelocityFactorSlider;
@property (strong, nonatomic) IBOutlet UILabel *touchPointerVelocityFactorUILabel;
@property (strong, nonatomic) IBOutlet UISlider *mousePointerVelocityFactorSlider;
@property (strong, nonatomic) IBOutlet UILabel *mousePointerVelocityFactorUILabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *unlockDisplayOrientationSelector;
@property (strong, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC;
@property (nonatomic, strong) MainFrameViewController *mainFrameViewController;
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
// This is okay because it's just an enum and access uses @available checks
@property(nonatomic) UIUserInterfaceStyle overrideUserInterfaceStyle;
#pragma clang diagnostic pop
- (void)saveSettings;
+ (bool)isLandscapeNow;
- (void)updateResolutionTable;
- (void)widget:(UISlider*)widget setEnabled:(bool)enabled;
- (void)updateTheme;
- (void)hideDynamicLabelsWhenOverlapped:(UIView* )view;
- (void)setHidden:(BOOL)hidden forStack:(UIStackView* )stack;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
//
// StreamFrameViewController.h
// Moonlight
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
// Modified by True砖家 since 2024.6.26
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import "Connection.h"
#import "StreamConfiguration.h"
#import "StreamView.h"
#import "LayoutOnScreenControlsViewController.h"
#import "MainFrameViewController.h"
#import <UIKit/UIKit.h>
#if TARGET_OS_TV
@import GameController;
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
#else
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, ToolBoxSpecialEntryDelegate>
#endif
@property (nonatomic) StreamConfiguration* streamConfig;
@property (nonatomic, assign) MainFrameViewController *mainFrameViewcontroller;
-(void)updatePreferredDisplayMode:(BOOL)streamActive;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,384 @@
//
// KeyManagerViewController.swift
// VoidLink
//
// Created by True砖家 on 2024/7/23.
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc protocol ToolBoxSpecialEntryDelegate: NSObjectProtocol {
@objc optional func openWidgetLayoutTool()
@objc optional func switchWidgetProfile()
}
@objc public class ToolBoxViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@objc weak var specialEntryDelegate: ToolBoxSpecialEntryDelegate?
public let tableView = UITableView()
private let addButton = UIButton(type: .system)
private let deleteButton = UIButton(type: .system)
private let editButton = UIButton(type: .system)
private let exitButton = UIButton(type: .system)
private let pinButton = UIButton(type: .system)
private let viewBackgroundColor = UIColor(white: 0.2, alpha: 0.8);
private let highlightColor = UIColor(white: 0.39, alpha: 0.8);
private let titleLabel = UILabel()
@objc public var specialEntries : NSMutableArray = ["widgetLayoutTool"]
private let specialEntryAliasDic : [String:String] = [
"widgetSwitchTool":SwiftLocalizationHelper.localizedString(forKey: "[ Switch among on-screen widget profiles ]"),
"widgetLayoutTool":SwiftLocalizationHelper.localizedString(forKey: "[ Open on-screen widget tool ]")
]
private var viewPinned: Bool = false
private var isEditingMode: Bool = false {
didSet {
updateEditingMode()
}
}
override public func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateEditingMode()
tableView.dataSource = self
tableView.delegate = self
// Register the cell class
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
//pass self to the CommandManager
CommandManager.shared.viewController = self
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//setupConstraints()
reloadTableView()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//setupConstraints()
}
private func setupViews() {
// Set corner radius
view.layer.cornerRadius = 20 // Adjust the corner radius
view.layer.masksToBounds = true
// Set up the title label
titleLabel.text = SwiftLocalizationHelper.localizedString(forKey: "Command Tool")
titleLabel.font = UIFont.boldSystemFont(ofSize: 20) // Adjust font size as needed
titleLabel.textColor = UIColor.white // Adjust color as needed
titleLabel.textAlignment = .center
view.addSubview(titleLabel)
view.backgroundColor = viewBackgroundColor
tableView.backgroundColor = .clear
tableView.rowHeight = isIPhone() ? 47 : 60
tableView.separatorColor = highlightColor
// Configure buttons
addButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Add / Duplicate"), for: .normal)
deleteButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Delete"), for: .normal)
editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Edit"), for: .normal)
exitButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Exit"), for: .normal)
pinButton.setTitle("📌", for: .normal)
addButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) // Adjust the size as needed
deleteButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
editButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
exitButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
pinButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
// Add subviews
view.addSubview(tableView)
view.addSubview(addButton)
view.addSubview(deleteButton)
view.addSubview(editButton)
view.addSubview(exitButton)
view.addSubview(pinButton)
// Setup button targets
addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
exitButton.addTarget(self, action: #selector(exitButtonTapped), for: .touchUpInside)
pinButton.addTarget(self, action: #selector(pinButtonTapped), for: .touchUpInside)
pinButton.contentEdgeInsets = UIEdgeInsets(top: 7, left: 20, bottom: 7, right: 20)
}
public override func updateViewConstraints() {
self.setupConstraints()
super.updateViewConstraints()
}
private func isIPhone()->Bool {
return UIDevice.current.userInterfaceIdiom == .phone
}
@objc public func setupConstraints() {
guard view.superview != nil else {
return
}
NSLayoutConstraint.deactivate(view.constraints)
view.translatesAutoresizingMaskIntoConstraints = false
tableView.translatesAutoresizingMaskIntoConstraints = false
addButton.translatesAutoresizingMaskIntoConstraints = false
deleteButton.translatesAutoresizingMaskIntoConstraints = false
editButton.translatesAutoresizingMaskIntoConstraints = false
exitButton.translatesAutoresizingMaskIntoConstraints = false
pinButton.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// Set the width and height of the view
view.widthAnchor.constraint(equalTo: view.superview!.widthAnchor, multiplier: isIPhone() ? 0.7 : 0.8),
view.heightAnchor.constraint(equalTo: view.superview!.heightAnchor, multiplier: 0.99),
// Set the width and height of the view
//view.leadingAnchor.constraint(equalTo: view.superview!.leadingAnchor, constant: 60),
//view.trailingAnchor.constraint(equalTo: view.superview!.trailingAnchor, constant: -60),
// Center the view horizontally and vertically
view.centerXAnchor.constraint(equalTo: view.superview!.centerXAnchor),
view.centerYAnchor.constraint(equalTo: view.superview!.centerYAnchor),
// ViewTitle constrains
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 13.5),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
// TableView constraints
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60),
// ExitButton constraints
exitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25),
exitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
// EditButton constraints
editButton.leadingAnchor.constraint(equalTo: exitButton.trailingAnchor, constant: 50),
editButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
// AddButton constraints
addButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -25),
addButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
// DeleteButton constraints
deleteButton.trailingAnchor.constraint(equalTo: addButton.leadingAnchor, constant: -50),
deleteButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
// DeleteButton constraints
pinButton.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -50),
pinButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
])
}
private func updateEditingMode() {
addButton.isEnabled = isEditingMode
deleteButton.isEnabled = isEditingMode
if(isEditingMode){ editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Done"), for: .normal) }
else{ editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Edit"), for: .normal) }
}
@objc private func pinButtonTapped() {
viewPinned = !viewPinned
if viewPinned {
pinButton.backgroundColor = highlightColor
}
else{
pinButton.backgroundColor = viewBackgroundColor
}
}
@objc private func addButtonTapped() {
let previouslySelectedIndexPath = tableView.indexPathForSelectedRow //memorize selected indexpath
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "New Command"), message: SwiftLocalizationHelper.localizedString(forKey: "Enter a new command and alias"), preferredStyle: .alert)
alert.addTextField { $0.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Command") }
alert.addTextField { $0.placeholder = SwiftLocalizationHelper.localizedString(forKey: "Alias (optional)") }
alert.textFields?[0].keyboardType = .asciiCapable
alert.textFields?[0].autocorrectionType = .no
alert.textFields?[0].spellCheckingType = .no
alert.textFields?[1].keyboardType = .asciiCapable
alert.textFields?[1].autocorrectionType = .no
alert.textFields?[1].spellCheckingType = .no
let submitAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Add"), style: .default) { [unowned alert] _ in
let keyboardCmdString = alert.textFields?[0].text ?? ""
let alias = alert.textFields?[1].text ?? keyboardCmdString
let newCommand = RemoteCommand(cmdString: keyboardCmdString, alias: alias)
let addCommandSuceeded = CommandManager.shared.addCommand(newCommand)
//self.reloadTableView() // don't know why but this reload has to be called from the CommandManager, it doesn't work here.
//if previouslySelectedIndexPath == nil { return }
if addCommandSuceeded {
let lastRow = self.tableView.numberOfRows(inSection: 0) - 1 //for now there's only 1 section for the tableview, just use setion 0
let newEntryIndexPath = IndexPath(row: lastRow, section: 0)
self.tableView.selectRow(at: newEntryIndexPath, animated: true, scrollPosition: .middle) // shift the highlight to the newly added entry
}
else {
self.tableView.selectRow(at: previouslySelectedIndexPath, animated: true, scrollPosition: .middle) // keep the highlight on the previous entry if failed to add command
}
}
let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey:"Cancel"), style: .cancel)
alert.addAction(submitAction)
alert.addAction(cancelAction)
if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
if isSpecialEntrySelected() {return}
let selectedCommand = CommandManager.shared.getAllCommands()[selectedIndexPath.row-specialEntries.count]
alert.textFields?[0].text = selectedCommand.cmdString // load selected keyboard cmd string
//alert.textFields?[1].text = selectedCommand.alias // leave the alias input field empty
}
self.present(alert, animated: true)
}
@objc private func deleteButtonTapped() {
guard let selectedIndexPath = tableView.indexPathForSelectedRow else {
return
}
if isSpecialEntrySelected() {return}
let previouslySelectedIndexPath = selectedIndexPath // memorize selected row before reloading tableview
CommandManager.shared.deleteCommand(at: selectedIndexPath.row-specialEntries.count)
reloadTableView()
// target new entry after deletion
var targetRowAfterDel = previouslySelectedIndexPath.row - 1
if targetRowAfterDel < 0 { targetRowAfterDel = 0 }
let targetIndexPathAfterDel = IndexPath(row: targetRowAfterDel, section: previouslySelectedIndexPath.section)
if targetRowAfterDel < tableView.numberOfRows(inSection: previouslySelectedIndexPath.section) {
tableView.selectRow(at: targetIndexPathAfterDel, animated: true, scrollPosition: .middle) // shift highlight to the target entry after deletion
}
}
@objc private func editButtonTapped() {
isEditingMode.toggle()
deleteButton.isEnabled = !isSpecialEntrySelected()
addButton.isEnabled = !isSpecialEntrySelected()
}
@objc private func exitButtonTapped() {
dismiss(animated: true, completion: nil)
}
@objc public func reloadTableView() {
tableView.layoutIfNeeded()
tableView.reloadData()
/*
let previouslySelectedIndexPath = tableView.indexPathForSelectedRow
if let indexPath = previouslySelectedIndexPath {
// Make sure the indexPath is still valid and scroll to the selected indexPath
if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle) // keep the entry of previous index selected.
}
} */
}
// UITableViewDataSource
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return specialEntries.count + CommandManager.shared.getAllCommands().count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
// Configure cell appearance
cell.backgroundColor = .clear // Set background color of the cell
cell.textLabel?.textColor = UIColor.white // Set font color of the text
// Set selected background view
let selectedBackgroundView = UIView()
selectedBackgroundView.backgroundColor = highlightColor // Color for the selected state
cell.selectedBackgroundView = selectedBackgroundView
if indexPath.row < specialEntries.count {
cell.textLabel?.text = specialEntryAliasDic[specialEntries[indexPath.row] as! String]
}
else {
let command = CommandManager.shared.getAllCommands()[indexPath.row-specialEntries.count]
cell.textLabel?.text = command.alias
}
return cell
}
// UITableViewDelegate
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
// Set the cell background color to the flashing color
// Animate the flash effect
UIView.animate(withDuration: 0.1, animations: {
cell.selectedBackgroundView?.backgroundColor = .clear
}) { _ in
// Reset the cell background color after the animation
UIView.animate(withDuration: 0.1) {
cell.selectedBackgroundView?.backgroundColor = self.highlightColor
}
}
}
if !isEditingMode {
// Sending keyboard command
if indexPath.row < specialEntries.count{
handleSpecialEntries(index: indexPath.row)
}
else {
let command = CommandManager.shared.getAllCommands()[indexPath.row-specialEntries.count]
sendKeyboardCommand(command)
}
if !viewPinned { dismiss(animated: false, completion: nil) } // dimiss the view in sending mode & the view is not pinned
}
else {
let specialEntrySelected = indexPath.row < specialEntries.count
addButton.isEnabled = !specialEntrySelected
deleteButton.isEnabled = !specialEntrySelected
}
}
private func isSpecialEntrySelected() -> Bool {
return self.tableView.indexPathForSelectedRow?.row ?? specialEntries.count+1 < specialEntries.count
}
private func handleSpecialEntries(index:Int){
usleep(1000*100)
dismiss(animated: true, completion: nil)
switch specialEntries[index] as? String {
case "widgetLayoutTool":
specialEntryDelegate?.openWidgetLayoutTool!()
case "widgetSwitchTool":
specialEntryDelegate?.switchWidgetProfile?()
default: break
}
}
private func sendKeyboardCommand(_ cmd: RemoteCommand) {
print("Sending key-value")
let keyboardCmdStrings = CommandManager.shared.extractKeyStringsFromComboCommand(from: cmd.cmdString)
CommandManager.shared.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings!)
}
}