mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:03 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXBookmarkManager.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXBookmarkManager : NSObject | ||||
|  | ||||
| @property (nonatomic, readonly, class) NSMutableArray *bookmarks; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  FLEXBookmarkManager.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXBookmarkManager.h" | ||||
|  | ||||
| static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil; | ||||
|  | ||||
| @implementation FLEXBookmarkManager | ||||
|  | ||||
| + (void)initialize { | ||||
|     if (self == [FLEXBookmarkManager class]) { | ||||
|         kFLEXBookmarkManagerBookmarks = [NSMutableArray new]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (NSMutableArray *)bookmarks { | ||||
|     return kFLEXBookmarkManagerBookmarks; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXBookmarksViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXBookmarksViewController : FLEXTableViewController | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,235 @@ | ||||
| // | ||||
| //  FLEXBookmarksViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXBookmarksViewController.h" | ||||
| #import "FLEXExplorerViewController.h" | ||||
| #import "FLEXNavigationController.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXBookmarkManager.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXTableView.h" | ||||
|  | ||||
| @interface FLEXBookmarksViewController () | ||||
| @property (nonatomic, copy) NSArray *bookmarks; | ||||
| @property (nonatomic, readonly) FLEXExplorerViewController *corePresenter; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXBookmarksViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.navigationController.hidesBarsOnSwipe = NO; | ||||
|     self.tableView.allowsMultipleSelectionDuringEditing = YES; | ||||
|      | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|     [self setupDefaultBarItems]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)reloadData { | ||||
|     // We assume the bookmarks aren't going to change out from under us, since | ||||
|     // presenting any other tool via keyboard shortcuts should dismiss us first | ||||
|     self.bookmarks = FLEXBookmarkManager.bookmarks; | ||||
|     self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)]; | ||||
| } | ||||
|  | ||||
| - (void)setupDefaultBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated)); | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)), | ||||
|     ]; | ||||
|      | ||||
|     // Disable editing if no bookmarks available | ||||
|     self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0; | ||||
| } | ||||
|  | ||||
| - (void)setupEditingBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = nil; | ||||
|     self.toolbarItems = @[ | ||||
|         [UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         // We use a non-system done item because we change its title dynamically | ||||
|         [UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)] | ||||
|     ]; | ||||
|      | ||||
|     self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor; | ||||
| } | ||||
|  | ||||
| - (FLEXExplorerViewController *)corePresenter { | ||||
|     // We must be presented by a FLEXExplorerViewController, or presented | ||||
|     // by another view controller that was presented by FLEXExplorerViewController | ||||
|     FLEXExplorerViewController *presenter = (id)self.presentingViewController; | ||||
|     presenter = (id)presenter.presentingViewController ?: presenter; | ||||
|     presenter = (id)presenter.presentingViewController ?: presenter; | ||||
|     NSAssert( | ||||
|         [presenter isKindOfClass:[FLEXExplorerViewController class]], | ||||
|         @"The bookmarks view controller expects to be presented by the explorer controller" | ||||
|     ); | ||||
|     return presenter; | ||||
| } | ||||
|  | ||||
| #pragma mark Button Actions | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     [self dismissAnimated:nil]; | ||||
| } | ||||
|  | ||||
| - (void)dismissAnimated:(id)selectedObject { | ||||
|     if (selectedObject) { | ||||
|         UIViewController *explorer = [FLEXObjectExplorerFactory | ||||
|             explorerViewControllerForObject:selectedObject | ||||
|         ]; | ||||
|         if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) { | ||||
|             // I am presented on an existing navigation stack, so | ||||
|             // dismiss myself and push the bookmark there | ||||
|             UINavigationController *presenter = (id)self.presentingViewController; | ||||
|             [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|                 [presenter pushViewController:explorer animated:YES]; | ||||
|             }]; | ||||
|         } else { | ||||
|             // Dismiss myself and present explorer | ||||
|             UIViewController *presenter = self.corePresenter; | ||||
|             [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|                 [presenter presentViewController:[FLEXNavigationController | ||||
|                     withRootViewController:explorer | ||||
|                 ] animated:YES completion:nil]; | ||||
|             }]; | ||||
|         } | ||||
|     } else { | ||||
|         // Just dismiss myself | ||||
|         [self dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)toggleEditing { | ||||
|     NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows; | ||||
|     self.editing = !self.editing; | ||||
|      | ||||
|     if (self.isEditing) { | ||||
|         [self setupEditingBarItems]; | ||||
|     } else { | ||||
|         [self setupDefaultBarItems]; | ||||
|          | ||||
|         // Get index set of bookmarks to close | ||||
|         NSMutableIndexSet *indexes = [NSMutableIndexSet new]; | ||||
|         for (NSIndexPath *ip in selected) { | ||||
|             [indexes addIndex:ip.row]; | ||||
|         } | ||||
|          | ||||
|         if (selected.count) { | ||||
|             // Close bookmarks and update data source | ||||
|             [FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes]; | ||||
|             [self reloadData]; | ||||
|              | ||||
|             // Remove deleted rows | ||||
|             [self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeAllButtonPressed:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         NSInteger count = self.bookmarks.count; | ||||
|         NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark"); | ||||
|         make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             [self closeAll]; | ||||
|             [self toggleEditing]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| - (void)closeAll { | ||||
|     NSInteger rowCount = self.bookmarks.count; | ||||
|      | ||||
|     // Close bookmarks and update data source | ||||
|     [FLEXBookmarkManager.bookmarks removeAllObjects]; | ||||
|     [self reloadData]; | ||||
|      | ||||
|     // Delete rows from table view | ||||
|     NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) { | ||||
|         return [NSIndexPath indexPathForRow:row inSection:0]; | ||||
|     }]; | ||||
|     [self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.bookmarks.count; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath]; | ||||
|      | ||||
|     id object = self.bookmarks[indexPath.row]; | ||||
|     cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object]; | ||||
|     cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object]; | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (self.editing) { | ||||
|         // Case: editing with multi-select | ||||
|         self.toolbarItems.lastObject.title = @"Remove Selected"; | ||||
|         self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor; | ||||
|     } else { | ||||
|         // Case: selected a bookmark | ||||
|         [self dismissAnimated:self.bookmarks[indexPath.row]]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(self.editing); | ||||
|      | ||||
|     if (tableView.indexPathsForSelectedRows.count == 0) { | ||||
|         self.toolbarItems.lastObject.title = @"Done"; | ||||
|         self.toolbarItems.lastObject.tintColor = self.view.tintColor; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)table | ||||
| commitEditingStyle:(UITableViewCellEditingStyle)edit | ||||
| forRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(edit == UITableViewCellEditingStyleDelete); | ||||
|      | ||||
|     // Remove bookmark and update data source | ||||
|     [FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row]; | ||||
|     [self reloadData]; | ||||
|      | ||||
|     // Delete row from table view | ||||
|     [table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										61
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| // | ||||
| //  FLEXExplorerViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 4/4/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXExplorerToolbar.h" | ||||
|  | ||||
| @class FLEXWindow; | ||||
| @protocol FLEXExplorerViewControllerDelegate; | ||||
|  | ||||
| /// A view controller that manages the FLEX toolbar. | ||||
| @interface FLEXExplorerViewController : UIViewController | ||||
|  | ||||
| @property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate; | ||||
| @property (nonatomic, readonly) BOOL wantsWindowToBecomeKey; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar; | ||||
|  | ||||
| - (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates; | ||||
|  | ||||
| /// @brief Used to present (or dismiss) a modal view controller ("tool"), | ||||
| /// typically triggered by pressing a button in the toolbar. | ||||
| /// | ||||
| /// If a tool is already presented, this method simply dismisses it and calls the completion block. | ||||
| /// If no tool is presented, @code future() @endcode is presented and the completion block is called. | ||||
| - (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future | ||||
|                                   completion:(void (^)(void))completion; | ||||
|  | ||||
| /// @brief Used to present (or dismiss) a modal view controller ("tool"), | ||||
| /// typically triggered by pressing a button in the toolbar. | ||||
| /// | ||||
| /// If a tool is already presented, this method dismisses it and presents the given tool. | ||||
| /// The completion block is called once the tool has been presented. | ||||
| - (void)presentTool:(UINavigationController *(^)(void))future | ||||
|          completion:(void (^)(void))completion; | ||||
|  | ||||
| // Keyboard shortcut helpers | ||||
|  | ||||
| - (void)toggleSelectTool; | ||||
| - (void)toggleMoveTool; | ||||
| - (void)toggleViewsTool; | ||||
| - (void)toggleMenuTool; | ||||
|  | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleDownArrowKeyPressed; | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleUpArrowKeyPressed; | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleRightArrowKeyPressed; | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleLeftArrowKeyPressed; | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - | ||||
| @protocol FLEXExplorerViewControllerDelegate <NSObject> | ||||
| - (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController; | ||||
| @end | ||||
							
								
								
									
										1050
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1050
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXViewControllersViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 2/13/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXViewControllersViewController : FLEXFilteringTableViewController | ||||
|  | ||||
| + (instancetype)controllersForViews:(NSArray<UIView *> *)views; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,79 @@ | ||||
| // | ||||
| //  FLEXViewControllersViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 2/13/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXViewControllersViewController.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXViewControllersViewController () | ||||
| @property (nonatomic, readonly) FLEXMutableListSection *section; | ||||
| @property (nonatomic, readonly) NSArray<UIViewController *> *controllers; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXViewControllersViewController | ||||
| @dynamic sections, allSections; | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)controllersForViews:(NSArray<UIView *> *)views { | ||||
|     return [[self alloc] initWithViews:views]; | ||||
| } | ||||
|  | ||||
| - (id)initWithViews:(NSArray<UIView *> *)views { | ||||
|     NSParameterAssert(views.count); | ||||
|      | ||||
|     self = [self initWithStyle:UITableViewStylePlain]; | ||||
|     if (self) { | ||||
|         _controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) { | ||||
|             return [FLEXUtility viewControllerForView:view]; | ||||
|         }]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.title = @"View Controllers at Tap"; | ||||
|     self.showsSearchBar = YES; | ||||
|     [self disableToolbar]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     _section = [FLEXMutableListSection list:self.controllers | ||||
|         cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) { | ||||
|             cell.textLabel.text = [NSString | ||||
|                 stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller | ||||
|             ]; | ||||
|             cell.detailTextLabel.text = controller.view.description; | ||||
|             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; | ||||
|             cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; | ||||
|     } filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) { | ||||
|         return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText]; | ||||
|     }]; | ||||
|      | ||||
|     self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) { | ||||
|         [host.navigationController pushViewController: | ||||
|             [FLEXObjectExplorerFactory explorerViewControllerForObject:controller] | ||||
|         animated:YES]; | ||||
|     }; | ||||
|      | ||||
|     self.section.customTitle = @"View Controllers"; | ||||
|     return @[self.section]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     [self dismissViewControllerAnimated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										29
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  FLEXWindow.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 4/13/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @protocol FLEXWindowEventDelegate <NSObject> | ||||
|  | ||||
| - (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow; | ||||
| - (BOOL)canBecomeKeyWindow; | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - | ||||
| @interface FLEXWindow : UIWindow | ||||
|  | ||||
| @property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate; | ||||
|  | ||||
| /// Tracked so we can restore the key window after dismissing a modal. | ||||
| /// We need to become key after modal presentation so we can correctly capture input. | ||||
| /// If we're just showing the toolbar, we want the main app's window to remain key | ||||
| /// so that we don't interfere with input, status bar, etc. | ||||
| @property (nonatomic, readonly) UIWindow *previousKeyWindow; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										72
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // | ||||
| //  FLEXWindow.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 4/13/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXWindow.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import <objc/runtime.h> | ||||
|  | ||||
| @implementation FLEXWindow | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         // Some apps have windows at UIWindowLevelStatusBar + n. | ||||
|         // If we make the window level too high, we block out UIAlertViews. | ||||
|         // There's a balance between staying above the app's windows and staying below alerts. | ||||
|         // UIWindowLevelStatusBar + 100 seems to hit that balance. | ||||
|         self.windowLevel = UIWindowLevelStatusBar + 100.0; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { | ||||
|     BOOL pointInside = NO; | ||||
|     if ([self.eventDelegate shouldHandleTouchAtPoint:point]) { | ||||
|         pointInside = [super pointInside:point withEvent:event]; | ||||
|     } | ||||
|     return pointInside; | ||||
| } | ||||
|  | ||||
| - (BOOL)shouldAffectStatusBarAppearance { | ||||
|     return [self isKeyWindow]; | ||||
| } | ||||
|  | ||||
| - (BOOL)canBecomeKeyWindow { | ||||
|     return [self.eventDelegate canBecomeKeyWindow]; | ||||
| } | ||||
|  | ||||
| - (void)makeKeyWindow { | ||||
|     _previousKeyWindow = FLEXUtility.appKeyWindow; | ||||
|     [super makeKeyWindow]; | ||||
| } | ||||
|  | ||||
| - (void)resignKeyWindow { | ||||
|     [super resignKeyWindow]; | ||||
|     _previousKeyWindow = nil; | ||||
| } | ||||
|  | ||||
| + (void)initialize { | ||||
|     // This adds a method (superclass override) at runtime which gives us the status bar behavior we want. | ||||
|     // The FLEX window is intended to be an overlay that generally doesn't affect the app underneath. | ||||
|     // Most of the time, we want the app's main window(s) to be in control of status bar behavior. | ||||
|     // Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways... | ||||
|     NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""]; | ||||
|     SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString); | ||||
|     Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance)); | ||||
|     IMP canAffectImplementation = method_getImplementation(shouldAffectMethod); | ||||
|     class_addMethod(self, canAffectSelector, canAffectImplementation, method_getTypeEncoding(shouldAffectMethod)); | ||||
|  | ||||
|     // One more... | ||||
|     NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))]; | ||||
|     SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString); | ||||
|     Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow)); | ||||
|     IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod); | ||||
|     class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation, method_getTypeEncoding(canBecomeKeyMethod)); | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										17
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXWindowManagerController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXWindowManagerController : FLEXTableViewController | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										302
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,302 @@ | ||||
| // | ||||
| //  FLEXWindowManagerController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXWindowManagerController.h" | ||||
| #import "FLEXManager+Private.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
|  | ||||
| @interface FLEXWindowManagerController () | ||||
| @property (nonatomic) UIWindow *keyWindow; | ||||
| @property (nonatomic, copy) NSString *keyWindowSubtitle; | ||||
| @property (nonatomic, copy) NSArray<UIWindow *> *windows; | ||||
| @property (nonatomic, copy) NSArray<NSString *> *windowSubtitles; | ||||
| @property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13)); | ||||
| @property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles; | ||||
| @property (nonatomic, copy) NSArray<NSArray *> *sections; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXWindowManagerController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.title = @"Windows"; | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.title = @"Windows and Scenes"; | ||||
|     } | ||||
|      | ||||
|     [self disableToolbar]; | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)reloadData { | ||||
|     self.keyWindow = UIApplication.sharedApplication.keyWindow; | ||||
|     self.windows = UIApplication.sharedApplication.windows; | ||||
|     self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]]; | ||||
|     self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) { | ||||
|         return [NSString stringWithFormat:@"Level: %@ — Root: %@", | ||||
|             @(window.windowLevel), window.rootViewController | ||||
|         ]; | ||||
|     }]; | ||||
|      | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects; | ||||
|         self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) { | ||||
|             return [self sceneDescription:scene]; | ||||
|         }]; | ||||
|          | ||||
|         self.sections = @[@[self.keyWindow], self.windows, self.scenes]; | ||||
|     } else { | ||||
|         self.sections = @[@[self.keyWindow], self.windows]; | ||||
|     } | ||||
|      | ||||
|     [self.tableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     [self dismissViewControllerAnimated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
| - (void)showRevertOrDismissAlert:(void(^)(void))revertBlock { | ||||
|     [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; | ||||
|     [self reloadData]; | ||||
|     [self.tableView reloadData]; | ||||
|      | ||||
|     UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow; | ||||
|     UIWindowLevel maxLevel = 0; | ||||
|     for (UIWindow *window in UIApplication.sharedApplication.windows) { | ||||
|         if (window.windowLevel > maxLevel) { | ||||
|             maxLevel = window.windowLevel; | ||||
|             highestWindow = window; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Keep Changes?"); | ||||
|         make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below."); | ||||
|          | ||||
|         make.button(@"Keep Changes").destructiveStyle(); | ||||
|         make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             [self dismissAnimated]; | ||||
|         }); | ||||
|         make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             revertBlock(); | ||||
|             [self reloadData]; | ||||
|             [self.tableView reloadData]; | ||||
|         }); | ||||
|     } showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]]; | ||||
| } | ||||
|  | ||||
| - (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) { | ||||
|     NSString *state = [self stringFromSceneState:scene.activationState]; | ||||
|     NSString *title = scene.title.length ? scene.title : nil; | ||||
|     NSString *suffix = nil; | ||||
|      | ||||
|     if ([scene isKindOfClass:[UIWindowScene class]]) { | ||||
|         UIWindowScene *windowScene = (id)scene; | ||||
|         suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window"); | ||||
|     } | ||||
|      | ||||
|     NSMutableString *description = state.mutableCopy; | ||||
|     if (title) { | ||||
|         [description appendFormat:@" — %@", title]; | ||||
|     } | ||||
|     if (suffix) { | ||||
|         [description appendFormat:@" — %@", suffix]; | ||||
|     } | ||||
|      | ||||
|     return description.copy; | ||||
| } | ||||
|  | ||||
| - (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) { | ||||
|     switch (state) { | ||||
|         case UISceneActivationStateUnattached: | ||||
|             return @"Unattached"; | ||||
|         case UISceneActivationStateForegroundActive: | ||||
|             return @"Active"; | ||||
|         case UISceneActivationStateForegroundInactive: | ||||
|             return @"Inactive"; | ||||
|         case UISceneActivationStateBackground: | ||||
|             return @"Backgrounded"; | ||||
|     } | ||||
|      | ||||
|     return [NSString stringWithFormat:@"Unknown state: %@", @(state)]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { | ||||
|     return self.sections.count; | ||||
| } | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.sections[section].count; | ||||
| } | ||||
|  | ||||
| - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { | ||||
|     switch (section) { | ||||
|         case 0: return @"Key Window"; | ||||
|         case 1: return @"Windows"; | ||||
|         case 2: return @"Connected Scenes"; | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath]; | ||||
|     cell.accessoryType = UITableViewCellAccessoryDetailButton; | ||||
|     cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; | ||||
|      | ||||
|     UIWindow *window = nil; | ||||
|     NSString *subtitle = nil; | ||||
|      | ||||
|     switch (indexPath.section) { | ||||
|         case 0: | ||||
|             window = self.keyWindow; | ||||
|             subtitle = self.keyWindowSubtitle; | ||||
|             break; | ||||
|         case 1: | ||||
|             window = self.windows[indexPath.row]; | ||||
|             subtitle = self.windowSubtitles[indexPath.row]; | ||||
|             break; | ||||
|         case 2: | ||||
|             if (@available(iOS 13, *)) { | ||||
|                 UIScene *scene = self.scenes[indexPath.row]; | ||||
|                 cell.textLabel.text = scene.description; | ||||
|                 cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row]; | ||||
|                 return cell; | ||||
|             } | ||||
|     } | ||||
|      | ||||
|     cell.textLabel.text = window.description; | ||||
|     cell.detailTextLabel.text = [NSString | ||||
|         stringWithFormat:@"Level: %@ — Root: %@", | ||||
|         @((NSInteger)window.windowLevel), window.rootViewController.class | ||||
|     ]; | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UIWindow *window = nil; | ||||
|     NSString *subtitle = nil; | ||||
|     FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow; | ||||
|      | ||||
|     id cancelHandler = ^{ | ||||
|         [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; | ||||
|     }; | ||||
|      | ||||
|     switch (indexPath.section) { | ||||
|         case 0: | ||||
|             window = self.keyWindow; | ||||
|             subtitle = self.keyWindowSubtitle; | ||||
|             break; | ||||
|         case 1: | ||||
|             window = self.windows[indexPath.row]; | ||||
|             subtitle = self.windowSubtitles[indexPath.row]; | ||||
|             break; | ||||
|         case 2: | ||||
|             if (@available(iOS 13, *)) { | ||||
|                 UIScene *scene = self.scenes[indexPath.row]; | ||||
|                 UIWindowScene *oldScene = flex.windowScene; | ||||
|                 BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]]; | ||||
|                 BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO; | ||||
|                  | ||||
|                 [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|                     make.title(NSStringFromClass(scene.class)); | ||||
|                      | ||||
|                     if (isWindowScene) { | ||||
|                         if (isFLEXScene) { | ||||
|                             make.message(@"Already the FLEX window scene"); | ||||
|                         } | ||||
|                          | ||||
|                         make.button(@"Set as FLEX Window Scene") | ||||
|                         .handler(^(NSArray<NSString *> *strings) { | ||||
|                             flex.windowScene = (id)scene; | ||||
|                             [self showRevertOrDismissAlert:^{ | ||||
|                                 flex.windowScene = oldScene; | ||||
|                             }]; | ||||
|                         }).enabled(!isFLEXScene); | ||||
|                         make.button(@"Cancel").cancelStyle(); | ||||
|                     } else { | ||||
|                         make.message(@"Not a UIWindowScene"); | ||||
|                         make.button(@"Dismiss").cancelStyle().handler(cancelHandler); | ||||
|                     } | ||||
|                 } showFrom:self]; | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     __block UIWindow *targetWindow = nil, *oldKeyWindow = nil; | ||||
|     __block UIWindowLevel oldLevel; | ||||
|     __block BOOL wasVisible; | ||||
|      | ||||
|     subtitle = [subtitle stringByAppendingString: | ||||
|         @"\n\n1) Adjust the FLEX window level relative to this window,\n" | ||||
|         "2) adjust this window's level relative to the FLEX window,\n" | ||||
|         "3) set this window's level to a specific value, or\n" | ||||
|         "4) make this window the key window if it isn't already." | ||||
|     ]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(NSStringFromClass(window.class)).message(subtitle); | ||||
|         make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) { | ||||
|             targetWindow = flex; oldLevel = flex.windowLevel; | ||||
|             flex.windowLevel = window.windowLevel + strings.firstObject.integerValue; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }]; | ||||
|         }); | ||||
|         make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) { | ||||
|             targetWindow = window; oldLevel = window.windowLevel; | ||||
|             window.windowLevel = flex.windowLevel + strings.firstObject.integerValue; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }]; | ||||
|         }); | ||||
|         make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) { | ||||
|             targetWindow = window; oldLevel = window.windowLevel; | ||||
|             window.windowLevel = strings.firstObject.integerValue; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }]; | ||||
|         }); | ||||
|         make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) { | ||||
|             oldKeyWindow = UIApplication.sharedApplication.keyWindow; | ||||
|             wasVisible = window.hidden; | ||||
|             [window makeKeyAndVisible]; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ | ||||
|                 window.hidden = wasVisible; | ||||
|                 [oldKeyWindow makeKeyWindow]; | ||||
|             }]; | ||||
|         }).enabled(!window.isKeyWindow && !window.hidden); | ||||
|         make.button(@"Cancel").cancelStyle().handler(cancelHandler); | ||||
|          | ||||
|         make.textField(@"+/- window level, i.e. 5 or -10"); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip { | ||||
|     [self.navigationController pushViewController: | ||||
|         [FLEXObjectExplorerFactory explorerViewControllerForObject:self.sections[ip.section][ip.row]] | ||||
|     animated:YES]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										45
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| // | ||||
| //  FLEXTabList.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/1/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXTabList : NSObject | ||||
|  | ||||
| @property (nonatomic, readonly, class) FLEXTabList *sharedList; | ||||
|  | ||||
| @property (nonatomic, readonly, nullable) UINavigationController *activeTab; | ||||
| @property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs; | ||||
| /// Snapshots of each tab when they were last active. | ||||
| @property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots; | ||||
| /// \c NSNotFound if no tabs are present. | ||||
| /// Setting this property changes the active tab to one of the already open tabs. | ||||
| @property (nonatomic) NSInteger activeTabIndex; | ||||
|  | ||||
| /// Adds a new tab and sets the new tab as the active tab. | ||||
| - (void)addTab:(UINavigationController *)newTab; | ||||
| /// Closes the given tab. If this tab was the active tab, | ||||
| /// the most recent tab before that becomes the active tab. | ||||
| - (void)closeTab:(UINavigationController *)tab; | ||||
| /// Closes a tab at the given index. If this tab was the active tab, | ||||
| /// the most recent tab before that becomes the active tab. | ||||
| - (void)closeTabAtIndex:(NSInteger)idx; | ||||
| /// Closes all of the tabs at the given indexes. If the active tab | ||||
| /// is included, the most recent still-open tab becomes the active tab. | ||||
| - (void)closeTabsAtIndexes:(NSIndexSet *)indexes; | ||||
| /// A shortcut to close the active tab. | ||||
| - (void)closeActiveTab; | ||||
| /// A shortcut to close \e every tab. | ||||
| - (void)closeAllTabs; | ||||
|  | ||||
| - (void)updateSnapshotForActiveTab; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										133
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| // | ||||
| //  FLEXTabList.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/1/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTabList.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXTabList () { | ||||
|     NSMutableArray *_openTabs; | ||||
|     NSMutableArray *_openTabSnapshots; | ||||
| } | ||||
| @end | ||||
| #pragma mark - | ||||
| @implementation FLEXTabList | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| + (FLEXTabList *)sharedList { | ||||
|     static FLEXTabList *sharedList = nil; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         sharedList = [self new]; | ||||
|     }); | ||||
|      | ||||
|     return sharedList; | ||||
| } | ||||
|  | ||||
| - (id)init { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _openTabs = [NSMutableArray new]; | ||||
|         _openTabSnapshots = [NSMutableArray new]; | ||||
|         _activeTabIndex = NSNotFound; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| - (void)chooseNewActiveTab { | ||||
|     if (self.openTabs.count) { | ||||
|         self.activeTabIndex = self.openTabs.count - 1; | ||||
|     } else { | ||||
|         self.activeTabIndex = NSNotFound; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Public | ||||
|  | ||||
| - (void)setActiveTabIndex:(NSInteger)idx { | ||||
|     NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound); | ||||
|     if (_activeTabIndex == idx) return; | ||||
|      | ||||
|     _activeTabIndex = idx; | ||||
|     _activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx]; | ||||
| } | ||||
|  | ||||
| - (void)addTab:(UINavigationController *)newTab { | ||||
|     NSParameterAssert(newTab); | ||||
|      | ||||
|     // Update snapshot of the last active tab | ||||
|     if (self.activeTab) { | ||||
|         [self updateSnapshotForActiveTab]; | ||||
|     } | ||||
|      | ||||
|     // Add new tab and snapshot, | ||||
|     // update active tab and index | ||||
|     [_openTabs addObject:newTab]; | ||||
|     [_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]]; | ||||
|     _activeTab = newTab; | ||||
|     _activeTabIndex = self.openTabs.count - 1; | ||||
| } | ||||
|  | ||||
| - (void)closeTab:(UINavigationController *)tab { | ||||
|     NSParameterAssert(tab); | ||||
|     NSInteger idx = [self.openTabs indexOfObject:tab]; | ||||
|     if (idx != NSNotFound) { | ||||
|         [self closeTabAtIndex:idx]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeTabAtIndex:(NSInteger)idx { | ||||
|     NSParameterAssert(idx < self.openTabs.count); | ||||
|      | ||||
|     // Remove old tab and snapshot | ||||
|     [_openTabs removeObjectAtIndex:idx]; | ||||
|     [_openTabSnapshots removeObjectAtIndex:idx]; | ||||
|      | ||||
|     // Update active tab and index if needed | ||||
|     if (self.activeTabIndex == idx) { | ||||
|         [self chooseNewActiveTab]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeTabsAtIndexes:(NSIndexSet *)indexes { | ||||
|     // Remove old tabs and snapshot | ||||
|     [_openTabs removeObjectsAtIndexes:indexes]; | ||||
|     [_openTabSnapshots removeObjectsAtIndexes:indexes]; | ||||
|      | ||||
|     // Update active tab and index if needed | ||||
|     if ([indexes containsIndex:self.activeTabIndex]) { | ||||
|         [self chooseNewActiveTab]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeActiveTab { | ||||
|     [self closeTab:self.activeTab]; | ||||
| } | ||||
|  | ||||
| - (void)closeAllTabs { | ||||
|     // Remove tabs and snapshots | ||||
|     [_openTabs removeAllObjects]; | ||||
|     [_openTabSnapshots removeAllObjects]; | ||||
|      | ||||
|     // Update active tab index | ||||
|     self.activeTabIndex = NSNotFound; | ||||
| } | ||||
|  | ||||
| - (void)updateSnapshotForActiveTab { | ||||
|     if (self.activeTabIndex != NSNotFound) { | ||||
|         UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view]; | ||||
|         [_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										13
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXTabsViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/4/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| @interface FLEXTabsViewController : FLEXTableViewController | ||||
|  | ||||
| @end | ||||
							
								
								
									
										335
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,335 @@ | ||||
| // | ||||
| //  FLEXTabsViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/4/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTabsViewController.h" | ||||
| #import "FLEXNavigationController.h" | ||||
| #import "FLEXTabList.h" | ||||
| #import "FLEXBookmarkManager.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "FLEXExplorerViewController.h" | ||||
| #import "FLEXGlobalsViewController.h" | ||||
| #import "FLEXBookmarksViewController.h" | ||||
|  | ||||
| @interface FLEXTabsViewController () | ||||
| @property (nonatomic, copy) NSArray<UINavigationController *> *openTabs; | ||||
| @property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots; | ||||
| @property (nonatomic) NSInteger activeIndex; | ||||
| @property (nonatomic) BOOL presentNewActiveTabOnDismiss; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXExplorerViewController *corePresenter; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTabsViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.title = @"Open Tabs"; | ||||
|     self.navigationController.hidesBarsOnSwipe = NO; | ||||
|     self.tableView.allowsMultipleSelectionDuringEditing = YES; | ||||
|      | ||||
|     [self reloadData:NO]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|     [self setupDefaultBarItems]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidAppear:(BOOL)animated { | ||||
|     [super viewDidAppear:animated]; | ||||
|      | ||||
|     // Instead of updating the active snapshot before we present, | ||||
|     // we update it after we present to avoid pre-presenation latency | ||||
|     dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|         [FLEXTabList.sharedList updateSnapshotForActiveTab]; | ||||
|         [self reloadData:NO]; | ||||
|         [self.tableView reloadData]; | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| /// @param trackActiveTabDelta whether to check if the active | ||||
| /// tab changed and needs to be presented upon "Done" dismissal. | ||||
| /// @return whether the active tab changed or not (if there are any tabs left) | ||||
| - (BOOL)reloadData:(BOOL)trackActiveTabDelta { | ||||
|     BOOL activeTabDidChange = NO; | ||||
|     FLEXTabList *list = FLEXTabList.sharedList; | ||||
|      | ||||
|     // Flag to enable check to determine whether | ||||
|     if (trackActiveTabDelta) { | ||||
|         NSInteger oldActiveIndex = self.activeIndex; | ||||
|         if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) { | ||||
|             self.presentNewActiveTabOnDismiss = YES; | ||||
|             activeTabDidChange = YES; | ||||
|         } else if (self.presentNewActiveTabOnDismiss) { | ||||
|             // If we had something to present before, now we don't | ||||
|             // (i.e. activeTabIndex == NSNotFound) | ||||
|             self.presentNewActiveTabOnDismiss = NO; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // We assume the tabs aren't going to change out from under us, since | ||||
|     // presenting any other tool via keyboard shortcuts should dismiss us first | ||||
|     self.openTabs = list.openTabs; | ||||
|     self.tabSnapshots = list.openTabSnapshots; | ||||
|     self.activeIndex = list.activeTabIndex; | ||||
|      | ||||
|     return activeTabDidChange; | ||||
| } | ||||
|  | ||||
| - (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged { | ||||
|     // Refresh the newly active tab row if needed | ||||
|     if (activeTabChanged) { | ||||
|         NSIndexPath *active = [NSIndexPath | ||||
|            indexPathForRow:self.activeIndex inSection:0 | ||||
|         ]; | ||||
|         [self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setupDefaultBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated)); | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_fixedSpace, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed:)), | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)), | ||||
|     ]; | ||||
|      | ||||
|     // Disable editing if no tabs available | ||||
|     self.toolbarItems.lastObject.enabled = self.openTabs.count > 0; | ||||
| } | ||||
|  | ||||
| - (void)setupEditingBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = nil; | ||||
|     self.toolbarItems = @[ | ||||
|         [UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         [UIBarButtonItem flex_disabledSystemItem:UIBarButtonSystemItemAdd], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         // We use a non-system done item because we change its title dynamically | ||||
|         [UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)] | ||||
|     ]; | ||||
|      | ||||
|     self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor; | ||||
| } | ||||
|  | ||||
| - (FLEXExplorerViewController *)corePresenter { | ||||
|     // We must be presented by a FLEXExplorerViewController, or presented | ||||
|     // by another view controller that was presented by FLEXExplorerViewController | ||||
|     FLEXExplorerViewController *presenter = (id)self.presentingViewController; | ||||
|     presenter = (id)presenter.presentingViewController ?: presenter; | ||||
|     NSAssert( | ||||
|         [presenter isKindOfClass:[FLEXExplorerViewController class]], | ||||
|         @"The tabs view controller expects to be presented by the explorer controller" | ||||
|     ); | ||||
|     return presenter; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Button Actions | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     if (self.presentNewActiveTabOnDismiss) { | ||||
|         // The active tab was closed so we need to present the new one | ||||
|         UIViewController *activeTab = FLEXTabList.sharedList.activeTab; | ||||
|         FLEXExplorerViewController *presenter = self.corePresenter; | ||||
|         [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|             [presenter presentViewController:activeTab animated:YES completion:nil]; | ||||
|         }]; | ||||
|     } else if (self.activeIndex == NSNotFound) { | ||||
|         // The only tab was closed, so dismiss everything | ||||
|         [self.corePresenter dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } else { | ||||
|         // Simple dismiss with the same active tab, only dismiss myself | ||||
|         [self dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)toggleEditing { | ||||
|     NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows; | ||||
|     self.editing = !self.editing; | ||||
|      | ||||
|     if (self.isEditing) { | ||||
|         [self setupEditingBarItems]; | ||||
|     } else { | ||||
|         [self setupDefaultBarItems]; | ||||
|          | ||||
|         // Get index set of tabs to close | ||||
|         NSMutableIndexSet *indexes = [NSMutableIndexSet new]; | ||||
|         for (NSIndexPath *ip in selected) { | ||||
|             [indexes addIndex:ip.row]; | ||||
|         } | ||||
|          | ||||
|         if (selected.count) { | ||||
|             // Close tabs and update data source | ||||
|             [FLEXTabList.sharedList closeTabsAtIndexes:indexes]; | ||||
|             BOOL activeTabChanged = [self reloadData:YES]; | ||||
|              | ||||
|             // Remove deleted rows | ||||
|             [self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|              | ||||
|             // Refresh the newly active tab row if needed | ||||
|             [self reloadActiveTabRowIfChanged:activeTabChanged]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)addTabButtonPressed:(UIBarButtonItem *)sender { | ||||
|     if (FLEXBookmarkManager.bookmarks.count) { | ||||
|         [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|             make.title(@"New Tab"); | ||||
|             make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) { | ||||
|                 [self addTabAndDismiss:[FLEXNavigationController | ||||
|                     withRootViewController:[FLEXGlobalsViewController new] | ||||
|                 ]]; | ||||
|             }); | ||||
|             make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) { | ||||
|                 [self presentViewController:[FLEXNavigationController | ||||
|                     withRootViewController:[FLEXBookmarksViewController new] | ||||
|                 ] animated:YES completion:nil]; | ||||
|             }); | ||||
|             make.button(@"Cancel").cancelStyle(); | ||||
|         } showFrom:self source:sender]; | ||||
|     } else { | ||||
|         // No bookmarks, just open the main menu | ||||
|         [self addTabAndDismiss:[FLEXNavigationController | ||||
|             withRootViewController:[FLEXGlobalsViewController new] | ||||
|         ]]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)addTabAndDismiss:(UINavigationController *)newTab { | ||||
|     FLEXExplorerViewController *presenter = self.corePresenter; | ||||
|     [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|         [presenter presentViewController:newTab animated:YES completion:nil]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (void)closeAllButtonPressed:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         NSInteger count = self.openTabs.count; | ||||
|         NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab"); | ||||
|         make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             [self closeAll]; | ||||
|             [self toggleEditing]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| - (void)closeAll { | ||||
|     NSInteger rowCount = self.openTabs.count; | ||||
|      | ||||
|     // Close tabs and update data source | ||||
|     [FLEXTabList.sharedList closeAllTabs]; | ||||
|     [self reloadData:YES]; | ||||
|      | ||||
|     // Delete rows from table view | ||||
|     NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) { | ||||
|         return [NSIndexPath indexPathForRow:row inSection:0]; | ||||
|     }]; | ||||
|     [self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.openTabs.count; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath]; | ||||
|      | ||||
|     UINavigationController *tab = self.openTabs[indexPath.row]; | ||||
|     cell.imageView.image = self.tabSnapshots[indexPath.row]; | ||||
|     cell.textLabel.text = tab.topViewController.title; | ||||
|     cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page"); | ||||
|      | ||||
|     if (!cell.tag) { | ||||
|         cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; | ||||
|         cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; | ||||
|         cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; | ||||
|         cell.tag = 1; | ||||
|     } | ||||
|      | ||||
|     if (indexPath.row == self.activeIndex) { | ||||
|         cell.backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|     } else { | ||||
|         cell.backgroundColor = FLEXColor.primaryBackgroundColor; | ||||
|     } | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (self.editing) { | ||||
|         // Case: editing with multi-select | ||||
|         self.toolbarItems.lastObject.title = @"Close Selected"; | ||||
|         self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor; | ||||
|     } else { | ||||
|         if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) { | ||||
|             // Case: selected the already active tab | ||||
|             [self dismissAnimated]; | ||||
|         } else { | ||||
|             // Case: selected a different tab, | ||||
|             // or selected a tab when presented from the FLEX toolbar | ||||
|             FLEXTabList.sharedList.activeTabIndex = indexPath.row; | ||||
|             self.presentNewActiveTabOnDismiss = YES; | ||||
|             [self dismissAnimated]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(self.editing); | ||||
|      | ||||
|     if (tableView.indexPathsForSelectedRows.count == 0) { | ||||
|         self.toolbarItems.lastObject.title = @"Done"; | ||||
|         self.toolbarItems.lastObject.tintColor = self.view.tintColor; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)table | ||||
| commitEditingStyle:(UITableViewCellEditingStyle)edit | ||||
| forRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(edit == UITableViewCellEditingStyleDelete); | ||||
|      | ||||
|     // Close tab and update data source | ||||
|     [FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]]; | ||||
|     BOOL activeTabChanged = [self reloadData:YES]; | ||||
|      | ||||
|     // Delete row from table view | ||||
|     [table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|      | ||||
|     // Refresh the newly active tab row if needed | ||||
|     [self reloadActiveTabRowIfChanged:activeTabChanged]; | ||||
| } | ||||
|  | ||||
| @end | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn