├── AVTools.h ├── AVTools.plist ├── CrossOverIPC.h ├── Makefile ├── README.md ├── Tweak.xm ├── control ├── ent.plist └── layout └── DEBIAN └── postinst /AVTools.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | 5 | #pragma GCC diagnostic ignored "-Wunused-variable" 6 | #pragma GCC diagnostic ignored "-Wprotocol" 7 | #pragma GCC diagnostic ignored "-Wmacro-redefined" 8 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 9 | #pragma GCC diagnostic ignored "-Wincomplete-implementation" 10 | #pragma GCC diagnostic ignored "-Wunknown-pragmas" 11 | #pragma GCC diagnostic ignored "-Wformat" 12 | #pragma GCC diagnostic ignored "-Wunknown-warning-option" 13 | #pragma GCC diagnostic ignored "-Wincompatible-pointer-types" 14 | #pragma GCC diagnostic ignored "-Wunused-value" 15 | #pragma GCC diagnostic ignored "-Wnullability-completeness" 16 | 17 | 18 | #define rgbValue 19 | #define UIColorFromHEX(rgbValue) [UIColor \ 20 | colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ 21 | green:((float)((rgbValue & 0xFF00) >> 8))/255.0 \ 22 | blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0] 23 | 24 | 25 | static UIViewController *_topMostController(UIViewController *cont) { 26 | UIViewController *topController = cont; 27 | while (topController.presentedViewController) { 28 | topController = topController.presentedViewController; 29 | } 30 | if ([topController isKindOfClass:[UINavigationController class]]) { 31 | UIViewController *visible = ((UINavigationController *)topController).visibleViewController; 32 | if (visible) { 33 | topController = visible; 34 | } 35 | } 36 | return (topController != cont ? topController : nil); 37 | } 38 | static UIViewController *topMostController() { 39 | UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController; 40 | UIViewController *next = nil; 41 | while ((next = _topMostController(topController)) != nil) { 42 | topController = next; 43 | } 44 | return topController; 45 | } 46 | 47 | 48 | void WriteData(id Data, NSString *DataFileName) { 49 | 50 | NSString *StringOfDate = [NSString stringWithFormat:@"%@",Data]; 51 | 52 | NSString *DataFile = [NSString stringWithFormat:@"%@/Documents/%@",NSHomeDirectory(),DataFileName]; 53 | 54 | if (![[NSFileManager defaultManager] fileExistsAtPath:DataFile]) 55 | [[NSFileManager defaultManager] createFileAtPath:DataFile contents:nil attributes:nil]; 56 | 57 | NSString *PrevData = [NSString stringWithContentsOfFile:DataFile encoding:NSUTF8StringEncoding error:nil]; 58 | 59 | NSString *NewData = [NSString stringWithFormat:@"%@\n\n-----------\n\n%@",PrevData,StringOfDate]; 60 | 61 | [NewData writeToFile:DataFile atomically:YES encoding:NSUTF8StringEncoding error:nil]; 62 | } 63 | 64 | 65 | 66 | void Alert(float Timer,id Message, ...) { 67 | 68 | va_list args; 69 | va_start(args, Message); 70 | NSString *Formated = [[NSString alloc] initWithFormat:Message arguments:args]; 71 | va_end(args); 72 | 73 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 74 | 75 | [NSThread sleepForTimeInterval:Timer]; 76 | 77 | dispatch_async(dispatch_get_main_queue(), ^{ 78 | 79 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Hola" message:Formated preferredStyle:UIAlertControllerStyleAlert]; 80 | 81 | UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 82 | }]; 83 | 84 | [alert addAction:action]; 85 | 86 | [topMostController() presentViewController:alert animated:true completion:nil]; 87 | 88 | 89 | }); 90 | 91 | }); 92 | 93 | } 94 | 95 | 96 | 97 | 98 | @interface CMManagerMini : NSObject 99 | +(void) InitTextFieldAlertWithTitle:(NSString *)Title Message:(NSString *)Message Buttons:(NSArray *)Buttons CancelButtonTitle:(NSString *)CancelButtonTitle handler:(void(^_Nullable)(NSString * ButtonTitle, NSString * Text, UITextField * TextField))handler; 100 | @end 101 | 102 | @implementation CMManagerMini 103 | 104 | +(void) InitTextFieldAlertWithTitle:(NSString *)Title Message:(NSString *)Message Buttons:(NSArray *)Buttons CancelButtonTitle:(NSString *)CancelButtonTitle handler:(void(^)(NSString * ButtonTitle, NSString * Text, UITextField * TextField))handler { 105 | 106 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:Title message:Message preferredStyle:UIAlertControllerStyleAlert]; 107 | [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { 108 | textField.clearButtonMode = UITextFieldViewModeWhileEditing; 109 | [textField resignFirstResponder]; 110 | handler(nil,nil,textField); 111 | }]; 112 | for (NSString *EachButton in Buttons) { 113 | NSArray *fields = alert.textFields; 114 | UITextField *getText = [fields firstObject]; 115 | UIAlertAction *action = [UIAlertAction actionWithTitle:EachButton style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 116 | handler(action.title,getText.text,nil); 117 | }]; 118 | [alert addAction:action]; 119 | } 120 | if (!(CancelButtonTitle == NULL)) { 121 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:CancelButtonTitle style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 122 | }]; 123 | [alert addAction:cancelAction]; 124 | } 125 | [topMostController() presentViewController:alert animated:true completion:nil]; 126 | } 127 | @end 128 | 129 | 130 | 131 | 132 | UIButton *InitButtonWithName(NSString *BuName, UIView *View, id Target,SEL Action){ 133 | 134 | UIButton *Button = [UIButton buttonWithType:UIButtonTypeCustom]; 135 | [Button setTitle:BuName forState:UIControlStateNormal]; 136 | [Button addTarget:Target action:Action forControlEvents:UIControlEventTouchUpInside]; 137 | [View addSubview:Button]; 138 | 139 | return Button; 140 | } 141 | 142 | 143 | #define MainPlist @"/var/jb/var/mobile/Library/Preferences/AVTools.plist" 144 | 145 | void WriteToPlist(BOOL isLock) { 146 | 147 | NSMutableDictionary *MainDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:MainPlist]; 148 | isLock ? [MainDictionary setValue:@"1" forKey:@"isLock"] : [MainDictionary setValue:@"2" forKey:@"isLock"]; 149 | 150 | if (!MainDictionary[@"SeekTime"]) 151 | [MainDictionary setValue:@"15" forKey:@"SeekTime"]; 152 | 153 | [MainDictionary writeToFile:MainPlist atomically:YES]; 154 | } 155 | 156 | BOOL isLocked(void) { 157 | if ([[NSMutableDictionary dictionaryWithContentsOfFile:MainPlist][@"isLock"] isEqual:@"1"]) return YES; 158 | return NO; 159 | } 160 | 161 | 162 | 163 | @interface SBControlCenterSystemAgent : NSObject 164 | -(void) unlockOrientation; 165 | -(void) lockOrientation; 166 | -(BOOL) isOrientationLocked; 167 | @end 168 | 169 | @interface SBLockScreenManager : NSObject 170 | -(void) NotificationAction:(NSString *)Name userInfo:(NSDictionary *)Info; 171 | @end 172 | 173 | @interface AVPlayerViewController : UIViewController 174 | @end 175 | 176 | @interface AVLayoutView : UIView 177 | @property NSString *debugIdentifier; 178 | @end 179 | 180 | 181 | @interface AVTouchIgnoringView : UIView 182 | @end 183 | 184 | @interface AVPlaybackControlsController : NSObject 185 | -(void)_seekByTimeInterval:(double)arg1 toleranceBefore:(double)arg2 toleranceAfter:(double)arg3; 186 | @end 187 | 188 | @interface AVButton : NSObject 189 | @property NSString *imageName; 190 | @end 191 | -------------------------------------------------------------------------------- /AVTools.plist: -------------------------------------------------------------------------------- 1 | { 2 | Filter = { 3 | Bundles = ( 4 | "com.apple.AVKit", 5 | "com.apple.springboard" 6 | ); 7 | }; 8 | } -------------------------------------------------------------------------------- /CrossOverIPC.h: -------------------------------------------------------------------------------- 1 | @import CoreFoundation; 2 | @import Foundation; 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #import 9 | 10 | 11 | typedef enum CrossOverIPCServiceType : CFIndex { 12 | 13 | SERVICE_TYPE_SENDER, 14 | SERVICE_TYPE_LISTENER 15 | 16 | } CrossOverIPCServiceType; 17 | 18 | 19 | @interface CrossOverIPC : NSObject 20 | 21 | + (instancetype) centerNamed:(NSString *)serviceName type:(CrossOverIPCServiceType)type; 22 | - (void) sendMessageName:(NSString *)msgName userInfo:(NSDictionary *)userInfo; 23 | - (NSDictionary *) sendMessageAndReceiveReplyName:(NSString *)msgName userInfo:(NSDictionary *)userInfo; 24 | - (void) registerForMessageName:(NSString *)msgName target:(id)target selector:(SEL)sel; 25 | 26 | @end 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARCHS = arm64e arm64 2 | 3 | 4 | 5 | THEOS_PACKAGE_SCHEME=rootless 6 | 7 | export SDKVERSION = 14.5 8 | 9 | export iP = 192.168.1.102 10 | export Port = 22 11 | export Pass = alpine 12 | export Bundle = com.apple.springboard 13 | 14 | DEBUG = 0 15 | 16 | INSTALL_TARGET_PROCESSES = SpringBoard 17 | 18 | include $(THEOS)/makefiles/common.mk 19 | 20 | TWEAK_NAME = AVTools 21 | 22 | AVTools_FILES = Tweak.xm 23 | AVTools_CFLAGS = -fobjc-arc 24 | AVTools_PRIVATE_FRAMEWORKS = SpringBoardServices 25 | AVTools_CODESIGN_FLAGS = -Sent.plist 26 | 27 | include $(THEOS_MAKE_PATH)/tweak.mk 28 | 29 | 30 | before-package:: 31 | $(ECHO_NOTHING) chmod 755 $(CURDIR)/.theos/_/DEBIAN/* $(ECHO_END) 32 | $(ECHO_NOTHING) chmod 755 $(CURDIR)/.theos/_/DEBIAN $(ECHO_END) 33 | 34 | 35 | install6:: 36 | install6.exec 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AVTools 2 | iOS tweak for av player 3 | 4 | # Features 5 | - Rotation button 6 | - Custom skip sec instead of 15 seconds 7 | 8 | # Supports iOS 13 , 14 , 15 and 16 9 | 10 | ### ScreenShot 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tweak.xm: -------------------------------------------------------------------------------- 1 | // By @CrazyMind90 2 | 3 | #import 4 | #import 5 | #import "AVTools.h" 6 | #import "CrossOverIPC.h" 7 | 8 | #pragma GCC diagnostic ignored "-Wunused-variable" 9 | #pragma GCC diagnostic ignored "-Wprotocol" 10 | #pragma GCC diagnostic ignored "-Wmacro-redefined" 11 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 12 | #pragma GCC diagnostic ignored "-Wincomplete-implementation" 13 | #pragma GCC diagnostic ignored "-Wunknown-pragmas" 14 | #pragma GCC diagnostic ignored "-Wformat" 15 | #pragma GCC diagnostic ignored "-Wunknown-warning-option" 16 | #pragma GCC diagnostic ignored "-Wincompatible-pointer-types" 17 | #pragma GCC diagnostic ignored "-Wunused-value" 18 | 19 | 20 | 21 | BOOL DidStartServer = NO; 22 | BOOL DidInitButton = NO; 23 | BOOL isSeekingAllowed = NO; 24 | 25 | 26 | %hook AVPlayerViewController 27 | -(void) viewWillAppear:(BOOL)arg { 28 | %orig; 29 | 30 | DidInitButton = NO; 31 | } 32 | %end 33 | 34 | 35 | 36 | void SendNotificationMessage(NSString *Action) { 37 | // CPDistributedMessagingCenter *c = [CPDistributedMessagingCenter centerNamed:@"com.crazymind90.AVTools"]; 38 | // rocketbootstrap_distributedmessagingcenter_apply(c); 39 | // [c sendMessageName:@"AVTools" userInfo:@{@"Action" : Action}]; 40 | #define _serviceName @"com.crazymind90.AVTools" 41 | CrossOverIPC *crossOver = [objc_getClass("CrossOverIPC") centerNamed:_serviceName type:SERVICE_TYPE_SENDER]; 42 | [crossOver sendMessageName:@"AVTools" userInfo:@{@"Action" : Action}]; 43 | } 44 | 45 | 46 | %hook SBLockScreenManager 47 | -(void) lockScreenViewControllerDidDismiss { 48 | 49 | %orig; 50 | 51 | if (![[NSFileManager defaultManager] fileExistsAtPath:MainPlist]) 52 | [@{} writeToFile:MainPlist atomically:YES]; 53 | 54 | if (!DidStartServer) { 55 | #define _serviceName @"com.crazymind90.AVTools" 56 | CrossOverIPC *crossOver = [objc_getClass("CrossOverIPC") centerNamed:_serviceName type:SERVICE_TYPE_LISTENER]; 57 | [crossOver registerForMessageName:@"AVTools" target:self selector:@selector(NotificationAction:userInfo:)]; 58 | 59 | DidStartServer = YES; 60 | } 61 | 62 | WriteToPlist([[%c(SBControlCenterSystemAgent) alloc] isOrientationLocked]); 63 | } 64 | 65 | %new 66 | -(void) NotificationAction:(NSString *)Name userInfo:(NSDictionary *)Info { 67 | 68 | if ([(NSString *)[Info objectForKey:@"Action"] isEqual:@"Rotate"]) { 69 | SBControlCenterSystemAgent *SystemAgent = [[%c(SBControlCenterSystemAgent) alloc] init]; 70 | if ([SystemAgent isOrientationLocked]) 71 | [SystemAgent unlockOrientation]; 72 | else 73 | [SystemAgent lockOrientation]; 74 | 75 | } else { 76 | 77 | NSMutableDictionary *MainDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:MainPlist]; 78 | [MainDictionary setValue:(NSString *)Info[@"Action"] forKey:@"SeekTime"]; 79 | [MainDictionary writeToFile:MainPlist atomically:YES]; 80 | 81 | } 82 | } 83 | %end 84 | 85 | %hook SBControlCenterSystemAgent 86 | -(void) unlockOrientation { 87 | %orig; 88 | WriteToPlist(NO); 89 | } 90 | -(void) lockOrientation { 91 | %orig; 92 | WriteToPlist(YES); 93 | } 94 | %end 95 | 96 | 97 | 98 | %hook AVTouchIgnoringView 99 | -(void) layoutSubviews { 100 | 101 | %orig; 102 | 103 | if (self.subviews.count <= 0) 104 | return %orig; 105 | 106 | AVLayoutView *Layout = self.subviews[0]; 107 | 108 | if ([Layout isKindOfClass:[%c(AVLayoutView) class]] && [Layout.debugIdentifier isEqual:@"ScreenModeControls"]) { 109 | 110 | if (!DidInitButton) { 111 | 112 | #pragma mark - RotateButton 113 | UIButton *RotateButton = InitButtonWithName(@"",self,self,@selector(Button_Tapped:)); 114 | 115 | if (isLocked()) { 116 | [RotateButton setImage:[UIImage systemImageNamed:@"lock.rotation"] forState:UIControlStateNormal]; 117 | RotateButton.tintColor = UIColor.orangeColor; 118 | RotateButton.tag = 1; 119 | } else { 120 | [RotateButton setImage:[UIImage systemImageNamed:@"lock.rotation.open"] forState:UIControlStateNormal]; 121 | RotateButton.tintColor = UIColorFromHEX(0x999999); 122 | RotateButton.tag = 2; 123 | } 124 | 125 | RotateButton.alpha = 0.8; 126 | RotateButton.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMinXMaxYCorner; 127 | RotateButton.layer.cornerRadius = 15.4; 128 | RotateButton.layer.backgroundColor = UIColorFromHEX(0x181818).CGColor; 129 | 130 | [RotateButton setTranslatesAutoresizingMaskIntoConstraints:false]; 131 | [NSLayoutConstraint activateConstraints:@[ 132 | [RotateButton.topAnchor constraintEqualToAnchor:Layout.bottomAnchor constant:2], 133 | [RotateButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:0], 134 | [RotateButton.trailingAnchor constraintEqualToAnchor:self.leadingAnchor constant:60], 135 | [RotateButton.bottomAnchor constraintEqualToAnchor:Layout.bottomAnchor constant:49] 136 | ]]; 137 | 138 | #pragma mark - Lock image layout 139 | [RotateButton.imageView setTranslatesAutoresizingMaskIntoConstraints:NO]; 140 | [RotateButton.imageView.topAnchor constraintEqualToAnchor:RotateButton.topAnchor constant:11].active = YES; 141 | [RotateButton.imageView.leadingAnchor constraintEqualToAnchor:RotateButton.leadingAnchor constant:16].active = YES; 142 | [RotateButton.imageView.trailingAnchor constraintEqualToAnchor:RotateButton.trailingAnchor constant:-16].active = YES; 143 | [RotateButton.imageView.bottomAnchor constraintEqualToAnchor:RotateButton.bottomAnchor constant:-11].active = YES; 144 | 145 | 146 | #pragma mark - SeekButton 147 | float SeekTime = [[[NSMutableDictionary dictionaryWithContentsOfFile:MainPlist] objectForKey:@"SeekTime"] floatValue]; 148 | if (SeekTime <= 0) 149 | SendNotificationMessage(@"15"); 150 | 151 | UIButton *SeekButton = InitButtonWithName([NSMutableDictionary dictionaryWithContentsOfFile:MainPlist][@"SeekTime"],self,self,@selector(SeekButton_Tapped:)); 152 | 153 | SeekButton.alpha = 0.8; 154 | SeekButton.layer.maskedCorners = kCALayerMaxXMaxYCorner | kCALayerMaxXMinYCorner; 155 | SeekButton.layer.cornerRadius = 15.4; 156 | SeekButton.layer.backgroundColor = UIColorFromHEX(0x303030).CGColor; 157 | 158 | [SeekButton setTranslatesAutoresizingMaskIntoConstraints:false]; 159 | [NSLayoutConstraint activateConstraints:@[ 160 | [SeekButton.topAnchor constraintEqualToAnchor:Layout.bottomAnchor constant:2], 161 | [SeekButton.leadingAnchor constraintEqualToAnchor:RotateButton.trailingAnchor constant:0], 162 | [SeekButton.trailingAnchor constraintEqualToAnchor:RotateButton.trailingAnchor constant:60], 163 | [SeekButton.bottomAnchor constraintEqualToAnchor:Layout.bottomAnchor constant:49] 164 | ]]; 165 | 166 | DidInitButton = YES; 167 | } 168 | } 169 | } 170 | 171 | %new 172 | -(void) SeekButton_Tapped:(UIButton *)Sender { 173 | 174 | [CMManagerMini InitTextFieldAlertWithTitle:@"Set skip time" Message:nil Buttons:@[@"Set"] CancelButtonTitle:@"Cancel" handler:^(NSString * ButtonTitle, NSString * Text, UITextField * TextField) { 175 | 176 | TextField.keyboardType = UIKeyboardTypeNumberPad; 177 | TextField.text = (NSString *)[[NSMutableDictionary dictionaryWithContentsOfFile:MainPlist] objectForKey:@"SeekTime"]; 178 | 179 | if ([ButtonTitle isEqual:@"Set"]) { 180 | 181 | [Sender setTitle:Text forState:UIControlStateNormal]; 182 | 183 | SendNotificationMessage([NSString stringWithFormat:@"%@",Text] ? : @"10"); 184 | 185 | } 186 | 187 | }]; 188 | 189 | } 190 | 191 | %new 192 | -(void) Button_Tapped:(UIButton *)Sender { 193 | 194 | SendNotificationMessage(@"Rotate"); 195 | 196 | if (Sender.tag == 1) { 197 | [Sender setImage:[UIImage systemImageNamed:@"lock.rotation.open"] forState:UIControlStateNormal]; 198 | Sender.tintColor = UIColorFromHEX(0x999999); 199 | Sender.tag = 2; 200 | } else { 201 | [Sender setImage:[UIImage systemImageNamed:@"lock.rotation"] forState:UIControlStateNormal]; 202 | Sender.tintColor = UIColor.orangeColor; 203 | Sender.tag = 1; 204 | } 205 | } 206 | 207 | %end 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | %hook AVPlaybackControlsController 217 | - (void)skipButtonTouchUpInside:(id)Sender { 218 | %orig; 219 | 220 | float SeekTime = [[[NSMutableDictionary dictionaryWithContentsOfFile:MainPlist] objectForKey:@"SeekTime"] floatValue]; 221 | if (SeekTime <= 0) 222 | SeekTime = 15; 223 | 224 | AVButton *avbutton = Sender; 225 | isSeekingAllowed = YES; 226 | if ([avbutton.imageName.lowercaseString containsString:@"forward"]) 227 | [self _seekByTimeInterval:SeekTime toleranceBefore:0.5 toleranceAfter:0.5]; 228 | else 229 | [self _seekByTimeInterval:-SeekTime toleranceBefore:0.5 toleranceAfter:0.5]; 230 | } 231 | - (void)_seekByTimeInterval:(double)arg1 toleranceBefore:(double)arg2 toleranceAfter:(double)arg3 { 232 | 233 | if (isSeekingAllowed) { 234 | isSeekingAllowed = NO; 235 | return %orig; 236 | } 237 | return; 238 | } 239 | %end 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | // 255 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.crazymind90.avtools 2 | Name: AVTools 3 | Depends: firmware (>= 13.0),mobilesubstrate 4 | Version: 1.1 5 | Architecture: iphoneos-arm 6 | Description: An awesome MobileSubstrate tweak! 7 | Maintainer: @CrazyMind90 8 | Author: @CrazyMind90 9 | Section: Tweaks 10 | -------------------------------------------------------------------------------- /ent.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | platform-application 5 | 6 | com.apple.private.security.container-required 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /layout/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Signing ..." 4 | ldid -s /var/jb/Library/MobileSubstrate/DynamicLibraries/AVTools.dylib /var/jb/Library/MobileSubstrate/DynamicLibraries/AVTools.dylib 5 | echo "Done!" 6 | exit 0 7 | --------------------------------------------------------------------------------