diff --git a/Limelight/Base.lproj/iPad.storyboard b/Limelight/Base.lproj/iPad.storyboard index 3f1909a9..9065e5b7 100644 --- a/Limelight/Base.lproj/iPad.storyboard +++ b/Limelight/Base.lproj/iPad.storyboard @@ -985,7 +985,7 @@ ICAgICAgICAgICAgICA - + diff --git a/Limelight/Base.lproj/iPhone.storyboard b/Limelight/Base.lproj/iPhone.storyboard index 6f7c52af..506a721d 100644 --- a/Limelight/Base.lproj/iPhone.storyboard +++ b/Limelight/Base.lproj/iPhone.storyboard @@ -1005,7 +1005,7 @@ ICAgICAgICAgICAgIA - + diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index 99379944..428f5bd0 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; 2AAA4E842F7666EE001E20DC /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; }; 2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; }; + 3A4A3A112F0C000100000001 /* OSCProfilesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */; }; 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; 84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; }; 84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; }; @@ -45,9 +46,7 @@ 84351B102C29ACCB00A77989 /* OSCProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0A2C29ACCA00A77989 /* OSCProfile.m */; }; 84351B112C29ACCB00A77989 /* OSCProfilesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0B2C29ACCB00A77989 /* OSCProfilesManager.m */; }; 84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */; }; - 84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */; }; 84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B292C29AD6000A77989 /* ToolBarContainerView.m */; }; - 84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */; }; 84351B5D2C29ADBE00A77989 /* ProfileTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */; }; 843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */ = {isa = PBXBuildFile; fileRef = 843667EF2D9D7173004A7D7F /* widgetTemplates.bin */; }; 843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */; }; @@ -204,6 +203,7 @@ 2A039E562F72892B00546768 /* VoidLinkSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoidLinkSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = ""; }; + 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesTableViewController.swift; sourceTree = ""; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomTapGestureRecognizer.h; sourceTree = ""; }; 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomTapGestureRecognizer.m; sourceTree = ""; }; @@ -217,13 +217,9 @@ 84351B0C2C29ACCB00A77989 /* LayoutOnScreenControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutOnScreenControls.h; sourceTree = ""; }; 84351B0D2C29ACCB00A77989 /* OSCProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfile.h; sourceTree = ""; }; 84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControlsViewController.m; sourceTree = ""; }; - 84351B262C29AD6000A77989 /* OSCProfilesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfilesTableViewController.h; sourceTree = ""; }; - 84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCProfilesTableViewController.m; sourceTree = ""; }; 84351B282C29AD6000A77989 /* LayoutOnScreenControlsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutOnScreenControlsViewController.h; sourceTree = ""; }; 84351B292C29AD6000A77989 /* ToolBarContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ToolBarContainerView.m; sourceTree = ""; }; 84351B2A2C29AD6000A77989 /* ToolBarContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ToolBarContainerView.h; sourceTree = ""; }; - 84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProfileTableViewCell.m; sourceTree = ""; }; - 84351B5A2C29ADBE00A77989 /* ProfileTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileTableViewCell.h; sourceTree = ""; }; 84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProfileTableViewCell.xib; sourceTree = ""; }; 843667EF2D9D7173004A7D7F /* widgetTemplates.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplates.bin; path = PresetConfigs/widgetTemplates.bin; sourceTree = ""; }; 843CC7EE2C1E757D00BBC475 /* NativeTouchPointer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeTouchPointer.h; sourceTree = ""; }; @@ -499,27 +495,24 @@ 84351B242C29AD2E00A77989 /* CustomOSCViewControl */ = { isa = PBXGroup; children = ( - 84351B2E2C29AD6600A77989 /* Cells */, + 84351B2E2C29AD6600A77989 /* Table */, 84351B282C29AD6000A77989 /* LayoutOnScreenControlsViewController.h */, 84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */, 187F4B232E04633100EECC59 /* WidgetPanelStackView.h */, 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */, - 84351B262C29AD6000A77989 /* OSCProfilesTableViewController.h */, - 84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */, 84351B2A2C29AD6000A77989 /* ToolBarContainerView.h */, 84351B292C29AD6000A77989 /* ToolBarContainerView.m */, ); path = CustomOSCViewControl; sourceTree = ""; }; - 84351B2E2C29AD6600A77989 /* Cells */ = { + 84351B2E2C29AD6600A77989 /* Table */ = { isa = PBXGroup; children = ( - 84351B5A2C29ADBE00A77989 /* ProfileTableViewCell.h */, - 84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */, 84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */, + 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */, ); - path = Cells; + path = Table; sourceTree = ""; }; 843667ED2D9D70A3004A7D7F /* PresetConfigs */ = { @@ -1197,7 +1190,7 @@ 97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */, 97E20DD12E21647A000D5DCA /* MetalView.m in Sources */, 97E20DD22E21647A000D5DCA /* MetalVideoRenderer.m in Sources */, - 84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */, + 3A4A3A112F0C000100000001 /* OSCProfilesTableViewController.swift in Sources */, 98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */, 1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */, 18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */, @@ -1222,7 +1215,6 @@ FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */, FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */, 84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */, - 84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */, 18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */, 18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */, FB89463119F646E200339C8A /* StreamManager.m in Sources */, diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 5fece34b..35a3c376 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -3209,7 +3209,7 @@ - + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index 38f15791..c7457fa0 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -978,7 +978,7 @@ - + diff --git a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m index 532b6183..5eca5ab5 100644 --- a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m +++ b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m @@ -10,7 +10,6 @@ // #import "LayoutOnScreenControls.h" -#import "OSCProfilesTableViewController.h" #import "OnScreenButtonState.h" #import "OSCProfilesManager.h" diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.h b/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.h deleted file mode 100644 index ee758a07..00000000 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// ProfileTableViewCell.h -// Moonlight -// -// Created by Long Le on 12/11/22. -// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ProfileTableViewCell : UITableViewCell - -@property (weak, nonatomic) IBOutlet UILabel *name; - -@end - -NS_ASSUME_NONNULL_END diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.m b/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.m deleted file mode 100644 index f1388f47..00000000 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.m +++ /dev/null @@ -1,21 +0,0 @@ -// -// 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 diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h index ac18610f..39241396 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h @@ -13,7 +13,6 @@ #import "LayoutOnScreenControls.h" #import "ToolBarContainerView.h" #import "OSCProfilesManager.h" -#import "OSCProfilesTableViewController.h" #import "VoidLink-Swift.h" NS_ASSUME_NONNULL_BEGIN diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index 6751c864..ef99a1f9 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -10,7 +10,6 @@ // #import "LayoutOnScreenControlsViewController.h" -#import "OSCProfilesTableViewController.h" #import "OnScreenButtonState.h" #import "OnScreenControls.h" #import "OSCProfilesManager.h" @@ -1579,7 +1578,6 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [selectedWidgetView showStickIndicator]; } - - (void)handleMissingToolBarIcon:(UIView *)view { for (UIView *subview in view.subviews) { if ([subview isKindOfClass:[UIButton class]]) { @@ -1900,7 +1898,9 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { strongSelf->_oscProfilesTableViewController.currentOSCButtonLayers = weakSelf.layoutOSC.OSCButtonLayerPool; //pass updated OSCLayout to OSCProfileTableView again }; - [self.oscProfilesTableViewController profileViewRefresh]; // execute this will make sure OSCLayout is updated from persisted profile, not any cache. + [self.oscProfilesTableViewController.tableView reloadData]; + [self reloadOnScreenWidgetViews]; + NSLog(@"profileRefresh %f", CACurrentMediaTime()); // [self reloadOnScreenWidgetViews]; diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h deleted file mode 100644 index 71e69271..00000000 --- a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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 - -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 - -- (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) NSMutableSet *currentOSCButtonLayers; -@property (nonatomic, assign) CGRect layoutViewBounds; -@property (weak, nonatomic) IBOutlet UINavigationBar *profileTableViewNavigationBar; -@property (nonatomic, assign) bool pickProfileEnabled; - -@end - -NS_ASSUME_NONNULL_END diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m deleted file mode 100644 index 2d8d7b93..00000000 --- a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m +++ /dev/null @@ -1,485 +0,0 @@ -// -// 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 () -@property (weak, nonatomic) IBOutlet UINavigationItem *profileTableViewNavigationItem; - -@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) viewWillDisappear:(BOOL)animated{ - [super viewWillDisappear:animated]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutTableViewCloseNotification" object:self]; // notify other view that oscLayoutManager is closing -} - - -- (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.cornerRadius = 15; // 设置圆角半径 - self.profileTableViewNavigationBar.layer.maskedCorners = kCALayerMinXMinYCorner|kCALayerMaxXMinYCorner; - - - self.profileTableViewNavigationBar.layer.masksToBounds = true; // 使圆角生效 - // self.profileTableViewNavigationBar.layer.mask = maskLayer; - - self.tableView.layer.cornerRadius = 15; - self.tableView.layer.maskedCorners = kCALayerMinXMaxYCorner|kCALayerMaxXMaxYCorner; - self.tableView.layer.masksToBounds = true; - - tableView.separatorStyle = (GenericUtils.isIPhone&&GenericUtils.liquidGlassEnabled) ? UITableViewCellSeparatorStyleSingleLine : UITableViewCellSeparatorStyleSingleLine; - tableView.separatorInset = UIEdgeInsetsMake(0, 15, 0, 15); - - if(GenericUtils.liquidGlassEnabled){ - tableView.separatorColor = [UIColor.whiteColor colorWithAlphaComponent:GenericUtils.isIPhone ? 0.28 : 0.165]; - } - else{ - tableView.separatorColor = [UIColor.whiteColor colorWithAlphaComponent:0.3]; - } - -} - -- (void) viewDidLoad { - [super viewDidLoad]; - - // self.profileTableViewNavigationBar.layer.cornerRadius = 15; // 设置圆角半径 - - profilesManager = [OSCProfilesManager sharedManager:_layoutViewBounds]; - - self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; - - 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 = 1; - //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.43]; // set background color & transparency - // self.tableView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2]; // set background color & transparency - - UITapGestureRecognizer *tap = - [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(dismissSelf)]; - tap.cancelsTouchesInView = NO; - tap.delegate = (id)self; - - [self.view addGestureRecognizer:tap]; - - if(GenericUtils.liquidGlassEnabled){ - if (@available(iOS 26.0, *)) { - for(UIBarButtonItem* button in self.profileTableViewNavigationItem.leftBarButtonItems){ - button.hidesSharedBackground = true; - } - for(UIBarButtonItem* button in self.profileTableViewNavigationItem.rightBarButtonItems){ - button.hidesSharedBackground = true; - } - } - } -} - -- (void)dismissSelf { - [self dismissViewControllerAnimated:NO completion:nil]; -} - -- (void) viewDidAppear:(BOOL)animated { - [super viewDidAppear: animated]; - - - self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; - 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 duplicateSelectedProfileWithName:enteredProfileName]; // the OSC layout here is passed from parent LayoutOSCViewController; - // [self->profilesManager setProfileToSelected: [self->profilesManager getIndexOfLastProfile]]; - - 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(_pickProfileEnabled) return; - 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) restoreTapped:(id)sender { - [profilesManager importDefaultTemplates]; - [self profileViewRefresh]; -} - -#pragma mark - UIDocumentPickerDelegate -- (void)documentPicker:(UIDocumentPickerViewController *)controller -didPickDocumentsAtURLs:(NSArray *)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 colorWithAlphaComponent:GenericUtils.isIPhone ? 0.9 : 0.9]; // Opaque white text - cell.name.font =[UIFont systemFontOfSize:GenericUtils.isIPhone ? 18.5 : 18.5 weight:UIFontWeightMedium]; - cell.name.shadowColor = [UIColor blackColor]; // Black shadow - // Set the shadow offset - cell.name.shadowOffset = CGSizeMake(0.5, 0.5); - - // Set cell and contentView background colors - cell.backgroundColor = [UIColor clearColor]; - cell.contentView.backgroundColor = [UIColor clearColor]; - - // Configure the checkmark accessory - if (indexPath.row == [profilesManager getIndexOfSelectedProfile]) { - cell.accessoryType = UITableViewCellAccessoryCheckmark; - } else { - cell.accessoryType = UITableViewCellAccessoryNone; - } - - // Remove existing custom separators to avoid duplicates - - // [cell.contentView addSubview:separatorView]; - // [cell.contentView bringSubviewToFront:separatorView]; - - // Replace the default checkmark with a UILabel displaying a checkmark character - if (indexPath.row == [profilesManager getIndexOfSelectedProfile]) { - 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 - } - - 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]; - - [profilesManager setProfileToSelected:(uint32_t)indexPath.row]; // 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 -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer - shouldReceiveTouch:(UITouch *)touch { - if (_pickProfileEnabled && [touch.view isDescendantOfView:self.tableView]) { - return true; - } - if(touch.view == self.view) return true; - return false; -} - -@end diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift new file mode 100644 index 00000000..88599ae2 --- /dev/null +++ b/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift @@ -0,0 +1,409 @@ +// +// OSCProfilesTableViewController.m -> OSCProfilesTableViewController.swift +// 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 + +@objc(ProfileTableViewCell) +final class ProfileTableViewCell: UITableViewCell { + @IBOutlet weak var name: UILabel! +} + +@objc(FileOperation) +enum FileOperation: Int { + case importOperation = 0 + case exportOperation = 1 +} + +@objcMembers +@objc(OSCProfilesTableViewController) +final class OSCProfilesTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIDocumentPickerDelegate, UIGestureRecognizerDelegate { + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var profileTableViewNavigationBar: UINavigationBar! + @IBOutlet private weak var profileTableViewNavigationItem: UINavigationItem! + + var currentFileOperation: FileOperation = .importOperation + var needToUpdateOscLayoutTVC: (() -> Void)? + weak var currentOSCButtonLayers: NSMutableSet? + var layoutViewBounds: CGRect = .zero + var pickProfileEnabled: Bool = false + + private var profilesManager: OSCProfilesManager! + + private func getCurrentOrientation() -> UIInterfaceOrientationMask { + let bounds = UIScreen.main.bounds + if bounds.width > bounds.height { + return .landscape + } else { + return [.portrait, .portraitUpsideDown] + } + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + getCurrentOrientation() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + NotificationCenter.default.post(name: Notification.Name("OscLayoutTableViewCloseNotification"), object: self) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + profileTableViewNavigationBar.layer.cornerRadius = 15 + profileTableViewNavigationBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + profileTableViewNavigationBar.layer.masksToBounds = true + + tableView.layer.cornerRadius = 15 + tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + tableView.layer.masksToBounds = true + + tableView.separatorStyle = .singleLine + tableView.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) + + if GenericUtils.liquidGlassEnabled { + tableView.separatorColor = UIColor.white.withAlphaComponent(GenericUtils.isIPhone() ? 0.28 : 0.165) + } else { + tableView.separatorColor = UIColor.white.withAlphaComponent(0.3) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + profilesManager = OSCProfilesManager.sharedManager(layoutViewBounds) + configureTableView() + tableView.alpha = 1 + tableView.backgroundColor = UIColor.black.withAlphaComponent(0.43) + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf)) + tap.cancelsTouchesInView = false + tap.delegate = self + view.addGestureRecognizer(tap) + + if GenericUtils.liquidGlassEnabled, #available(iOS 26.0, *) { + profileTableViewNavigationItem.leftBarButtonItems?.forEach { $0.hidesSharedBackground = true } + profileTableViewNavigationItem.rightBarButtonItems?.forEach { $0.hidesSharedBackground = true } + } + } + + @objc private func dismissSelf() { + dismiss(animated: false) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + configureTableView() + if profilesManager.getAllProfiles().count > 0 { + let indexPath = IndexPath(row: profilesManager.getIndexOfSelectedProfile(), section: 0) + tableView.scrollToRow(at: indexPath, at: .middle, animated: true) + } + } + + private func configureTableView() { + tableView.tableHeaderView = UIView(frame: .zero) + tableView.delegate = self + tableView.dataSource = self + tableView.register(UINib(nibName: "ProfileTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell") + } + + @IBAction func duplicateTapped(_ sender: Any?) { + let inputAlert = UIAlertController( + title: SwiftLocalizationHelper.localizedString(forKey: "Enter the name you want to save this profile as"), + message: "", + preferredStyle: .alert + ) + inputAlert.addTextField { textField in + textField.placeholder = SwiftLocalizationHelper.localizedString(forKey: "name") + textField.textColor = .lightGray + textField.clearButtonMode = .whileEditing + textField.borderStyle = .none + } + inputAlert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Cancel"), style: .default) { _ in + inputAlert.dismiss(animated: false) + }) + inputAlert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Save"), style: .default) { [weak self] _ in + guard let self else { return } + let enteredProfileName = inputAlert.textFields?.first?.text ?? "" + + if enteredProfileName == "Default" { + let alert = UIAlertController( + title: "", + message: SwiftLocalizationHelper.localizedString(forKey: "Saving over the 'Default' profile is not allowed"), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in + alert.dismiss(animated: false) { + self.present(inputAlert, animated: true) + } + }) + self.present(alert, animated: true) + } else if enteredProfileName.isEmpty { + let alert = UIAlertController( + title: "", + message: SwiftLocalizationHelper.localizedString(forKey: "Profile name cannot be blank!"), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in + alert.dismiss(animated: false) { + self.present(inputAlert, animated: true) + } + }) + self.present(alert, animated: true) + } else if self.profilesManager.profileNameAlreadyExist(enteredProfileName) { + let alert = UIAlertController( + title: "", + message: SwiftLocalizationHelper.localizedString(forKey: "Profile name already exists"), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Ok"), style: .default)) + self.present(alert, animated: true) + } else { + self.profilesManager.duplicateSelectedProfile(withName: enteredProfileName) + let alert = UIAlertController( + title: "", + message: SwiftLocalizationHelper.localizedString(forKey: "Profile %@ duplicated from current layout", enteredProfileName), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in + alert.dismiss(animated: false) + self.profileViewRefresh() + }) + self.present(alert, animated: true) + } + }) + + present(inputAlert, animated: true) + } + + func profileViewRefresh() { + tableView.reloadData() + needToUpdateOscLayoutTVC?() + NotificationCenter.default.post(name: Notification.Name("OscLayoutProfileSelctedInTableView"), object: self) + } + + @IBAction func deleteTapped(_ sender: Any?) { + profilesManager.deleteCurrentSelectedProfile() + profileViewRefresh() + } + + @IBAction func exportDataTapped(_ sender: Any?) { + currentFileOperation = .exportOperation + let tempPath = NSTemporaryDirectory().appending("profiles.bin") + try? Data().write(to: URL(fileURLWithPath: tempPath), options: .atomic) + + let picker = UIDocumentPickerViewController(url: URL(fileURLWithPath: tempPath), in: .exportToService) + picker.delegate = self + present(picker, animated: true) + } + + @IBAction func importDataTapped(_ sender: Any?) { + currentFileOperation = .importOperation + let picker = UIDocumentPickerViewController(documentTypes: ["public.item"], in: .open) + picker.delegate = self + picker.allowsMultipleSelection = false + present(picker, animated: true) + } + + @IBAction func restoreTapped(_ sender: Any?) { + profilesManager.importDefaultTemplates() + profileViewRefresh() + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { return } + switch currentFileOperation { + case .exportOperation: + profilesToFile(url) + case .importOperation: + fileToProfiles(url) + } + } + + private func profilesToFile(_ destinationURL: URL) { + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: profilesManager.getEncodedProfiles(), requiringSecureCoding: true) + let accessed = destinationURL.startAccessingSecurityScopedResource() + defer { + if accessed { + destinationURL.stopAccessingSecurityScopedResource() + } + } + try data.write(to: destinationURL, options: .atomic) + } catch { + NSLog("写入失败: \(error)") + } + } + + private func fileToProfiles(_ sourceURL: URL) { + var restoreFailed = false + let accessed = sourceURL.startAccessingSecurityScopedResource() + if !accessed { + restoreFailed = true + } + + defer { + if accessed { + sourceURL.stopAccessingSecurityScopedResource() + } + } + + do { + let fileData = try Data(contentsOf: sourceURL) + NSLog("profile file read: \(UInt32(fileData.count))") + let classes: [AnyClass] = [NSMutableData.self, NSMutableArray.self] + let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: classes, from: fileData) as? NSMutableArray + + let restoredAlert = UIAlertController( + title: SwiftLocalizationHelper.localizedString(forKey: ""), + message: SwiftLocalizationHelper.localizedString(forKey: "Pofiles imported"), + preferredStyle: .alert + ) + let failedAlert = UIAlertController( + title: SwiftLocalizationHelper.localizedString(forKey: ""), + message: SwiftLocalizationHelper.localizedString(forKey: "Failed to import profiles"), + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) + + if restoreFailed || profilesEncoded == nil { + failedAlert.addAction(okAction) + present(failedAlert, animated: true) + } else { + profilesManager.importEncodedProfiles(profilesEncoded!) + restoredAlert.addAction(okAction) + present(restoredAlert, animated: true) + } + + profileViewRefresh() + NSLog("profile test: \(UInt32(profilesEncoded?.count ?? 0))") + } catch { + restoreFailed = true + let failedAlert = UIAlertController( + title: SwiftLocalizationHelper.localizedString(forKey: ""), + message: SwiftLocalizationHelper.localizedString(forKey: "Failed to import profiles"), + preferredStyle: .alert + ) + failedAlert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default)) + present(failedAlert, animated: true) + profileViewRefresh() + NSLog("解码失败: \(error)") + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + profilesManager.getAllProfiles().count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? ProfileTableViewCell, + let profile = profilesManager.getAllProfiles().object(at: indexPath.row) as? OSCProfile, + let nameLabel = cell.name else { + return UITableViewCell() + } + + nameLabel.text = profile.name + nameLabel.backgroundColor = UIColor.clear + nameLabel.alpha = 1.0 + nameLabel.textColor = UIColor.white.withAlphaComponent(0.9) + nameLabel.font = UIFont.systemFont(ofSize: 18.5, weight: .medium) + nameLabel.shadowColor = UIColor.black + nameLabel.shadowOffset = CGSize(width: 0.5, height: 0.5) + + cell.backgroundColor = UIColor.clear + cell.contentView.backgroundColor = UIColor.clear + + if indexPath.row == profilesManager.getIndexOfSelectedProfile() { + cell.accessoryType = UITableViewCell.AccessoryType.checkmark + let checkmarkLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) + checkmarkLabel.text = "✓" + checkmarkLabel.font = .systemFont(ofSize: 25) + checkmarkLabel.textAlignment = .center + checkmarkLabel.textColor = .green + checkmarkLabel.layer.zPosition = 0 + cell.accessoryView = checkmarkLabel + } else { + cell.accessoryType = UITableViewCell.AccessoryType.none + cell.accessoryView = nil + } + + return cell + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + true + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 60.0 + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + let profiles = profilesManager.getAllProfiles() + + if let profile = profiles.object(at: indexPath.row) as? OSCProfile, profile.name == "Default" { + let alert = UIAlertController(title: "", message: "Deleting the 'Default' profile is not allowed", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in + alert.dismiss(animated: false) + }) + present(alert, animated: true) + return + } + + if editingStyle == .delete { + if let profile = profiles.object(at: indexPath.row) as? OSCProfile, profile.isSelected, indexPath.row > 0, + let previousProfile = profiles.object(at: indexPath.row - 1) as? OSCProfile { + previousProfile.isSelected = true + } + + profiles.removeObject(at: indexPath.row) + + let profilesEncoded = NSMutableArray() + for case let profileDecoded as OSCProfile in profiles { + if let profileEncoded = try? NSKeyedArchiver.archivedData(withRootObject: profileDecoded, requiringSecureCoding: true) { + profilesEncoded.add(profileEncoded) + } + } + if let data = try? NSKeyedArchiver.archivedData(withRootObject: profilesEncoded, requiringSecureCoding: true) { + UserDefaults.standard.set(data, forKey: "OSCProfiles") + UserDefaults.standard.synchronize() + } + + tableView.reloadData() + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedIndexPath = IndexPath(row: indexPath.row, section: 0) + let lastSelectedIndexPath = IndexPath(row: profilesManager.getIndexOfSelectedProfile(), section: 0) + + if selectedIndexPath != lastSelectedIndexPath { + let selectedCell = tableView.cellForRow(at: selectedIndexPath) + selectedCell?.accessoryType = .checkmark + selectedCell?.accessoryView?.tintColor = UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 1.0).withAlphaComponent(1.0) + + profilesManager.setProfileToSelected(UInt32(indexPath.row)) + + let lastSelectedCell = tableView.cellForRow(at: lastSelectedIndexPath) + lastSelectedCell?.accessoryType = .none + tableView.deselectRow(at: lastSelectedIndexPath, animated: true) + } + profileViewRefresh() + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if pickProfileEnabled, let touchView = touch.view, touchView.isDescendant(of: tableView) { + return true + } + return touch.view == view + } +} diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.xib b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileTableViewCell.xib similarity index 98% rename from VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.xib rename to VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileTableViewCell.xib index f32dd8f4..8f291de9 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Cells/ProfileTableViewCell.xib +++ b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileTableViewCell.xib @@ -10,7 +10,7 @@ - +