diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index a332e952..668b4e9e 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -47,7 +47,7 @@ 2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; }; 2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; }; 2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; }; - 3A4A3A112F0C000100000001 /* OSCProfilesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */; }; + 3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */; }; 3A4A3A132F0C000100000003 /* LayoutOnScreenControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */; }; 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; 84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; }; @@ -56,7 +56,6 @@ 84351B102C29ACCB00A77989 /* OSCProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0A2C29ACCA00A77989 /* OSCProfile.m */; }; 84351B112C29ACCB00A77989 /* OSCProfilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */; }; 84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B292C29AD6000A77989 /* ToolBarContainerView.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 */; }; 843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */; }; @@ -219,7 +218,7 @@ 2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamepadNavigationIllustrationHud.swift; sourceTree = ""; }; 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = ""; }; 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = ""; }; - 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesTableViewController.swift; sourceTree = ""; }; + 3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSelectorViewController.swift; sourceTree = ""; }; 3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutOnScreenControlsViewController.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 = ""; }; @@ -233,7 +232,6 @@ 84351B0D2C29ACCB00A77989 /* OSCProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfile.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 = ""; }; - 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 = ""; }; 843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NativeTouchPointer.m; sourceTree = ""; }; @@ -522,8 +520,7 @@ 84351B2E2C29AD6600A77989 /* Table */ = { isa = PBXGroup; children = ( - 84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */, - 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */, + 3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */, ); path = Table; sourceTree = ""; @@ -1112,7 +1109,6 @@ 8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */, 843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */, 84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */, - 84351B5D2C29ADBE00A77989 /* ProfileTableViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1218,7 +1214,7 @@ 97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */, 97E20DD12E21647A000D5DCA /* MetalView.m in Sources */, 97E20DD22E21647A000D5DCA /* MetalVideoRenderer.m in Sources */, - 3A4A3A112F0C000100000001 /* OSCProfilesTableViewController.swift in Sources */, + 3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */, 98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */, 1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */, 18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */, diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 862f5f11..ee21cfb9 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -2,7 +2,6 @@ - @@ -13,7 +12,7 @@ - + @@ -3879,10 +3878,10 @@ - + - + @@ -3890,83 +3889,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index d01e2267..e3809593 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -2,7 +2,6 @@ - @@ -15,7 +14,7 @@ - + @@ -1507,83 +1506,17 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -3796,19 +3729,19 @@ - + - + - + - + - + diff --git a/VoidLink/Input/ControllerNavigator.swift b/VoidLink/Input/ControllerNavigator.swift index fb71c82a..657ed4cc 100644 --- a/VoidLink/Input/ControllerNavigator.swift +++ b/VoidLink/Input/ControllerNavigator.swift @@ -696,7 +696,7 @@ final class ControllerNavigator: NSObject { - if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is ToolboxViewController { + if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is ToolboxViewController || uiNavigationDelegate is ProfileSelectorViewController { let verticalNavigationAxis: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .leftStickY : .rightStickY ControllerUtil.listenPrimaryControllerStickAxis(verticalNavigationAxis, threshold: 0.6) {state in GamepadNavigationIllustrationHud.updateActionState(for: verticalNavigationAxis, isInAction: state != .orderedSame) @@ -726,7 +726,7 @@ final class ControllerNavigator: NSObject { } } - if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || uiNavigationDelegate is ToolboxViewController{ + if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || uiNavigationDelegate is ToolboxViewController || uiNavigationDelegate is ProfileSelectorViewController { let horizontalNavigationAxis: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .leftStickX : .rightStickX ControllerUtil.listenPrimaryControllerStickAxis(horizontalNavigationAxis, threshold: 0.6) {state in GamepadNavigationIllustrationHud.updateActionState(for: horizontalNavigationAxis, isInAction: state != .orderedSame) @@ -750,7 +750,7 @@ final class ControllerNavigator: NSObject { } } - if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is UIAlertController || uiNavigationDelegate is OSCProfilesTableViewController || uiNavigationDelegate is ToolboxViewController { + if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is UIAlertController || uiNavigationDelegate is ProfileSelectorViewController || uiNavigationDelegate is ToolboxViewController { let navigations = uiNavigationDelegate.getNavigationElements() let buttonNavigations = navigations.filter({$0.control.type == .button}) // let stickNavigations = navigations.filter({$0.control.type == .stick || $0.control.type == .stickAxis}) @@ -1494,6 +1494,9 @@ extension ControllerCollectionNavigationDelegate { } func applyControllerNavigationHighlight(to cell: UICollectionViewCell, highlighted: Bool) { + if self is ProfileSelectorViewController { + return + } clearControllerNavigationHighlightBorder(in: cell) guard highlighted else { return } let highlightedView = controllerNavigationHighlightTargetView(for: cell) diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.swift b/VoidLink/Input/CustomOSC/OSCProfilesManager.swift index 0016b808..ab5b8b9e 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.swift +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.swift @@ -86,6 +86,10 @@ class OSCProfilesManager: NSObject { persistEncodedProfiles(profilesEncoded) } + func persistProfileOrder(_ orderedProfiles: NSMutableArray) { + persistProfiles(orderedProfiles) + } + private func persistEncodedProfiles(_ profilesEncoded: NSMutableArray) { guard let data = try? NSKeyedArchiver.archivedData(withRootObject: profilesEncoded, requiringSecureCoding: true) else { return @@ -172,13 +176,19 @@ class OSCProfilesManager: NSObject { func importEncodedProfiles(_ profilesEncoded: NSMutableArray) { let localProfiles = currentProfiles.mutableCopy() as? NSMutableArray ?? NSMutableArray() + let profilesDecoded = decodeProfiles(from: profilesEncoded) + let importedIncludesDefault = (profilesDecoded.firstObject as? OSCProfile)?.name == "Default" + let isSingleProfile = profilesEncoded.count == 1 - if localProfiles.count > 0 { + if importedIncludesDefault && localProfiles.count > 0 { + localProfiles.removeObject(at: 0) + } else if !importedIncludesDefault, + let localDefaultProfile = localProfiles.firstObject as? OSCProfile, + localDefaultProfile.name == "Default" { + profilesDecoded.insert(localDefaultProfile, at: 0) localProfiles.removeObject(at: 0) } - - let profilesDecoded = decodeProfiles(from: profilesEncoded) - + let localProfileNames = Set( localProfiles.compactMap { ($0 as? OSCProfile)?.name } ) @@ -202,13 +212,24 @@ class OSCProfilesManager: NSObject { for case let profile as Any in localProfilesToRemove { localProfiles.remove(profile) } - - var indexOffset = 0 + + var indexOffset = importedIncludesDefault ? 0: 0 for profile in localProfiles { profilesDecoded.insert(profile, at: 1+indexOffset) indexOffset += 1 } - + + if isSingleProfile, !importedIncludesDefault { + for profile in profilesDecoded { + if let profile = profile as? OSCProfile { + profile.isSelected = false + } + } + if let profile = profilesDecoded[profilesDecoded.count-1] as? OSCProfile { + profile.isSelected = true + } + } + persistEncodedProfiles(encodedProfiles(from: profilesDecoded)) } diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index c363f854..dd0cefc7 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -4592,6 +4592,29 @@ } } }, + "All Profiles" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部配置" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部設定檔" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部設定檔" + } + } + } + }, "Alpha" : { "extractionState" : "manual", "localizations" : { @@ -6511,6 +6534,29 @@ } } }, + "Current Profile" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前配置" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前設定檔" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前設定檔" + } + } + } + }, "Del" : { "extractionState" : "manual", "localizations" : { @@ -6586,6 +6632,29 @@ } } }, + "Duplicate" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + } + } + }, "Delete Buttons Here" : { "extractionState" : "manual", "localizations" : { @@ -8142,6 +8211,29 @@ } } }, + "Export" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "匯出" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "匯出" + } + } + } + }, "Expands setting menu during streaming" : { "extractionState" : "manual", "localizations" : { @@ -14837,6 +14929,29 @@ } } }, + "Restore" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "恢复" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "還原" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "還原" + } + } + } + }, "Restore Purchase" : { "extractionState" : "manual", "localizations" : { @@ -18862,4 +18977,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/VoidLink/ThemeManager.swift b/VoidLink/ThemeManager.swift index 0238f005..55bf1d04 100644 --- a/VoidLink/ThemeManager.swift +++ b/VoidLink/ThemeManager.swift @@ -33,17 +33,44 @@ class ThemeManager: NSObject { _ = setPublicUIStyle() return _userInterfaceStyle } + + @objc class func overrideUserInterfaceStyle() -> UIUserInterfaceStyle { + return _privateUserInterfaceStyle + } @objc class func setUserInterfaceStyle(_ style: UIUserInterfaceStyle) { + let oldUserInterfaceStyle = _userInterfaceStyle _privateUserInterfaceStyle = style applyUserInterfaceStyleOverride(style) - if _userInterfaceStyle == style { + _ = setPublicUIStyle() + + if oldUserInterfaceStyle == _userInterfaceStyle { return } - _ = setPublicUIStyle() - + NotificationCenter.default.post( + name: Notification.Name(ThemeDidChangeNotification), + object: nil + ) + } + + @objc class func systemUserInterfaceStyleDidChange(_ style: UIUserInterfaceStyle) { + guard #available(iOS 13.0, *) else { return } + + if _privateUserInterfaceStyle != .unspecified { + applyUserInterfaceStyleOverride(_privateUserInterfaceStyle) + return + } + + let oldUserInterfaceStyle = _userInterfaceStyle + _userInterfaceStyle = style + applyUserInterfaceStyleOverride(.unspecified) + + if oldUserInterfaceStyle == _userInterfaceStyle { + return + } + NotificationCenter.default.post( name: Notification.Name(ThemeDidChangeNotification), object: nil diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h index e03a1881..e65b307b 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)profileRefresh; - (void)reloadOnScreenWidgetViews; -- (void)presentProfilesTableViewWithLoadingMode:(NSInteger)loadingMode; +- (void)presentProfileSelectorWithLoadingMode:(NSInteger)loadingMode; @end diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index 22dbf233..d6a9fe24 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -322,14 +322,14 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleProfileTablViewDismiss) - name:@"OscLayoutTableViewCloseNotification" + selector:@selector(handleProfileSelectorDismiss) + name:@"ProfileSelectorCloseNotification" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dummytest) - name:@"OscLayoutProfileSelctedInTableView" // This is a special notification for reloading the on screen keyboard buttons. which can't be executed by _oscProfilesTableViewController.needToUpdateOscLayoutTVC code block, and has to be triggered by a notification + name:@"OscLayoutProfileSelectedNotification" // This is a special notification for reloading the on screen keyboard buttons. It has to be triggered by a notification because it cannot be handled by the profile selector dismiss block. object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -364,7 +364,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { selectedWidgetView = nil; selectedControllerLayer = nil; - _oscProfilesTableViewController.layoutViewBounds = self.view.bounds; + _profileSelectorViewController.layoutViewBounds = self.view.bounds; [OSCProfilesManager setLayoutViewBounds:self.view.bounds]; [OSCProfilesManager setOnScreenWidgetViewsSet:self.onScreenWidgetViews]; // pass the keyboard button dict to profiles manager @@ -1029,7 +1029,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { else{ UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile Default can not be overwritten"] preferredStyle:UIAlertControllerStyleAlert]; [savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - [self.oscProfilesTableViewController profileViewRefresh]; // execute this will reset layout in OSC tool! + [self.profileSelectorViewController profileViewRefresh]; // execute this will reset layout in OSC tool! }]]; if(sender) [self presentViewController:savedAlertController animated:YES completion:nil]; } @@ -1863,7 +1863,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); } -- (void)handleProfileTablViewDismiss{ +- (void)handleProfileSelectorDismiss{ if(_quickSwitchEnabled){ [self clearOnScreenWidgets]; [self dismissViewControllerAnimated:NO completion:^{ @@ -1876,28 +1876,19 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { /* Basically the same method as loadTapped, without parameter*/ // Make sure whenever self view controller load the selected profile and layout its buttons. - (void)profileRefresh{ - UIStoryboard *storyboard; - BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); - if (isIPhone) { - storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil]; - } - else { - storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil]; - } - - //initialiaze _oscProfilesTableViewController - self->_oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"]; + // initialiaze _profileSelectorViewController + self->_profileSelectorViewController = [[ProfileSelectorViewController alloc] init]; + self->_profileSelectorViewController.layoutViewBounds = self.view.bounds; //this part is just for registration, will not be immediately executed. __weak typeof(self) weakSelf = self; - self->_oscProfilesTableViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC profile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position + self->_profileSelectorViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'ProfileSelectorViewController' VC is dismissed. By the time it is dismissed the user would have potentially selected a different OSC profile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; [weakSelf reloadOnScreenWidgetViews]; - strongSelf->_oscProfilesTableViewController.currentOSCButtonLayers = weakSelf.layoutOSC.OSCButtonLayerPool; //pass updated OSCLayout to OSCProfileTableView again + strongSelf->_profileSelectorViewController.currentOSCButtonLayers = weakSelf.layoutOSC.OSCButtonLayerPool; // pass updated OSCLayout to profile selector again }; - [self.oscProfilesTableViewController.tableView reloadData]; [self reloadOnScreenWidgetViews]; NSLog(@"profileRefresh %f", CACurrentMediaTime()); @@ -1906,43 +1897,34 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { // [self presentViewController:vc animated:YES completion:nil]; } -- (void) presentProfilesTableViewWithPickProfile:(bool)pickProfile{ +- (void) presentProfileSelectorWithPickProfile:(bool)pickProfile{ [self hideStickIndicators]; selectedWidgetView = nil; // if(pickProfile) [self clearOnScreenWidgets]; - - UIStoryboard *storyboard; - BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); - if (isIPhone) { - storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil]; - } - else { - storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil]; - } - - _oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"]; - _oscProfilesTableViewController.layoutViewBounds = self.view.bounds; + + _profileSelectorViewController = [[ProfileSelectorViewController alloc] init]; + _profileSelectorViewController.layoutViewBounds = self.view.bounds; __weak typeof(self) weakSelf = self; - _oscProfilesTableViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC ofile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the proffile and then hide/show and move each OSC button to their appropriate position + _profileSelectorViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'ProfileSelectorViewController' VC is dismissed. By the time it is dismissed the user would have potentially selected a different OSC profile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position if(!pickProfile) [weakSelf reloadOnScreenWidgetViews]; }; self.widgetPanelStack.hidden = YES; - _oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayerPool; + _profileSelectorViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayerPool; - // _oscProfilesTableViewController.modalPresentationStyle = UIModalPresentationCurrentContext; - _oscProfilesTableViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext; - // _oscProfilesTableViewController.modalPresentationStyle = UIModalPresentationPageSheet; - _oscProfilesTableViewController.pickProfileEnabled = pickProfile; - [self presentViewController:_oscProfilesTableViewController animated:NO completion:nil]; + // _profileSelectorViewController.modalPresentationStyle = UIModalPresentationCurrentContext; + _profileSelectorViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext; + // _profileSelectorViewController.modalPresentationStyle = UIModalPresentationPageSheet; + _profileSelectorViewController.pickProfileEnabled = pickProfile; + [self presentViewController:_profileSelectorViewController animated:NO completion:nil]; } /* Presents the view controller that lists all OSC profiles the user can choose from */ - (IBAction) loadTapped:(id)sender { [self saveTapped:nil]; - [self presentProfilesTableViewWithPickProfile:false]; + [self presentProfileSelectorWithPickProfile:false]; } @@ -2081,7 +2063,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { if([profilesManager getIndexOfSelectedProfile] == 0 && [self.layoutOSC.layoutChanges count] > 0){ UIAlertController * movedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Layout of the Default profile can not be changed"] preferredStyle:UIAlertControllerStyleAlert]; [movedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - [self.oscProfilesTableViewController profileViewRefresh]; + [self.profileSelectorViewController profileViewRefresh]; }]]; [self presentViewController:movedAlertController animated:YES completion:nil]; } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift index babc1980..f14574cc 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift @@ -188,7 +188,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg @IBOutlet private weak var toolbarTopConstraintiPhone: NSLayoutConstraint! @IBOutlet private weak var toolbarTopConstraintiPad: NSLayoutConstraint! - var oscProfilesTableViewController: OSCProfilesTableViewController? + var profileSelectorViewController: ProfileSelectorViewController? private var profilesManager: OSCProfilesManager! private var bulkEditEnabled: Bool = false private var selectedWidget: OnScreenWidgetView? @@ -286,7 +286,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NotificationCenter.default.addObserver(self, selector: #selector(legacyOscLayerTapped(_:)), name: Notification.Name("LegacyOscCALayerSelectedNotification"), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleProfileTablViewDismiss), name: Notification.Name("OscLayoutTableViewCloseNotification"), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleProfileSelectorDismiss), name: Notification.Name("ProfileSelectorCloseNotification"), object: nil) // NotificationCenter.default.addObserver(self, selector: #selector(dummytest), name: Notification.Name("GameProfileSelectedNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(widgetViewTapped(_:)), name: Notification.Name("OnScreenWidgetViewSelected"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(OSCLayoutChanged), name: Notification.Name("OSCLayoutChanged"), object: nil) @@ -358,7 +358,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg } @objc func reloadOnScreenWidgetViews() { - if self.profileTableLoadingMode == .selectProfileFromMainFrame { return } + if self.profileSelectorLoadingMode == .selectProfileFromMainFrame { return } OnScreenWidgetView.isTweakingHighlight = false OnScreenWidgetView.editMode = true @@ -369,7 +369,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg guard let oscProfile = self.profilesManager?.getSelectedProfile() else { return } - if self.profileTableLoadingMode == .selectProfileFromStreamView { + if self.profileSelectorLoadingMode == .selectProfileFromStreamView || self.profileSelectorLoadingMode == .pickProfile { NotificationCenter.default.post(name: Notification.Name("GameProfileSelectedNotification"), object: oscProfile) } @@ -525,66 +525,68 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg } @objc func profileRefresh() { - let storyboardName = UIDevice.current.userInterfaceIdiom == .phone ? "iPhone" : "iPad" - let storyboard = UIStoryboard(name: storyboardName, bundle: nil) - oscProfilesTableViewController = storyboard.instantiateViewController(withIdentifier: "OSCProfilesTableViewController") as? OSCProfilesTableViewController - oscProfilesTableViewController?.needToUpdateOscLayoutTVC = { [weak self] in + profileSelectorViewController = ProfileSelectorViewController() + profileSelectorViewController?.layoutViewBounds = view.bounds + profileSelectorViewController?.needToUpdateOscLayoutTVC = { [weak self] in guard let self else { return } if !loadJustTapped { self.reloadOnScreenWidgetViews() } loadJustTapped = false - self.oscProfilesTableViewController?.currentOSCButtonLayers = self.layoutOSC.oscButtonLayerPool + self.profileSelectorViewController?.currentOSCButtonLayers = self.layoutOSC.oscButtonLayerPool } - oscProfilesTableViewController?.tableView?.reloadData() reloadOnScreenWidgetViews() } - @objc var profileTableLoadingMode: OSCProfilesTableViewLoadingMode = .selectProfile - @objc(presentProfilesTableViewWithLoadingMode: animated:) - func presentProfilesTableView(with loadingMode: OSCProfilesTableViewLoadingMode, animated: Bool = false) { - profileTableLoadingMode = loadingMode - presentProfilesTableView(with: loadingMode, pickedProfileDataHandler: nil, animated:animated) + @objc var profileSelectorLoadingMode: ProfileSelectorLoadingMode = .selectProfile + + @objc(presentProfileSelectorWithLoadingMode:) + func presentProfileSelector(with loadingModeRawValue: Int) { + guard let loadingMode = ProfileSelectorLoadingMode(rawValue: loadingModeRawValue) else { + return + } + presentProfileSelector(with: loadingMode) } - private func presentProfilesTableView( - with loadingMode: OSCProfilesTableViewLoadingMode, + @objc func presentProfileSelector(with loadingMode: ProfileSelectorLoadingMode, animated: Bool = false) { + profileSelectorLoadingMode = loadingMode + presentProfileSelector(with: loadingMode, pickedProfileDataHandler: nil, animated:animated) + } + + private func presentProfileSelector( + with loadingMode: ProfileSelectorLoadingMode, pickedProfileDataHandler: ((OSCProfile) -> Void)? = nil, animated: Bool = false ) { hideStickIndicators() if loadingMode != .pickProfileData {selectedWidget = nil} - let storyboardName = UIDevice.current.userInterfaceIdiom == .phone ? "iPhone" : "iPad" - let storyboard = UIStoryboard(name: storyboardName, bundle: nil) - let controller = storyboard.instantiateViewController(withIdentifier: "OSCProfilesTableViewController") as? OSCProfilesTableViewController - controller?.layoutViewBounds = view.bounds - controller?.needToUpdateOscLayoutTVC = { [weak self] in + let controller = ProfileSelectorViewController() + controller.layoutViewBounds = view.bounds + controller.needToUpdateOscLayoutTVC = { [weak self] in guard let self else { return } - if loadingMode == .selectProfile || loadingMode == .selectProfileFromStreamView { + if loadingMode == .selectProfile || loadingMode == .selectProfileFromStreamView || loadingMode == .pickProfile { self.reloadOnScreenWidgetViews() } } - controller?.currentOSCButtonLayers = layoutOSC.oscButtonLayerPool - controller?.modalPresentationStyle = .overCurrentContext - controller?.loadingMode = loadingMode - controller?.pickedProfileDataHandler = pickedProfileDataHandler + controller.currentOSCButtonLayers = layoutOSC.oscButtonLayerPool + controller.modalPresentationStyle = .overCurrentContext + controller.loadingMode = loadingMode + controller.pickedProfileDataHandler = pickedProfileDataHandler widgetPanelStack.isHidden = true - oscProfilesTableViewController = controller - if let controller { - present(controller, animated: animated) - } + profileSelectorViewController = controller + present(controller, animated: animated) } private var loadJustTapped:Bool = false @IBAction func loadTapped(_ sender: Any?) { // saveTapped(nil) loadJustTapped = true - presentProfilesTableView(with: .selectProfile) + presentProfileSelector(with: .selectProfile) } @IBAction func importFromOtherButtonTapped(_ sender: Any?) { // importFromOtherButton.setTitle(LocalizationHelper.localizedString(forKey: "Import"), for: .normal) - presentProfilesTableView(with: .pickProfileData) { [weak self] profile in + presentProfileSelector(with: .pickProfileData) { [weak self] profile in if profile.name == self?.profilesManager.getSelectedProfile().name {return} self?.loadWidgets(from: profile, to: self?.selectedWidget) } @@ -647,8 +649,8 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg */ @objc private func handleReturnToForeground() { - if (self.profileTableLoadingMode != .selectProfileFromMainFrame - && self.profileTableLoadingMode != .selectProfileFromMainFrame) + if (self.profileSelectorLoadingMode != .selectProfileFromMainFrame + && self.profileSelectorLoadingMode != .selectProfileFromMainFrame) {setupWidgetPanel()} updateViewBounds() } @@ -657,7 +659,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg super.viewWillTransition(to: size, with: coordinator) viewWillBeResized = true hideStickIndicators() - if self.profileTableLoadingMode == .selectProfile { + if self.profileSelectorLoadingMode == .selectProfile { let oldSize = view.bounds.size let scaleX = size.width / oldSize.width @@ -710,7 +712,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg viewWillBeResized = false selectedWidget = nil selectedControllerLayer = nil - oscProfilesTableViewController?.layoutViewBounds = view.bounds + profileSelectorViewController?.layoutViewBounds = view.bounds OSCProfilesManager.setLayoutViewBounds(view.bounds) OSCProfilesManager.setOnScreenWidgetViewsSet(onScreenWidgetViews) // reloadOnScreenWidgetViews() @@ -1560,7 +1562,6 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg let alert = UIAlertController(title: success ? LocalizationHelper.localizedString(forKey: "Profile updated successfully") : "", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in if !success { - // self.oscProfilesTableViewController?.profileViewRefresh() } }) present(alert, animated: true) @@ -1628,11 +1629,11 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg } } - @objc private func handleProfileTablViewDismiss() { + @objc private func handleProfileSelectorDismiss() { - switch profileTableLoadingMode { + switch profileSelectorLoadingMode { case .selectProfile: - if oscProfilesTableViewController?.loadingMode != .pickProfileData { + if profileSelectorViewController?.loadingMode != .pickProfileData { setupWidgetPanel() } else { @@ -1647,17 +1648,6 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg case .pickProfileData: widgetPanelStack.isHidden = false } - - /* - if quickSwitchEnabled { - clearOnScreenWidgets() - dismiss(animated: false) - } else { - if oscProfilesTableViewController?.loadingMode != .pickProfileData { - setupWidgetPanel() - } - else {widgetPanelStack.isHidden = false} - }*/ } private func addInnerAnalogSticksToOuterAnalogLayers() { @@ -1731,7 +1721,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg } private func setupWidgetPanel() { - widgetPanelStack.isHidden = profileTableLoadingMode != .selectProfile + widgetPanelStack.isHidden = profileSelectorLoadingMode != .selectProfile tipTitleLabel.textAlignment = .left tipTitleLabel.contentMode = .top tipTitleLabel.lineBreakMode = .byWordWrapping @@ -2590,7 +2580,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg if profilesManager.getIndexOfSelectedProfile() == 0, layoutOSC.layoutChanges.count > 0 { let alert = UIAlertController(title: "", message: LocalizationHelper.localizedString(forKey: "Layout of the Default profile can not be changed"), preferredStyle: .alert) alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in - self.oscProfilesTableViewController?.profileViewRefresh() + self.profileSelectorViewController?.profileViewRefresh() }) present(alert, animated: true) } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift deleted file mode 100644 index 77d12547..00000000 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Table/OSCProfilesTableViewController.swift +++ /dev/null @@ -1,662 +0,0 @@ -// -// 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 - -final class ProfileTableViewCell: UITableViewCell { - @IBOutlet weak var name: UILabel! -} - -@objc(FileOperation) -enum FileOperation: Int { - case importOperation = 0 - case exportOperation = 1 -} - -@objc enum OSCProfilesTableViewLoadingMode: Int { - case selectProfile - case selectProfileFromStreamView - case selectProfileFromMainFrame - case pickProfile - case pickProfileData -} - -@objcMembers -final class OSCProfilesTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIDocumentPickerDelegate, UIGestureRecognizerDelegate, ControllerUINavigationDelegate { - - @IBOutlet weak var tableView: UITableView! - @IBOutlet weak var profileTableViewNavigationBar: UINavigationBar! - @IBOutlet private weak var profileTableViewNavigationItem: UINavigationItem! - - var currentFileOperation: FileOperation = .importOperation - var needToUpdateOscLayoutTVC: (() -> Void)? - var loadingMode: OSCProfilesTableViewLoadingMode = .selectProfile - @nonobjc var pickedProfileDataHandler: ((OSCProfile) -> Void)? - weak var currentOSCButtonLayers: NSMutableSet? - var layoutViewBounds: CGRect = .zero - - private var profilesManager: OSCProfilesManager! - private var horizontalConstraintsConfigured = false - private var profiles: NSMutableArray = [] - private var selectedProfileIndex = 0 - private var profilesLoadGeneration = 0 - - private func contentWidthMultiplier() -> CGFloat { - PublicUtils.viewIsLandscape(view) ? (PublicUtils.isIPhone ? 0.8 : 0.65) : (PublicUtils.isIPhone ? 0.83 : 0.85) - } - - private func updateHorizontalLayoutConstraints() { - let horizontalConstraints = view.constraints.filter { constraint in - let firstItem = constraint.firstItem as? UIView - let secondItem = constraint.secondItem as? UIView - let involvesManagedView = firstItem == tableView || - firstItem == profileTableViewNavigationBar || - secondItem == tableView || - secondItem == profileTableViewNavigationBar - let isHorizontalConstraint: Bool = { - switch (constraint.firstAttribute, constraint.secondAttribute) { - case (.leading, _), (.trailing, _), (.left, _), (.right, _), (.width, _), (.centerX, _), - (_, .leading), (_, .trailing), (_, .left), (_, .right), (_, .width), (_, .centerX): - return true - default: - return false - } - }() - - return involvesManagedView && isHorizontalConstraint - } - - NSLayoutConstraint.deactivate(horizontalConstraints) - - [tableView, profileTableViewNavigationBar].forEach { controlledView in - controlledView?.translatesAutoresizingMaskIntoConstraints = false - } - - NSLayoutConstraint.activate([ - profileTableViewNavigationBar.centerXAnchor.constraint(equalTo: view.centerXAnchor), - profileTableViewNavigationBar.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: contentWidthMultiplier()), - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - tableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: contentWidthMultiplier()), - ]) - } - - 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 viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - if #available(iOS 13.0, *) { - ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: 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 PublicUtils.liquidGlassEnabled { - tableView.separatorColor = UIColor.white.withAlphaComponent(PublicUtils.isIPhone ? 0.28 : 0.165) - } else { - tableView.separatorColor = UIColor.white.withAlphaComponent(0.3) - } - } - - override func viewDidLoad() { - super.viewDidLoad() - - profilesManager = OSCProfilesManager.sharedManager(layoutViewBounds) - configureTableView() - updateHorizontalLayoutConstraints() - horizontalConstraintsConfigured = true - tableView.alpha = 1 - tableView.backgroundColor = UIColor.black.withAlphaComponent(self.loadingMode == .selectProfileFromMainFrame ? ( - ThemeManager.userInterfaceStyle() == .dark ? 0.83 : 0.6 - ) : 0.43) - - let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf)) - tap.cancelsTouchesInView = false - tap.delegate = self - view.addGestureRecognizer(tap) - - if PublicUtils.liquidGlassEnabled, #available(iOS 26.0, *) { - profileTableViewNavigationItem.leftBarButtonItems?.forEach { $0.hidesSharedBackground = true } - profileTableViewNavigationItem.rightBarButtonItems?.forEach { $0.hidesSharedBackground = true } - } - - reloadProfilesAsync(scrollToSelected: true) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - coordinator.animate(alongsideTransition: { _ in - self.updateHorizontalLayoutConstraints() - self.view.layoutIfNeeded() - }) - } - - @objc private func dismissSelf() { - dismiss(animated: false) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !horizontalConstraintsConfigured { - updateHorizontalLayoutConstraints() - horizontalConstraintsConfigured = true - } - configureTableView() - if profiles.count > 0 { - scrollToSelectedProfile(animated: true) - } else { - reloadProfilesAsync(scrollToSelected: true) - } - - if (loadingMode == .selectProfileFromMainFrame || loadingMode == .selectProfileFromStreamView) - && ControllerUtil.primaryGCController != nil { - if #available(iOS 13.0, *) { - ControllerNavigator.setUINavigationDelegate(self) - } - } - } - - private func configureTableView() { - tableView.tableHeaderView = UIView(frame: .zero) - tableView.delegate = self - tableView.dataSource = self - tableView.register(UINib(nibName: "ProfileTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell") - } - - private func reloadProfilesAsync(scrollToSelected: Bool = false, notify: Bool = false) { - profilesLoadGeneration += 1 - let generation = profilesLoadGeneration - let manager = profilesManager - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let manager else { return } - - let loadedProfiles = manager.getAllProfiles() - let loadedSelectedIndex = Self.selectedProfileIndex(in: loadedProfiles) - - DispatchQueue.main.async { [weak self] in - guard let self, generation == self.profilesLoadGeneration else { return } - - self.profiles = loadedProfiles - self.selectedProfileIndex = loadedSelectedIndex - self.tableView.reloadData() - - if scrollToSelected { - self.scrollToSelectedProfile(animated: true) - } - - if notify { - self.notifyProfileViewDidRefresh() - } - } - } - } - - private func scrollToSelectedProfile(animated: Bool) { - guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex) else { - return - } - - let indexPath = IndexPath(row: selectedProfileIndex, section: 0) - tableView.scrollToRow(at: indexPath, at: .middle, animated: animated) - } - - private func moveSelectedProfile(by offset: Int) { - PublicUtils.runOnMain { [weak self] in - guard let self, - self.profiles.count > 0, - self.tableView.numberOfSections > 0, - self.tableView.numberOfRows(inSection: 0) > 0 else { - return - } - - let maxIndex = min(self.profiles.count, self.tableView.numberOfRows(inSection: 0)) - 1 - let currentIndex = min(max(self.selectedProfileIndex, 0), maxIndex) - let nextIndex = min(max(currentIndex + offset, 0), maxIndex) - let nextIndexPath = IndexPath(row: nextIndex, section: 0) - - self.tableView.selectRow(at: nextIndexPath, animated: true, scrollPosition: .middle) - self.tableView(self.tableView, didSelectRowAt: nextIndexPath) - } - } - - private func validProfileIndex(_ index: Int) -> Int? { - guard profiles.count > 0 else { return nil } - return min(max(index, 0), profiles.count - 1) - } - - private func notifyProfileViewDidRefresh() { - needToUpdateOscLayoutTVC?() - if loadingMode != .selectProfileFromStreamView - && loadingMode != .pickProfile - && loadingMode != .pickProfileData - && loadingMode != .selectProfile - { - guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex) else { return } - NotificationCenter.default.post(name: Notification.Name("GameProfileSelectedNotification"), object: self.profiles[selectedProfileIndex]) - } - } - - private static func selectedProfileIndex(in profiles: NSMutableArray) -> Int { - for index in 0.. Int { - profiles.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? ProfileTableViewCell, - indexPath.row < profiles.count, - let profile = profiles.object(at: indexPath.row) as? OSCProfile, - let nameLabel = cell.name else { - return UITableViewCell() - } - - nameLabel.text = profile.name.localizedProfileName - 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 loadingMode != .pickProfileData, - indexPath.row == selectedProfileIndex { - 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) { - guard indexPath.row < profiles.count else { return } - - 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: LocalizationHelper.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() - } - - reloadProfilesAsync() - } - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard indexPath.section == 0, - indexPath.row >= 0, - indexPath.row < profiles.count else { - return - } - - if loadingMode == .pickProfileData, - let pickedProfile = profiles.object(at: indexPath.row) as? OSCProfile { - pickedProfileDataHandler?(pickedProfile) - dismiss(animated: false) - return - } - - let selectedIndexPath = IndexPath(row: indexPath.row, section: 0) - let lastSelectedIndex = validProfileIndex(selectedProfileIndex) - let lastSelectedIndexPath = lastSelectedIndex.map { IndexPath(row: $0, section: 0) } - - if selectedIndexPath != lastSelectedIndexPath { - if let lastSelectedIndexPath { - let previousCell = tableView.cellForRow(at: lastSelectedIndexPath) - previousCell?.accessoryType = .none - previousCell?.accessoryView = nil - } - - profilesManager.setProfileToSelected(UInt32(indexPath.row)) - selectedProfileIndex = indexPath.row - - let selectedCell = tableView.cellForRow(at: selectedIndexPath) - selectedCell?.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 - selectedCell?.accessoryView = checkmarkLabel - - let rowsToReload = [lastSelectedIndexPath, selectedIndexPath] - .compactMap { $0 } - .filter { - $0.section < tableView.numberOfSections && - $0.row >= 0 && - $0.row < tableView.numberOfRows(inSection: $0.section) - } - if !rowsToReload.isEmpty { - tableView.reloadRows(at: rowsToReload, with: .none) - } - } - profileViewRefresh() - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - if (loadingMode != .selectProfile - && loadingMode != .selectProfileFromMainFrame - && loadingMode != .selectProfileFromStreamView - ), - let touchView = touch.view, - touchView.isDescendant(of: tableView) { - return true - } - return touch.view == view - } - - @available(iOS 13.0, *) - func getNavigationElements() -> [ControllerNavigationElement] { - var elements: [ControllerNavigationElement] = [] - elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "previousItem")) - elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadDown : .a, action: "nextItem")) - elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .x, action: "exit")) - return elements - } - - func navigateByController(forward: Bool) { - } - - func navigateByController(downward: Bool) { - - } - - func persistControllerNavigationHighlight() {} - func restoreControllerNavigationHighlight() {} - func restoreControllerNavigationHighlightAfterSettingsModeSwitch() {} - - func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) { - } - - func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) { - guard pressed else { return } - PublicUtils.runOnMain { [weak self] in - switch navigation.action { - case "previousItem": - self?.moveSelectedProfile(by: -1) - case "nextItem": - self?.moveSelectedProfile(by: 1) - case "exit": - self?.dismiss(animated: false) - default: - break - } - } - } -} diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift new file mode 100644 index 00000000..262c671e --- /dev/null +++ b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift @@ -0,0 +1,1622 @@ +// +// ProfileSelectorViewController.swift +// VoidLink +// +// Created by True砖家 on 2026.7.28 +// Copyright © 2026 True砖家 on Bilibili. All rights reserved. +// + +import Compression +import UIKit + +private final class ProfileCollectionViewCell: UICollectionViewCell, ControllerNavigationHighlightTargetProviding { + static let reuseIdentifier = "ProfileCollectionViewCell" + + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + private let checkmarkLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + } + + var controllerNavigationHighlightTargetView: UIView { + contentView + } + + private func setupViews() { + contentView.layer.cornerRadius = PublicUtils.isIPhone ? 14 : 16 + contentView.layer.masksToBounds = true + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = UIFont.systemFont(ofSize: PublicUtils.isIPhone ? 15 : 18, weight: .semibold) + titleLabel.numberOfLines = 2 + titleLabel.adjustsFontSizeToFitWidth = true + titleLabel.minimumScaleFactor = 0.72 + + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel.font = UIFont.systemFont(ofSize: PublicUtils.isIPhone ? 10 : 12, weight: .medium) + subtitleLabel.numberOfLines = 1 + + checkmarkLabel.translatesAutoresizingMaskIntoConstraints = false + checkmarkLabel.text = "✓" + checkmarkLabel.font = UIFont.systemFont(ofSize: PublicUtils.isIPhone ? 20 : 24, weight: .bold) + checkmarkLabel.textAlignment = .center + checkmarkLabel.textColor = .systemGreen + + contentView.addSubview(titleLabel) + contentView.addSubview(subtitleLabel) + contentView.addSubview(checkmarkLabel) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: PublicUtils.isIPhone ? 10 : 14), + titleLabel.trailingAnchor.constraint(equalTo: checkmarkLabel.leadingAnchor, constant: -8), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: PublicUtils.isIPhone ? 9 : 13), + + subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), + subtitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: PublicUtils.isIPhone ? -8 : -11), + + checkmarkLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: PublicUtils.isIPhone ? -9 : -12), + checkmarkLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + checkmarkLabel.widthAnchor.constraint(equalToConstant: PublicUtils.isIPhone ? 24 : 30), + checkmarkLabel.heightAnchor.constraint(equalTo: checkmarkLabel.widthAnchor), + ]) + } + + func configure(title: String, selected: Bool, pickingData: Bool, userInterfaceStyle: UIUserInterfaceStyle) { + let darkMode = userInterfaceStyle == .dark + titleLabel.text = title + subtitleLabel.text = selected && !pickingData ? "Selected".localized : "Profile".localized + checkmarkLabel.isHidden = !selected || pickingData + + titleLabel.textColor = darkMode ? .white : UIColor(white: 0.08, alpha: 1) + subtitleLabel.textColor = darkMode ? UIColor(white: 1, alpha: 0.5) : UIColor(white: 0, alpha: 0.45) + + if selected && !pickingData { + /* contentView.backgroundColor = darkMode + ? UIColor.systemGreen.withAlphaComponent(0.22) + : UIColor.systemGreen.withAlphaComponent(0.16) */ + contentView.backgroundColor = darkMode ? .clear : .clear + contentView.layer.borderWidth = darkMode ? 2 : 3 + contentView.layer.borderColor = UIColor.systemGreen.withAlphaComponent(0.65).cgColor + } else { + /* + contentView.backgroundColor = darkMode + ? UIColor(white: 1.0, alpha: 0.075) + : UIColor(white: 1.0, alpha: 0.56) */ + contentView.backgroundColor = darkMode ? .clear : .clear + contentView.layer.borderWidth = 1 + contentView.layer.borderColor = darkMode + ? UIColor(white: 1.0, alpha: 0.23).cgColor + : UIColor(white: 0.0, alpha: 0.3).cgColor + } + } + +} + +@objc(FileOperation) +enum FileOperation: Int { + case importOperation = 0 + case exportOperation = 1 +} + +private enum ProfileExportScope { + case selectedProfile + case allProfiles +} + +private enum ProfilePayloadCompression: UInt8 { + case none = 0 + case lzfse = 1 + case lz4 = 2 + case lzma = 3 + case zlib = 4 + + var systemAlgorithm: compression_algorithm? { + switch self { + case .none: + return nil + case .lzfse: + return COMPRESSION_LZFSE + case .lz4: + return COMPRESSION_LZ4 + case .lzma: + return COMPRESSION_LZMA + case .zlib: + return COMPRESSION_ZLIB + } + } +} + +private enum ProfileFileContainer { + private static let magic = Array("VLPR".utf8) + private static let version: UInt8 = 1 + private static let headerSize = 14 + private static let defaultCompression: ProfilePayloadCompression = .lz4 + + static func packedData(from payload: Data) -> Data { + pack(payload, compression: defaultCompression) + } + + static func unpackedData(from data: Data) throws -> Data { + guard hasHeader(data) else { + return data + } + + let header = Array(data.prefix(headerSize)) + guard header[4] == version, + let compression = ProfilePayloadCompression(rawValue: header[5]) else { + throw NSError(domain: "ProfileFileContainer", code: 1) + } + + let originalLength = UInt64(littleEndianBytes: Array(header[6..<14])) + let payload = data.dropFirst(headerSize) + + guard let algorithm = compression.systemAlgorithm else { + return Data(payload) + } + + guard let unpacked = decompress(Data(payload), originalLength: Int(originalLength), algorithm: algorithm) else { + throw NSError(domain: "ProfileFileContainer", code: 2) + } + + return unpacked + } + + private static func pack(_ payload: Data, compression: ProfilePayloadCompression) -> Data { + guard let algorithm = compression.systemAlgorithm, + let compressedPayload = compress(payload, algorithm: algorithm) else { + return header(compression: .none, originalLength: payload.count) + payload + } + + return header(compression: compression, originalLength: payload.count) + compressedPayload + } + + private static func hasHeader(_ data: Data) -> Bool { + data.count >= headerSize && Array(data.prefix(magic.count)) == magic + } + + private static func header(compression: ProfilePayloadCompression, originalLength: Int) -> Data { + var data = Data(magic) + data.append(version) + data.append(compression.rawValue) + + var length = UInt64(originalLength).littleEndian + withUnsafeBytes(of: &length) { bytes in + data.append(contentsOf: bytes) + } + + return data + } + + private static func compress(_ data: Data, algorithm: compression_algorithm) -> Data? { + if data.isEmpty { + return Data() + } + + return data.withUnsafeBytes { sourceBuffer in + guard let source = sourceBuffer.bindMemory(to: UInt8.self).baseAddress else { + return nil + } + + var destinationSize = max(64, data.count + data.count / 16 + 64) + let maximumDestinationSize = max(destinationSize, data.count * 4 + 1024) + + while destinationSize <= maximumDestinationSize { + var destination = [UInt8](repeating: 0, count: destinationSize) + let encodedCount = compression_encode_buffer( + &destination, + destinationSize, + source, + data.count, + nil, + algorithm + ) + + if encodedCount > 0 { + return Data(destination.prefix(encodedCount)) + } + + destinationSize *= 2 + } + + return nil + } + } + + private static func decompress(_ data: Data, originalLength: Int, algorithm: compression_algorithm) -> Data? { + if originalLength == 0 { + return Data() + } + + return data.withUnsafeBytes { sourceBuffer in + guard let source = sourceBuffer.bindMemory(to: UInt8.self).baseAddress else { + return nil + } + + var destination = [UInt8](repeating: 0, count: originalLength) + let decodedCount = compression_decode_buffer( + &destination, + originalLength, + source, + data.count, + nil, + algorithm + ) + + guard decodedCount == originalLength else { + return nil + } + + return Data(destination) + } + } +} + +private extension UInt64 { + init(littleEndianBytes bytes: [UInt8]) { + var value: UInt64 = 0 + for (index, byte) in bytes.enumerated() { + value |= UInt64(byte) << UInt64(index * 8) + } + self = value + } +} + +@objc enum ProfileSelectorLoadingMode: Int { + case selectProfile + case selectProfileFromStreamView + case selectProfileFromMainFrame + case pickProfile + case pickProfileData +} + +@objcMembers +final class ProfileSelectorViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UIDocumentPickerDelegate, UIGestureRecognizerDelegate, ControllerUINavigationDelegate { + + var currentFileOperation: FileOperation = .importOperation + private var currentExportScope: ProfileExportScope = .allProfiles + var needToUpdateOscLayoutTVC: (() -> Void)? + var loadingMode: ProfileSelectorLoadingMode = .selectProfile + @nonobjc var pickedProfileDataHandler: ((OSCProfile) -> Void)? + weak var currentOSCButtonLayers: NSMutableSet? + var layoutViewBounds: CGRect = .zero + + private var profilesManager: OSCProfilesManager! + private var horizontalConstraintsConfigured = false + private var profiles: NSMutableArray = [] + private var selectedProfileIndex = 0 + private var profilesLoadGeneration = 0 + private var profileCollectionView: UICollectionView! + private let contentView = UIView() + private let toolbarView = UIView() + private let toolbarStackView = UIStackView() + private let duplicateButton = UIButton(type: .system) + private let deleteButton = UIButton(type: .system) + private let importButton = UIButton(type: .system) + private let exportButton = UIButton(type: .system) + private let restoreButton = UIButton(type: .system) + private var panelConstraints: [NSLayoutConstraint] = [] + private var pendingTransitionSize: CGSize? + private var presentationWindow: UIWindow? + private var themeObserver: NSObjectProtocol? + private var profileOrderChanged = false + private var controllerNavigationReorderActive = false + private var controllerNavigationReorderUpdateInFlight = false + private var pendingControllerNavigationReorderMove: (horizontal: Int, vertical: Int)? + private var controllerNavigationReorderSnapshotView: UIView? + private var controllerNavigationReorderHiddenIndexPath: IndexPath? + private var controllerNavigationPersistOrderWhenReorderSettles = false + private var controllerNavigationReorderPressBeganAt: CFTimeInterval? + private var controllerNavigationDeleteDoublePressWindowOpen = false + private var controllerNavigationDeleteDoublePressToken = 0 + + private func contentWidthMultiplier() -> CGFloat { + isLayoutLandscape() ? (PublicUtils.isIPhone ? 0.62 : 0.595) : (PublicUtils.isIPhone ? 0.88 : 0.86) + } + + private func updateHorizontalLayoutConstraints() { + setupPanelConstraints() + } + + private func isLayoutLandscape() -> Bool { + if let pendingTransitionSize { + return pendingTransitionSize.width > pendingTransitionSize.height + } + return PublicUtils.viewIsLandscape(view) + } + + 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) + if isBeingDismissed || navigationController?.isBeingDismissed == true { + persistProfileOrderIfNeeded(notify: false) + } + NotificationCenter.default.post(name: Notification.Name("ProfileSelectorCloseNotification"), object: self) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + if #available(iOS 13.0, *) { + cancelControllerNavigationReorderIfNeeded() + ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self) + } + if isBeingDismissed || navigationController?.isBeingDismissed == true { + presentationWindow?.isHidden = true + presentationWindow = nil + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + profilesManager = OSCProfilesManager.sharedManager(layoutViewBounds) + observeThemeChanges() + setupProfilePanel() + updateHorizontalLayoutConstraints() + horizontalConstraintsConfigured = true + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf)) + tap.cancelsTouchesInView = false + tap.delegate = self + view.addGestureRecognizer(tap) + + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleCollectionLongPress(_:))) + longPress.minimumPressDuration = 0.25 + profileCollectionView.addGestureRecognizer(longPress) + + reloadProfilesAsync(scrollToSelected: true) + } + + deinit { + removeThemeObserver() + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate(alongsideTransition: { _ in + self.pendingTransitionSize = size + self.updateHorizontalLayoutConstraints() + self.profileCollectionView?.collectionViewLayout.invalidateLayout() + self.view.layoutIfNeeded() + }, completion: { _ in + self.pendingTransitionSize = nil + self.updateHorizontalLayoutConstraints() + self.profileCollectionView?.collectionViewLayout.invalidateLayout() + self.view.layoutIfNeeded() + }) + } + + @objc private func dismissSelf() { + dismiss(animated: false) + } + + @objc func presentInKeyWindow(animated: Bool) { + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + + guard #available(iOS 13.0, *), + let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) else { + PublicUtils.topViewController()?.present(self, animated: animated) + return + } + + let window = UIWindow(windowScene: windowScene) + let rootViewController = UIViewController() + rootViewController.view.backgroundColor = .clear + window.rootViewController = rootViewController + window.windowLevel = .alert + 1 + window.backgroundColor = .clear + window.isHidden = false + window.makeKeyAndVisible() + presentationWindow = window + + rootViewController.present(self, animated: animated) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !horizontalConstraintsConfigured { + updateHorizontalLayoutConstraints() + horizontalConstraintsConfigured = true + } + if profiles.count > 0 { + scrollToSelectedProfile(animated: true) + } else { + reloadProfilesAsync(scrollToSelected: true) + } + + if (loadingMode == .selectProfileFromMainFrame || loadingMode == .selectProfileFromStreamView) + && ControllerUtil.primaryGCController != nil { + if #available(iOS 13.0, *) { + ControllerNavigator.setUINavigationDelegate(self) + } + } + } + + private func setupProfilePanel() { + view.subviews.forEach { $0.removeFromSuperview() } + view.backgroundColor = .clear + + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.layer.cornerRadius = PublicUtils.isIPhone ? 18 : 22 + contentView.layer.masksToBounds = true + view.addSubview(contentView) + + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.minimumInteritemSpacing = gridSpacing() + layout.minimumLineSpacing = gridSpacing() + + profileCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + profileCollectionView.translatesAutoresizingMaskIntoConstraints = false + profileCollectionView.backgroundColor = collectionSurfaceColor() + profileCollectionView.layer.cornerRadius = PublicUtils.isIPhone ? 14 : 18 + profileCollectionView.layer.masksToBounds = true + profileCollectionView.alwaysBounceVertical = true + profileCollectionView.dataSource = self + profileCollectionView.delegate = self + profileCollectionView.allowsSelection = true + profileCollectionView.contentInsetAdjustmentBehavior = .never + profileCollectionView.register(ProfileCollectionViewCell.self, forCellWithReuseIdentifier: ProfileCollectionViewCell.reuseIdentifier) + + toolbarView.translatesAutoresizingMaskIntoConstraints = false + toolbarView.layer.cornerRadius = PublicUtils.isIPhone ? 13 : 16 + toolbarView.layer.masksToBounds = true + + toolbarStackView.translatesAutoresizingMaskIntoConstraints = false + toolbarStackView.axis = .horizontal + toolbarStackView.alignment = .fill + toolbarStackView.distribution = .fillEqually + toolbarStackView.spacing = PublicUtils.isIPhone ? 4 : 8 + + configureToolbarButton(duplicateButton, title: "Duplicate".localized, symbolName: "plus.square.on.square") + configureToolbarButton(deleteButton, title: "Delete".localized, symbolName: "minus.circle") + configureToolbarButton(importButton, title: "Import".localized, symbolName: "square.and.arrow.down") + configureToolbarButton(exportButton, title: "Export".localized, symbolName: "square.and.arrow.up") + configureToolbarButton(restoreButton, title: "Restore".localized, symbolName: "arrow.counterclockwise") + + duplicateButton.addTarget(self, action: #selector(duplicateTapped(_:)), for: .touchUpInside) + deleteButton.addTarget(self, action: #selector(deleteTapped(_:)), for: .touchUpInside) + importButton.addTarget(self, action: #selector(importDataTapped(_:)), for: .touchUpInside) + exportButton.addTarget(self, action: #selector(exportDataTapped(_:)), for: .touchUpInside) + restoreButton.addTarget(self, action: #selector(restoreTapped(_:)), for: .touchUpInside) + + contentView.addSubview(profileCollectionView) + contentView.addSubview(toolbarView) + toolbarView.addSubview(toolbarStackView) + [duplicateButton, deleteButton, importButton, exportButton, restoreButton].forEach(toolbarStackView.addArrangedSubview) + + updatePanelTheme() + updateToolbarVisibility() + } + + private func setupPanelConstraints() { + guard profileCollectionView != nil else { return } + + NSLayoutConstraint.deactivate(panelConstraints) + panelConstraints.removeAll() + + let sideInset: CGFloat = PublicUtils.isIPhone ? 10 : 18 + let topInset: CGFloat = PublicUtils.isIPhone ? 10 : 18 + let toolbarVerticalInset: CGFloat = PublicUtils.isIPhone ? 6 : 11 + let toolbarHeight: CGFloat = PublicUtils.isIPhone ? 38 : 54 + + panelConstraints = [ + contentView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: contentWidthMultiplier()), + contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: PublicUtils.isIPhone ? 0.93 : 0.93), + contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + contentView.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), + + toolbarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: sideInset), + toolbarView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -sideInset), + toolbarView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -toolbarVerticalInset), + toolbarView.heightAnchor.constraint(equalToConstant: toolbarHeight), + + toolbarStackView.leadingAnchor.constraint(equalTo: toolbarView.leadingAnchor, constant: PublicUtils.isIPhone ? 5 : 8), + toolbarStackView.trailingAnchor.constraint(equalTo: toolbarView.trailingAnchor, constant: PublicUtils.isIPhone ? -5 : -8), + toolbarStackView.topAnchor.constraint(equalTo: toolbarView.topAnchor, constant: PublicUtils.isIPhone ? 3 : 8), + toolbarStackView.bottomAnchor.constraint(equalTo: toolbarView.bottomAnchor, constant: PublicUtils.isIPhone ? -3 : -8), + + profileCollectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: sideInset), + profileCollectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -sideInset), + profileCollectionView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: topInset), + profileCollectionView.bottomAnchor.constraint( + equalTo: toolbarView.isHidden ? contentView.bottomAnchor : toolbarView.topAnchor, + constant: toolbarView.isHidden ? -toolbarVerticalInset : -toolbarVerticalInset + ), + ] + + if PublicUtils.isIPhone { + panelConstraints.append(contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 3)) + } + else { + panelConstraints.append(contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor)) + } + + + NSLayoutConstraint.activate(panelConstraints) + } + + private func updatePanelTheme() { + let darkMode = currentUserInterfaceStyle() == .dark + contentView.backgroundColor = darkMode + ? UIColor(white: 0.09, alpha: loadingMode == .selectProfileFromMainFrame ? 0.92 : 0.88) + : UIColor(white: 0.94, alpha: loadingMode == .selectProfileFromMainFrame ? 0.73 : 0.7) + toolbarView.backgroundColor = darkMode + ? UIColor(white: 1.0, alpha: 0.08) + : UIColor(white: 1.0, alpha: 0.5) + profileCollectionView?.backgroundColor = collectionSurfaceColor() + + contentView.layer.borderColor = UIColor(white: 0.0, alpha: 0.3).cgColor + contentView.layer.borderWidth = darkMode ? 0 : 0.5 + + let tint = darkMode ? UIColor(white: 1, alpha: 0.86) : UIColor(white: 0.10, alpha: 0.86) + [duplicateButton, deleteButton, importButton, exportButton, restoreButton].forEach { button in + button.tintColor = tint + button.setTitleColor(tint, for: .normal) + button.backgroundColor = .clear + } + } + + private func updateToolbarVisibility() { + let hidden = loadingMode == .pickProfileData + toolbarView.isHidden = hidden + setupPanelConstraints() + } + + private func collectionSurfaceColor() -> UIColor { + if currentUserInterfaceStyle() == .dark { + // return UIColor(white: 1.0, alpha: 0) + return .clear + } + return UIColor(white: 1.0, alpha: 0.18) + } + + private func currentUserInterfaceStyle() -> UIUserInterfaceStyle { + if #available(iOS 13.0, *) { + return ThemeManager.userInterfaceStyle() + } + return .light + } + + private func observeThemeChanges() { + removeThemeObserver() + themeObserver = NotificationCenter.default.addObserver( + forName: Notification.Name(ThemeManager.ThemeDidChangeNotification), + object: nil, + queue: .main + ) { [weak self] _ in + self?.handleThemeDidChange() + } + } + + private func removeThemeObserver() { + if let themeObserver { + NotificationCenter.default.removeObserver(themeObserver) + self.themeObserver = nil + } + } + + private func handleThemeDidChange() { + updatePanelTheme() + profileCollectionView?.reloadData() + } + + private func gridSpacing() -> CGFloat { + PublicUtils.isIPhone ? 8 : 12 + } + + private func gridInsets() -> UIEdgeInsets { + let inset = PublicUtils.isIPhone ? 2.0 : 4.0 + return UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) + } + + private func columnCount(for width: CGFloat) -> Int { + if PublicUtils.isIPhone { + return isLayoutLandscape() ? 3 : 2 + } + return width >= 760 ? 4 : 3 + } + + private func configureToolbarButton(_ button: UIButton, title: String, symbolName: String) { + button.layer.cornerRadius = PublicUtils.isIPhone ? 11 : 13 + button.layer.masksToBounds = true + button.titleLabel?.font = UIFont.systemFont(ofSize: PublicUtils.isIPhone ? 10 : 14, weight: .semibold) + button.titleLabel?.adjustsFontSizeToFitWidth = true + button.titleLabel?.minimumScaleFactor = 0.7 + button.setTitle(title, for: .normal) + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: PublicUtils.isIPhone ? 4 : 8, bottom: 0, right: PublicUtils.isIPhone ? 4 : 8) + + if #available(iOS 13.0, tvOS 13.0, *) { + let config = UIImage.SymbolConfiguration(pointSize: PublicUtils.isIPhone ? 9 : 11, weight: .semibold) + let image = UIImage(systemName: symbolName, withConfiguration: config) + button.setImage(image, for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: PublicUtils.isIPhone ? 3 : 6) + } + } + + private func reloadProfilesAsync(scrollToSelected: Bool = false, notify: Bool = false) { + profilesLoadGeneration += 1 + let generation = profilesLoadGeneration + let manager = profilesManager + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let manager else { return } + + let loadedProfiles = manager.getAllProfiles() + let loadedSelectedIndex = Self.selectedProfileIndex(in: loadedProfiles) + + DispatchQueue.main.async { [weak self] in + guard let self, generation == self.profilesLoadGeneration else { return } + + self.profiles = loadedProfiles + self.selectedProfileIndex = loadedSelectedIndex + self.profileCollectionView?.reloadData() + + if scrollToSelected { + self.scrollToSelectedProfile(animated: true) + } + + if notify { + self.notifyProfileViewDidRefresh() + } + } + } + } + + private func scrollToSelectedProfile(animated: Bool) { + guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex) else { + return + } + + profileCollectionView?.scrollToItem(at: IndexPath(item: selectedProfileIndex, section: 0), at: .centeredVertically, animated: animated) + } + + private func moveSelectedProfile(by offset: Int) { + moveSelectedProfile(horizontalOffset: offset, verticalOffset: 0) + } + + private func moveSelectedProfile(horizontalOffset: Int = 0, verticalOffset: Int = 0) { + PublicUtils.runOnMain { [weak self] in + guard let self, + self.profiles.count > 0, + self.profileCollectionView.numberOfSections > 0, + self.profileCollectionView.numberOfItems(inSection: 0) > 0 else { + return + } + + let maxIndex = min(self.profiles.count, self.profileCollectionView.numberOfItems(inSection: 0)) - 1 + let currentIndex = min(max(self.selectedProfileIndex, 0), maxIndex) + let itemCount = maxIndex + 1 + let columnOffset: Int + if verticalOffset == 0 { + columnOffset = 0 + } else { + let columns = max(1, self.columnCount(for: self.profileCollectionView.bounds.width)) + columnOffset = verticalOffset * columns + } + let rawNextIndex = currentIndex + horizontalOffset + columnOffset + let nextIndex = (rawNextIndex % itemCount + itemCount) % itemCount + guard nextIndex != currentIndex else { + self.restoreControllerNavigationHighlight() + return + } + + let nextIndexPath = IndexPath(item: nextIndex, section: 0) + + self.profileCollectionView.selectItem(at: nextIndexPath, animated: true, scrollPosition: .centeredVertically) + self.collectionView(self.profileCollectionView, didSelectItemAt: nextIndexPath) + self.restoreControllerNavigationHighlight() + } + } + + private func validProfileIndex(_ index: Int) -> Int? { + guard profiles.count > 0 else { return nil } + return min(max(index, 0), profiles.count - 1) + } + + private func notifyProfileViewDidRefresh() { + needToUpdateOscLayoutTVC?() + if loadingMode != .selectProfileFromStreamView + && loadingMode != .pickProfile + && loadingMode != .pickProfileData + && loadingMode != .selectProfile + { + guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex) else { return } + NotificationCenter.default.post(name: Notification.Name("GameProfileSelectedNotification"), object: self.profiles[selectedProfileIndex]) + } + } + + private func persistProfileOrderIfNeeded(notify: Bool) { + guard profileOrderChanged else { return } + profilesManager.persistProfileOrder(profiles) + profileOrderChanged = false + if notify { + notifyProfileViewDidRefresh() + } + } + + private func markProfileSelectedLocally(at index: Int) { + for i in 0.. Int { + for index in 0.. NSMutableArray { + switch currentExportScope { + case .allProfiles: + return profilesManager.getEncodedProfiles() + case .selectedProfile: + let encodedProfiles = NSMutableArray() + guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex), + let selectedProfile = profiles.object(at: selectedProfileIndex) as? OSCProfile, + let encodedProfile = try? NSKeyedArchiver.archivedData(withRootObject: selectedProfile, requiringSecureCoding: true) else { + return encodedProfiles + } + encodedProfiles.add(encodedProfile) + return encodedProfiles + } + } + + private func exportFileName(for scope: ProfileExportScope) -> String { + switch scope { + case .allProfiles: + return "profiles.bin" + case .selectedProfile: + guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex), + let selectedProfile = profiles.object(at: selectedProfileIndex) as? OSCProfile else { + return "profile.bin" + } + return "\(sanitizedExportFileName(selectedProfile.name)).bin" + } + } + + private func sanitizedExportFileName(_ fileName: String) -> String { + let invalidCharacters = CharacterSet(charactersIn: "/\\?%*|\"<>:") + .union(.newlines) + .union(.controlCharacters) + let sanitized = fileName + .components(separatedBy: invalidCharacters) + .joined(separator: "_") + .trimmingCharacters(in: .whitespacesAndNewlines) + return sanitized.isEmpty ? "profile" : sanitized + } + + 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) + let profilePayloadData = try ProfileFileContainer.unpackedData(from: fileData) + // NSLog("profile file read: \(UInt32(fileData.count))") + let classes: [AnyClass] = [NSMutableData.self, NSMutableArray.self] + let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: classes, from: profilePayloadData) as? NSMutableArray + + let restoredAlert = UIAlertController( + title: LocalizationHelper.localizedString(forKey: ""), + message: LocalizationHelper.localizedString(forKey: "Pofiles imported"), + preferredStyle: .alert + ) + let failedAlert = UIAlertController( + title: LocalizationHelper.localizedString(forKey: ""), + message: LocalizationHelper.localizedString(forKey: "Failed to import profiles"), + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: LocalizationHelper.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: LocalizationHelper.localizedString(forKey: ""), + message: LocalizationHelper.localizedString(forKey: "Failed to import profiles"), + preferredStyle: .alert + ) + failedAlert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default)) + present(failedAlert, animated: true) + profileViewRefresh() + // NSLog("解码失败: \(error)") + } + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + profiles.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCollectionViewCell.reuseIdentifier, for: indexPath) + cell.isHidden = false + guard let profileCell = cell as? ProfileCollectionViewCell, + indexPath.item < profiles.count, + let profile = profiles.object(at: indexPath.item) as? OSCProfile else { + return cell + } + + profileCell.configure( + title: profile.name.localizedProfileName, + selected: indexPath.item == selectedProfileIndex, + pickingData: loadingMode == .pickProfileData, + userInterfaceStyle: currentUserInterfaceStyle() + ) + cell.isHidden = indexPath == controllerNavigationReorderHiddenIndexPath + return profileCell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + activateProfile(at: indexPath.item) + } + + func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { + loadingMode != .pickProfileData && indexPath.item > 0 && indexPath.item < profiles.count + } + + func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { + if proposedIndexPath.item == 0 { + return IndexPath(item: 1, section: proposedIndexPath.section) + } + return proposedIndexPath + } + + func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + guard sourceIndexPath.item != destinationIndexPath.item, + sourceIndexPath.item > 0, + sourceIndexPath.item < profiles.count, + destinationIndexPath.item > 0, + destinationIndexPath.item < profiles.count else { + return + } + + let movedProfile = profiles.object(at: sourceIndexPath.item) + profiles.removeObject(at: sourceIndexPath.item) + profiles.insert(movedProfile, at: destinationIndexPath.item) + selectedProfileIndex = Self.selectedProfileIndex(in: profiles) + profileOrderChanged = true + } + + @objc private func handleCollectionLongPress(_ gesture: UILongPressGestureRecognizer) { + let location = gesture.location(in: profileCollectionView) + + switch gesture.state { + case .began: + guard loadingMode != .pickProfileData else { + return + } + guard let indexPath = profileCollectionView.indexPathForItem(at: location), + indexPath.item > 0 else { + return + } + profileCollectionView.beginInteractiveMovementForItem(at: indexPath) + + case .changed: + profileCollectionView.updateInteractiveMovementTargetPosition(location) + + case .ended: + profileCollectionView.endInteractiveMovement() + + default: + profileCollectionView.cancelInteractiveMovement() + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + gridInsets() + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + gridSpacing() + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + gridSpacing() + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let insets = gridInsets() + let columns = columnCount(for: collectionView.bounds.width) + let totalSpacing = CGFloat(columns - 1) * gridSpacing() + insets.left + insets.right + let width = floor((collectionView.bounds.width - totalSpacing) / CGFloat(columns)) + let height: CGFloat + + if PublicUtils.isIPhone { + height = max(58, min(102, width * 0.58)) + } else { + height = max(94, min(126, width * 0.62)) + } + + return CGSize(width: width, height: height) + } + + private func activateProfile(at index: Int) { + guard index >= 0, + index < profiles.count else { + return + } + + if loadingMode == .pickProfileData, + let pickedProfile = profiles.object(at: index) as? OSCProfile { + pickedProfileDataHandler?(pickedProfile) + dismiss(animated: false) + return + } + + let lastSelectedIndex = validProfileIndex(selectedProfileIndex) + + if index != lastSelectedIndex { + markProfileSelectedLocally(at: index) + if profileOrderChanged { + profilesManager.persistProfileOrder(profiles) + profileOrderChanged = false + } else { + profilesManager.setProfileToSelected(UInt32(index)) + } + + let itemsToReload = [lastSelectedIndex, selectedProfileIndex] + .compactMap { $0 } + .filter { $0 >= 0 && $0 < profileCollectionView.numberOfItems(inSection: 0) } + .map { IndexPath(item: $0, section: 0) } + if !itemsToReload.isEmpty { + UIView.performWithoutAnimation { + profileCollectionView.reloadItems(at: itemsToReload) + } + } + } + + if loadingMode == .pickProfile { + notifyProfileViewDidRefresh() + dismiss(animated: false) + } else { + profileViewRefresh() + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if (loadingMode != .selectProfile + && loadingMode != .selectProfileFromMainFrame + && loadingMode != .selectProfileFromStreamView + ), + let touchView = touch.view, + touchView.isDescendant(of: profileCollectionView) { + return true + } + return touch.view == view + } + + @available(iOS 13.0, *) + func getNavigationElements() -> [ControllerNavigationElement] { + var elements: [ControllerNavigationElement] = [] + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .rightStick : .leftStick, action: "focusNavigation")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "holdToReorder")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "doublePressToDelete")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "exit")) + return elements + } + + func navigateByController(forward: Bool) { + if controllerNavigationReorderActive { + if #available(iOS 13.0, *) { + moveControllerNavigationReorderedProfile(horizontalOffset: forward ? 1 : -1) + } + return + } + moveSelectedProfile(horizontalOffset: forward ? 1 : -1) + } + + func navigateByController(downward: Bool) { + if controllerNavigationReorderActive { + if #available(iOS 13.0, *) { + moveControllerNavigationReorderedProfile(verticalOffset: downward ? 1 : -1) + } + return + } + moveSelectedProfile(verticalOffset: downward ? 1 : -1) + } + + func persistControllerNavigationHighlight() {} + + func restoreControllerNavigationHighlight() { + guard #available(iOS 13.0, *) else { return } + restoreSelectedProfileControllerNavigationHighlight() + } + + func restoreControllerNavigationHighlightAfterSettingsModeSwitch() { + restoreControllerNavigationHighlight() + } + + func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) { + guard navigation.action == "focusNavigation" else { return } + moveSelectedProfile(horizontalOffset: forward ? 1 : -1) + } + + func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) { + if navigation.action == "holdToReorder" { + if #available(iOS 13.0, *) { + handleControllerNavigationReorderHold(pressed: pressed) + } + return + } + + guard pressed else { return } + PublicUtils.runOnMain { [weak self] in + switch navigation.action { + case "previousItem": + self?.moveSelectedProfile(by: -1) + case "nextItem": + self?.moveSelectedProfile(by: 1) + case "exit": + self?.dismiss(animated: false) + default: + break + } + } + } +} + +@available(iOS 13.0, *) +extension ProfileSelectorViewController: ControllerCollectionNavigationDelegate { + var controllerNavigationCollectionView: UICollectionView { + profileCollectionView + } + + private func restoreSelectedProfileControllerNavigationHighlight() { + PublicUtils.runOnMain { [weak self] in + guard let self, + self.profileCollectionView.numberOfSections > 0, + self.profileCollectionView.numberOfItems(inSection: 0) > 0, + let selectedProfileIndex = self.validProfileIndex(self.selectedProfileIndex) else { + self?.clearCollectionControllerNavigationHighlightForControllerNavigator() + return + } + + self.profileCollectionView.layoutIfNeeded() + self.controllerNavigationHighlightItemForControllerNavigator(at: IndexPath(item: selectedProfileIndex, section: 0)) + } + } + + private func handleControllerNavigationReorderHold(pressed: Bool) { + PublicUtils.runOnMain { [weak self] in + guard let self else { return } + + if pressed { + if self.controllerNavigationDeleteDoublePressWindowOpen { + self.closeControllerNavigationDeleteDoublePressWindow() + self.deleteControllerNavigationHighlightedProfile() + return + } + + guard !self.controllerNavigationReorderActive, + let currentIndexPath = self.controllerNavigationCurrentIndexPathForControllerNavigator() + ?? self.validProfileIndex(self.selectedProfileIndex).map({ IndexPath(item: $0, section: 0) }), + self.canControllerNavigationReorderProfile(at: currentIndexPath) else { + return + } + + self.controllerNavigationReorderPressBeganAt = CACurrentMediaTime() + self.controllerNavigationReorderActive = true + self.pendingControllerNavigationReorderMove = nil + self.controllerNavigationPersistOrderWhenReorderSettles = false + self.beginControllerNavigationReorderSnapshot(at: currentIndexPath) + self.controllerNavigationSelectedIndexPath = currentIndexPath + return + } + + let pressDuration = self.controllerNavigationReorderPressBeganAt.map { CACurrentMediaTime() - $0 } ?? .greatestFiniteMagnitude + self.controllerNavigationReorderPressBeganAt = nil + + guard self.controllerNavigationReorderActive else { return } + self.controllerNavigationReorderActive = false + self.pendingControllerNavigationReorderMove = nil + self.controllerNavigationPersistOrderWhenReorderSettles = true + if pressDuration <= 0.2 { + self.openControllerNavigationDeleteDoublePressWindow() + } else { + self.closeControllerNavigationDeleteDoublePressWindow() + } + + guard !self.controllerNavigationReorderUpdateInFlight else { return } + self.persistProfileOrderIfNeeded(notify: false) + self.controllerNavigationPersistOrderWhenReorderSettles = false + self.endControllerNavigationReorderSnapshot() + self.restoreControllerNavigationHighlight() + } + } + + private func moveControllerNavigationReorderedProfile(horizontalOffset: Int) { + moveControllerNavigationReorderedProfile(horizontalOffset: horizontalOffset, verticalOffset: 0) + } + + private func moveControllerNavigationReorderedProfile(verticalOffset: Int) { + moveControllerNavigationReorderedProfile(horizontalOffset: 0, verticalOffset: verticalOffset) + } + + private func moveControllerNavigationReorderedProfile(horizontalOffset: Int, verticalOffset: Int) { + guard !controllerNavigationReorderUpdateInFlight else { + pendingControllerNavigationReorderMove = (horizontalOffset, verticalOffset) + return + } + + guard let currentIndexPath = controllerNavigationCurrentIndexPathForControllerNavigator(), + canControllerNavigationReorderProfile(at: currentIndexPath), + let destinationIndexPath = controllerNavigationReorderDestination(from: currentIndexPath, horizontalOffset: horizontalOffset, verticalOffset: verticalOffset) else { + return + } + + moveControllerNavigationReorderedProfile(from: currentIndexPath, to: destinationIndexPath) + } + + private func openControllerNavigationDeleteDoublePressWindow() { + controllerNavigationDeleteDoublePressWindowOpen = true + controllerNavigationDeleteDoublePressToken += 1 + let token = controllerNavigationDeleteDoublePressToken + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self, + self.controllerNavigationDeleteDoublePressToken == token else { + return + } + + self.controllerNavigationDeleteDoublePressWindowOpen = false + } + } + + private func closeControllerNavigationDeleteDoublePressWindow() { + controllerNavigationDeleteDoublePressWindowOpen = false + controllerNavigationDeleteDoublePressToken += 1 + } + + private func deleteControllerNavigationHighlightedProfile() { + guard let indexPath = controllerNavigationCurrentIndexPathForControllerNavigator() + ?? validProfileIndex(selectedProfileIndex).map({ IndexPath(item: $0, section: 0) }), + indexPath.section == 0, + indexPath.item > 0, + indexPath.item < profiles.count, + indexPath.item < profileCollectionView.numberOfItems(inSection: 0) else { + return + } + + closeControllerNavigationDeleteDoublePressWindow() + controllerNavigationReorderActive = false + pendingControllerNavigationReorderMove = nil + controllerNavigationPersistOrderWhenReorderSettles = false + endControllerNavigationReorderSnapshot() + invalidateCollectionControllerNavigationHighlightForControllerNavigator() + + markProfileSelectedLocally(at: indexPath.item) + if profileOrderChanged { + profilesManager.persistProfileOrder(profiles) + profileOrderChanged = false + } else { + profilesManager.setProfileToSelected(UInt32(indexPath.item)) + } + + profilesManager.deleteCurrentSelectedProfile() + + let targetItem = max(indexPath.item - 1, 0) + profiles.removeObject(at: indexPath.item) + if profiles.count > 0 { + markProfileSelectedLocally(at: min(targetItem, profiles.count - 1)) + } + + profileCollectionView.performBatchUpdates { + profileCollectionView.deleteItems(at: [indexPath]) + } completion: { [weak self] _ in + guard let self else { return } + + self.profileCollectionView.reloadData() + self.notifyProfileViewDidRefresh() + self.scrollToSelectedProfile(animated: true) + self.restoreControllerNavigationHighlight() + } + } + + private func controllerNavigationReorderDestination(from currentIndexPath: IndexPath, horizontalOffset: Int, verticalOffset: Int) -> IndexPath? { + let itemCount = min(profiles.count, profileCollectionView.numberOfItems(inSection: 0)) + let movableCount = itemCount - 1 + guard movableCount > 1 else { return nil } + + let columns = max(1, columnCount(for: profileCollectionView.bounds.width)) + let rawOffset = horizontalOffset + verticalOffset * columns + guard rawOffset != 0 else { return nil } + + let currentSortableIndex = currentIndexPath.item - 1 + let nextSortableIndex = (currentSortableIndex + rawOffset) % movableCount + let wrappedSortableIndex = (nextSortableIndex + movableCount) % movableCount + let nextItem = wrappedSortableIndex + 1 + guard nextItem != currentIndexPath.item else { return nil } + + return IndexPath(item: nextItem, section: currentIndexPath.section) + } + + private func moveControllerNavigationReorderedProfile(from sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + guard canControllerNavigationReorderProfile(at: sourceIndexPath), + canControllerNavigationReorderProfile(at: destinationIndexPath), + sourceIndexPath != destinationIndexPath else { + return + } + + controllerNavigationReorderUpdateInFlight = true + pendingControllerNavigationReorderMove = nil + invalidateCollectionControllerNavigationHighlightForControllerNavigator() + controllerNavigationReorderHiddenIndexPath = sourceIndexPath + updateVisibleReorderHiddenCells() + + let movedProfile = profiles.object(at: sourceIndexPath.item) + profiles.removeObject(at: sourceIndexPath.item) + profiles.insert(movedProfile, at: destinationIndexPath.item) + selectedProfileIndex = destinationIndexPath.item + profileOrderChanged = true + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + + self.scrollToControllerNavigationReorderProfileIfNeeded(at: destinationIndexPath, animated: true) + self.updateControllerNavigationReorderSnapshotFrame(to: destinationIndexPath, animated: true) + CATransaction.begin() + CATransaction.setAnimationDuration(0.07) + self.profileCollectionView.performBatchUpdates { + self.profileCollectionView.moveItem(at: sourceIndexPath, to: destinationIndexPath) + } completion: { [weak self] _ in + guard let self else { return } + + self.scrollToControllerNavigationReorderProfileIfNeeded(at: destinationIndexPath, animated: false) + self.updateControllerNavigationReorderSnapshotFrame(to: destinationIndexPath, animated: false) + self.controllerNavigationReorderHiddenIndexPath = destinationIndexPath + self.updateVisibleReorderHiddenCells() + self.controllerNavigationSelectedIndexPath = destinationIndexPath + self.controllerNavigationReorderUpdateInFlight = false + + if self.consumePendingControllerNavigationReorderMoveIfNeeded() { + return + } + + if self.controllerNavigationPersistOrderWhenReorderSettles { + self.persistProfileOrderIfNeeded(notify: false) + self.controllerNavigationPersistOrderWhenReorderSettles = false + } + + guard !self.controllerNavigationReorderActive else { return } + self.endControllerNavigationReorderSnapshot() + self.restoreControllerNavigationHighlight() + } + CATransaction.commit() + } + } + + private func beginControllerNavigationReorderSnapshot(at indexPath: IndexPath) { + controllerNavigationReorderSnapshotView?.removeFromSuperview() + invalidateCollectionControllerNavigationHighlightForControllerNavigator() + + profileCollectionView.layoutIfNeeded() + guard let cell = profileCollectionView.cellForItem(at: indexPath), + let snapshot = cell.snapshotView(afterScreenUpdates: true) else { + return + } + + snapshot.frame = cell.frame + snapshot.layer.cornerRadius = cell.contentView.layer.cornerRadius + snapshot.layer.masksToBounds = true + snapshot.layer.zPosition = 1000 + + profileCollectionView.addSubview(snapshot) + controllerNavigationReorderSnapshotView = snapshot + controllerNavigationReorderHiddenIndexPath = indexPath + updateVisibleReorderHiddenCells() + } + + private func scrollToControllerNavigationReorderProfileIfNeeded(at indexPath: IndexPath, animated: Bool) { + guard indexPath.section < profileCollectionView.numberOfSections, + indexPath.item >= 0, + indexPath.item < profileCollectionView.numberOfItems(inSection: indexPath.section) else { + return + } + + profileCollectionView.scrollToItem(at: indexPath, at: [.centeredHorizontally, .centeredVertically], animated: animated) + profileCollectionView.layoutIfNeeded() + } + + private func updateControllerNavigationReorderSnapshotFrame(to indexPath: IndexPath, animated: Bool) { + guard let snapshot = controllerNavigationReorderSnapshotView else { return } + + profileCollectionView.layoutIfNeeded() + guard let attributes = profileCollectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) else { return } + + let updates = { + snapshot.center = attributes.center + snapshot.bounds.size = attributes.bounds.size + } + + if animated { + UIView.animate(withDuration: 0.07, delay: 0, options: [.beginFromCurrentState, .curveEaseInOut], animations: updates) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in + self?.updateControllerNavigationReorderSnapshotFrame(to: indexPath, animated: false) + } + } else { + updates() + } + } + + private func endControllerNavigationReorderSnapshot() { + guard let snapshot = controllerNavigationReorderSnapshotView else { return } + controllerNavigationReorderSnapshotView = nil + controllerNavigationReorderHiddenIndexPath = nil + updateVisibleReorderHiddenCells() + + UIView.animate(withDuration: 0.12, animations: { + snapshot.alpha = 0 + snapshot.transform = .identity + }) { _ in + snapshot.removeFromSuperview() + } + } + + private func updateVisibleReorderHiddenCells() { + profileCollectionView.visibleCells.forEach { cell in + guard let indexPath = profileCollectionView.indexPath(for: cell) else { + cell.isHidden = false + return + } + + cell.isHidden = indexPath == controllerNavigationReorderHiddenIndexPath + } + } + + private func canControllerNavigationReorderProfile(at indexPath: IndexPath) -> Bool { + loadingMode != .pickProfileData + && indexPath.section == 0 + && indexPath.item > 0 + && indexPath.item < profiles.count + && indexPath.item < profileCollectionView.numberOfItems(inSection: 0) + } + + @discardableResult + private func consumePendingControllerNavigationReorderMoveIfNeeded() -> Bool { + guard controllerNavigationReorderActive, + !controllerNavigationReorderUpdateInFlight, + let pendingMove = pendingControllerNavigationReorderMove else { + pendingControllerNavigationReorderMove = nil + return false + } + + pendingControllerNavigationReorderMove = nil + + if pendingMove.horizontal != 0 { + moveControllerNavigationReorderedProfile(horizontalOffset: pendingMove.horizontal) + return true + } + + if pendingMove.vertical != 0 { + moveControllerNavigationReorderedProfile(verticalOffset: pendingMove.vertical) + return true + } + + return false + } + + private func cancelControllerNavigationReorderIfNeeded() { + controllerNavigationReorderActive = false + controllerNavigationReorderUpdateInFlight = false + pendingControllerNavigationReorderMove = nil + controllerNavigationPersistOrderWhenReorderSettles = false + controllerNavigationReorderPressBeganAt = nil + closeControllerNavigationDeleteDoublePressWindow() + controllerNavigationReorderSnapshotView?.removeFromSuperview() + controllerNavigationReorderSnapshotView = nil + controllerNavigationReorderHiddenIndexPath = nil + updateVisibleReorderHiddenCells() + } +} diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileTableViewCell.xib b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileTableViewCell.xib deleted file mode 100644 index 8f291de9..00000000 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileTableViewCell.xib +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index ce198231..57797468 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -1288,13 +1288,13 @@ static NSMutableSet* hostList; layoutToolVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; - layoutToolVC.profileTableLoadingMode = OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame; + layoutToolVC.profileSelectorLoadingMode = ProfileSelectorLoadingModeSelectProfileFromMainFrame; layoutToolVC.toolbarStackView.hidden = true; layoutToolVC.toolbarRootView.hidden = true; self.gameProfileSelectorVC = layoutToolVC; [self presentViewController:layoutToolVC animated:false completion:^{ - [layoutToolVC presentProfilesTableViewWithLoadingMode:OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame animated:animated]; + [layoutToolVC presentProfileSelectorWith:ProfileSelectorLoadingModeSelectProfileFromMainFrame animated:animated]; }]; } @@ -1663,7 +1663,7 @@ static NSMutableSet* hostList; barButtonItem.tintColor = ThemeManager.appPrimaryColor; if (@available(iOS 13.0, *)) { - UIUserInterfaceStyle style = ThemeManager.userInterfaceStyle; + UIUserInterfaceStyle style = ThemeManager.overrideUserInterfaceStyle; UIView *customView = barButtonItem.customView; customView.overrideUserInterfaceStyle = style; @@ -1685,7 +1685,7 @@ static NSMutableSet* hostList; - (void)applyThemeToNavigationControls { if (@available(iOS 13.0, *)) { - UIUserInterfaceStyle style = ThemeManager.userInterfaceStyle; + UIUserInterfaceStyle style = ThemeManager.overrideUserInterfaceStyle; self.overrideUserInterfaceStyle = style; self.view.overrideUserInterfaceStyle = style; self.navigationController.overrideUserInterfaceStyle = style; @@ -1856,7 +1856,7 @@ static NSMutableSet* hostList; TemporarySettings* tempSettings = [dataMan getSettings]; if(tempSettings.appTheme.intValue != UIUserInterfaceStyleUnspecified) return; if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) { - [ThemeManager setUserInterfaceStyle:self.traitCollection.userInterfaceStyle]; + [ThemeManager systemUserInterfaceStyleDidChange:self.traitCollection.userInterfaceStyle]; } } } @@ -2806,8 +2806,8 @@ static NSMutableSet* hostList; }]; } else if(!self.gameProfileSelectorVC && !self.settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletorWithAnimated:true]; - else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}]; - else if (self.settingsViewController.layoutOnScreenControlsVC) [self.settingsViewController.layoutOnScreenControlsVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}]; + else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.profileSelectorViewController dismissViewControllerAnimated:true completion:^{}]; + else if (self.settingsViewController.layoutOnScreenControlsVC) [self.settingsViewController.layoutOnScreenControlsVC.profileSelectorViewController dismissViewControllerAnimated:true completion:^{}]; break; case RadialMenuItemHostView: [self switchToHostView]; @@ -2833,8 +2833,8 @@ static NSMutableSet* hostList; - (void)controllerNavigatorDidSelectGameProfiles { dispatch_async(dispatch_get_main_queue(), ^{ if(!self.gameProfileSelectorVC && !self.settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletorWithAnimated:true]; - else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}]; - else if (self.settingsViewController.layoutOnScreenControlsVC) [self.settingsViewController.layoutOnScreenControlsVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}]; + else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.profileSelectorViewController dismissViewControllerAnimated:true completion:^{}]; + else if (self.settingsViewController.layoutOnScreenControlsVC) [self.settingsViewController.layoutOnScreenControlsVC.profileSelectorViewController dismissViewControllerAnimated:true completion:^{}]; }); } diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index b05554ef..5fe31049 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -3226,9 +3226,9 @@ BOOL isCustomResolution(int resolutionSelected) { // self.layoutOnScreenControlsVC.view.backgroundColor = [UIColor colorWithWhite:0.55 alpha:1.0]; self.layoutOnScreenControlsVC.view.backgroundColor = UIColor.clearColor; self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; - self.layoutOnScreenControlsVC.profileTableLoadingMode = OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame; + self.layoutOnScreenControlsVC.profileSelectorLoadingMode = ProfileSelectorLoadingModeSelectProfileFromMainFrame; [self presentViewController:self.layoutOnScreenControlsVC animated:NO completion:^{ - [self->_layoutOnScreenControlsVC presentProfilesTableViewWithLoadingMode:OSCProfilesTableViewLoadingModeSelectProfileFromMainFrame animated:animated]; + [self->_layoutOnScreenControlsVC presentProfileSelectorWith:ProfileSelectorLoadingModeSelectProfileFromMainFrame animated:animated]; }]; _layoutOnScreenControlsVC.toolbarStackView.hidden = true; _layoutOnScreenControlsVC.toolbarRootView.hidden = true; diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index 87c1deb7..389eb41c 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -1064,9 +1064,9 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil _layoutOnScreenControlsVC.quickSwitchEnabled = true; _layoutOnScreenControlsVC.toolbarStackView.hidden = true; _layoutOnScreenControlsVC.toolbarRootView.hidden = true; - OSCProfilesTableViewLoadingMode loadingMode = pickProfile ? OSCProfilesTableViewLoadingModePickProfile : OSCProfilesTableViewLoadingModeSelectProfileFromStreamView; + ProfileSelectorLoadingMode loadingMode = pickProfile ? ProfileSelectorLoadingModePickProfile : ProfileSelectorLoadingModeSelectProfileFromStreamView; [self presentViewController:_layoutOnScreenControlsVC animated:NO completion:^{ - [self->_layoutOnScreenControlsVC presentProfilesTableViewWithLoadingMode:loadingMode animated:false]; + [self->_layoutOnScreenControlsVC presentProfileSelectorWith:loadingMode animated:false]; }]; } diff --git a/VoidLink/ViewControllers/ToolboxViewController.swift b/VoidLink/ViewControllers/ToolboxViewController.swift index 9baa67a8..d7a97722 100644 --- a/VoidLink/ViewControllers/ToolboxViewController.swift +++ b/VoidLink/ViewControllers/ToolboxViewController.swift @@ -295,11 +295,8 @@ import UIKit contentView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: contentWidthMultiplier()), contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: PublicUtils.isIPhone ? 0.93 : 0.93), contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - // contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - contentView.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), - contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: PublicUtils.isIPhone ? 3 : -5), toolbarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: sideInset), toolbarView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -sideInset), @@ -316,6 +313,14 @@ import UIKit collectionView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: topInset), collectionView.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: -toolbarVerticalInset), ] + + if PublicUtils.isIPhone { + layoutConstraints.append(contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 3)) + } + else { + layoutConstraints.append(contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor)) + } + NSLayoutConstraint.activate(layoutConstraints) setupToolbarConstraints() diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 0e8b4b16..cbe32d49 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -1647,7 +1647,7 @@ }, "Bhn-IO-Wlt.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"ExportAll\"; ObjectID = \"Bhn-IO-Wlt\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2409,7 +2409,7 @@ }, "CpY-Sb-BDc.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Import\"; ObjectID = \"CpY-Sb-BDc\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2517,7 +2517,7 @@ }, "czn-mj-8d1.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Restore\"; ObjectID = \"czn-mj-8d1\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2848,7 +2848,7 @@ }, "egC-9E-zS4.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Delete\"; ObjectID = \"egC-9E-zS4\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -5374,7 +5374,7 @@ }, "P34-hO-6cX.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Duplicate\"; ObjectID = \"P34-hO-6cX\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index ab62edf4..a75aa4d2 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -1713,7 +1713,7 @@ }, "bue-hp-CWD.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Duplicate\"; ObjectID = \"bue-hp-CWD\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2079,7 +2079,7 @@ }, "D9X-7p-9oc.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"ExportAll\"; ObjectID = \"D9X-7p-9oc\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2649,7 +2649,7 @@ }, "ghM-zc-sr6.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Delete\"; ObjectID = \"ghM-zc-sr6\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -5601,7 +5601,7 @@ }, "sUC-Ur-7eI.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Restore\"; ObjectID = \"sUC-Ur-7eI\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -6663,7 +6663,7 @@ }, "Z9M-yq-an2.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Import\"; ObjectID = \"Z9M-yq-an2\";", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : {