├── .gitignore ├── Velvet2.plist ├── headers ├── Log.h ├── Velvet2 │ ├── Velvet2Switch.h │ ├── Velvet2Button.h │ ├── Velvet2LinkCell.h │ ├── Velvet2AppSelectController.h │ ├── Velvet2SettingsController.h │ ├── Velvet2Slider.h │ ├── Velvet2ColorPicker.h │ ├── Velvet2CustomizationController.h │ ├── UIColor+Velvet.h │ ├── Velvet2RootListController.h │ ├── Velvet2PrefsManager.h │ ├── Velvet2PreviewController.h │ ├── Velvet2PreviewView.h │ ├── Velvet2Colorizer.h │ ├── Velvet2AppearanceCell.h │ └── ColorDetection.h ├── UIView+Private.h ├── MaterialKit.h ├── UIColor+Private.h ├── UserNotificationsKit.h ├── PlatterKit.h ├── NSTask.h ├── UIImage+Private.h ├── QuartzCore.h ├── Preferences.h ├── UIViewController+Private.h ├── HeadersTweak.h ├── CoreServices.h ├── HeadersPreferences.h └── UserNotificationsUIKit.h ├── preferences ├── Resources │ ├── icon.png │ ├── icon@2x.png │ ├── icon@3x.png │ ├── velvet-header-icon.png │ ├── Info.plist │ ├── Root.plist │ ├── CustomShadow.plist │ ├── CustomDate.plist │ ├── CustomTitle.plist │ ├── CustomMessage.plist │ ├── CustomBorder.plist │ ├── CustomBackground.plist │ ├── CustomLine.plist │ └── Settings.plist ├── layout │ └── Library │ │ └── PreferenceLoader │ │ └── Preferences │ │ └── Velvet2.plist ├── Velvet2CustomizationController.m ├── Makefile ├── Velvet2SettingsController.m ├── CustomCells │ ├── Velvet2LinkCell.m │ ├── Velvet2Button.m │ ├── Velvet2Switch.m │ ├── Velvet2SubtitleLinkCell.m │ ├── Velvet2Slider.m │ ├── Velvet2ColorPicker.m │ └── Velvet2AppearanceCell.m ├── Velvet2RootListController.m ├── Velvet2AppSelectController.m ├── Velvet2PreviewController.m └── Velvet2PreviewView.m ├── control ├── Makefile ├── LICENSE └── src ├── UIColor+Velvet.m ├── Velvet2PrefsManager.m ├── Tweak.x ├── Velvet2Colorizer.m └── ColorDetection.m /.gitignore: -------------------------------------------------------------------------------- 1 | .theos/ 2 | packages/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Velvet2.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.springboard" ); }; } 2 | -------------------------------------------------------------------------------- /headers/Log.h: -------------------------------------------------------------------------------- 1 | #define NSLog(fmt, ...) NSLog((@"[Velvet2] " fmt), ##__VA_ARGS__) -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2Switch.h: -------------------------------------------------------------------------------- 1 | @interface Velvet2Switch : PSSubtitleSwitchTableCell 2 | @end -------------------------------------------------------------------------------- /headers/UIView+Private.h: -------------------------------------------------------------------------------- 1 | @interface UIView (Private) 2 | -(id)_viewControllerForAncestor; 3 | @end -------------------------------------------------------------------------------- /preferences/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/Velvet2/HEAD/preferences/Resources/icon.png -------------------------------------------------------------------------------- /preferences/Resources/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/Velvet2/HEAD/preferences/Resources/icon@2x.png -------------------------------------------------------------------------------- /preferences/Resources/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/Velvet2/HEAD/preferences/Resources/icon@3x.png -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2Button.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Velvet2Button : PSTableCell 4 | @end -------------------------------------------------------------------------------- /headers/MaterialKit.h: -------------------------------------------------------------------------------- 1 | @interface MTMaterialView : UIView 2 | +(id)materialViewWithRecipe:(long long)arg1 configuration:(long long)arg2; 3 | @end -------------------------------------------------------------------------------- /headers/UIColor+Private.h: -------------------------------------------------------------------------------- 1 | @interface UIColor (Private) 2 | @property (nonatomic,readonly) NSString * pkaxApproximateColorDescription; 3 | @end -------------------------------------------------------------------------------- /preferences/Resources/velvet-header-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/Velvet2/HEAD/preferences/Resources/velvet-header-icon.png -------------------------------------------------------------------------------- /headers/UserNotificationsKit.h: -------------------------------------------------------------------------------- 1 | @interface NCNotificationRequest : NSObject 2 | @property (nonatomic,copy,readonly) NSString * sectionIdentifier; 3 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2LinkCell.h: -------------------------------------------------------------------------------- 1 | @interface Velvet2LinkCell : PSTableCell 2 | @end 3 | 4 | @interface Velvet2SubtitleLinkCell : PSTableCell 5 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2AppSelectController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Velvet2AppSelectController : PSListController 4 | @end 5 | -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2SettingsController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Velvet2SettingsController : Velvet2PreviewController 4 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2Slider.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Velvet2Slider : PSSliderTableCell 4 | @property (nonatomic, retain) UILabel *nameLabel; 5 | @end -------------------------------------------------------------------------------- /headers/PlatterKit.h: -------------------------------------------------------------------------------- 1 | @interface PLPlatterView : UIView 2 | @end 3 | 4 | @interface NCNotificationSummaryPlatterView : PLPlatterView 5 | @property (nonatomic,retain) UIView *velvetView; 6 | -(void)velvetUpdateStyle; 7 | @end -------------------------------------------------------------------------------- /headers/NSTask.h: -------------------------------------------------------------------------------- 1 | @interface NSTask : NSObject 2 | @property (copy) NSArray * arguments; 3 | @property (retain) id standardOutput; 4 | - (void)setLaunchPath:(NSString *)path; 5 | - (void)launch; 6 | - (void)waitUntilExit; 7 | @end -------------------------------------------------------------------------------- /headers/UIImage+Private.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIImage (Private) 4 | +(id)_applicationIconImageForBundleIdentifier:(id)identifier format:(int)format scale:(double)scale; 5 | + (UIImage*)kitImageNamed:(NSString*)name; 6 | @end -------------------------------------------------------------------------------- /headers/QuartzCore.h: -------------------------------------------------------------------------------- 1 | @interface CALayer (Private) 2 | @property (assign) BOOL continuousCorners; 3 | @end 4 | 5 | @interface CAFilter : NSObject 6 | @property (copy) NSString * name; 7 | @property (getter=isEnabled) BOOL enabled; 8 | +(id)filterWithName:(id)name; 9 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2ColorPicker.h: -------------------------------------------------------------------------------- 1 | @interface Velvet2ColorPicker : PSTableCell 2 | @property (nonatomic, retain) UIView *cellColorDisplay; 3 | @property (nonatomic, retain) UIColor *selectedColor; 4 | -(void)openColorPicker; 5 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2CustomizationController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "Velvet2PreviewController.h" 3 | 4 | @interface Velvet2CustomizationController : Velvet2PreviewController 5 | @property (nonatomic,copy) NSString * currentKey; 6 | @end -------------------------------------------------------------------------------- /preferences/layout/Library/PreferenceLoader/Preferences/Velvet2.plist: -------------------------------------------------------------------------------- 1 | { 2 | entry = { 3 | bundle = Velvet2; 4 | cell = PSLinkCell; 5 | detail = Velvet2RootListController; 6 | icon = "icon.png"; 7 | isController = 1; 8 | label = "Velvet 2"; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /headers/Preferences.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PSViewController (Private) 4 | -(void)showController:(id)controller; 5 | @end 6 | 7 | @interface PSSegmentableSlider : UISlider 8 | @end 9 | 10 | @interface PSSubtitleSwitchTableCell : PSSwitchTableCell 11 | @end -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.noisyflake.velvet2 2 | Name: Velvet 2 3 | Version: 2.1.2 4 | Architecture: iphoneos-arm 5 | Description: A fully customizable notification experience 6 | Maintainer: NoisyFlake 7 | Author: NoisyFlake 8 | Section: Tweaks 9 | Depends: mobilesubstrate (>= 0.9.5000), preferenceloader 10 | Conflicts: com.initwithframe.velvet 11 | -------------------------------------------------------------------------------- /headers/UIViewController+Private.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIViewController (Private) 4 | @property (assign,setter=_setContentOverlayInsets:,nonatomic) UIEdgeInsets _contentOverlayInsets; 5 | @end 6 | 7 | @interface UINavigationController (Private) 8 | @property (nonatomic,readonly) UIViewController * previousViewController; 9 | @end -------------------------------------------------------------------------------- /headers/Velvet2/UIColor+Velvet.h: -------------------------------------------------------------------------------- 1 | @interface UIColor (Velvet) 2 | + (UIColor *)colorFromGradient:(NSArray*)colors withDirection:(NSString *)direction inFrame:(CGRect)frame flipY:(BOOL)flipY; 3 | + (UIColor *)colorFromGradient:(NSArray *)colors withDirection:(NSString *)direction inFrame:(CGRect)frame; 4 | + (UIColor *)colorFromP3String:(NSString *)string; 5 | @end -------------------------------------------------------------------------------- /preferences/Velvet2CustomizationController.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2CustomizationController 4 | 5 | - (NSArray *)specifiers { 6 | if (!_specifiers) { 7 | _specifiers = [self visibleSpecifiersFromPlist:[NSString stringWithFormat:@"Custom%@", self.currentKey]]; 8 | } 9 | 10 | return _specifiers; 11 | } 12 | 13 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2RootListController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface Velvet2RootListController : PSListController 5 | -(void)setupHeader; 6 | -(void)setupFooterVersion; 7 | -(void)resetSettings; 8 | -(void)twitter; 9 | -(void)paypal; 10 | -(void)setTweakEnabled:(id)value specifier:(PSSpecifier *)specifier; 11 | -(void)respring; 12 | @end 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:latest:15.0 2 | INSTALL_TARGET_PROCESSES = SpringBoard 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | TWEAK_NAME = Velvet2 7 | 8 | Velvet2_FILES = src/Tweak.x src/UIColor+Velvet.m src/Velvet2PrefsManager.m src/ColorDetection.m src/Velvet2Colorizer.m 9 | Velvet2_CFLAGS = -fobjc-arc 10 | 11 | include $(THEOS_MAKE_PATH)/tweak.mk 12 | SUBPROJECTS += preferences 13 | include $(THEOS_MAKE_PATH)/aggregate.mk 14 | -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2PrefsManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Velvet2PrefsManager : NSUserDefaults 4 | + (instancetype)sharedInstance; 5 | - (instancetype)initWithSuiteName:(NSString *)suitename; 6 | - (id)settingForKey:(NSString *)key withIdentifier:(NSString *)identifier; 7 | - (UIColor *)colorForKey:(NSString *)key withIdentifier:(NSString *)identifier; 8 | - (CGFloat)alphaValueForKey:(NSString *)key withIdentifier:(NSString *)identifier; 9 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2PreviewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "Velvet2PreviewView.h" 3 | 4 | @interface Velvet2PreviewController : PSListController 5 | 6 | @property (nonatomic,copy) NSString * identifier; 7 | @property (nonatomic,copy) NSString * identifierName; 8 | @property (nonatomic,retain) Velvet2PreviewView *preview; 9 | 10 | - (NSMutableArray*)visibleSpecifiersFromPlist:(NSString*)plist; 11 | - (BOOL)appSettingForKeyExists:(NSString *)key; 12 | 13 | @end -------------------------------------------------------------------------------- /headers/HeadersTweak.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "Log.h" 4 | #import "MaterialKit.h" 5 | #import "PlatterKit.h" 6 | #import "QuartzCore.h" 7 | #import "UIView+Private.h" 8 | #import "UserNotificationsKit.h" 9 | #import "UserNotificationsUIKit.h" 10 | #import "Velvet2/ColorDetection.h" 11 | #import "Velvet2/UIColor+Velvet.h" 12 | #import "Velvet2/Velvet2Colorizer.h" 13 | #import "Velvet2/Velvet2PrefsManager.h" 14 | 15 | #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) -------------------------------------------------------------------------------- /preferences/Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:14.5:15.0 2 | INSTALL_TARGET_PROCESSES = Preferences 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | BUNDLE_NAME = Velvet2 7 | 8 | Velvet2_FILES = $(wildcard *.m CustomCells/*.m ../src/Velvet2PrefsManager.m ../src/UIColor+Velvet.m ../src/ColorDetection.m ../src/Velvet2Colorizer.m) 9 | Velvet2_FRAMEWORKS = UIKit 10 | Velvet2_PRIVATE_FRAMEWORKS = Preferences 11 | Velvet2_INSTALL_PATH = $(THEOS_PACKAGE_INSTALL_PREFIX)/Library/PreferenceBundles 12 | Velvet2_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' 13 | 14 | include $(THEOS_MAKE_PATH)/bundle.mk 15 | -------------------------------------------------------------------------------- /preferences/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | Velvet2 9 | CFBundleIdentifier 10 | com.noisyflake.velvet2 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1.0 21 | NSPrincipalClass 22 | Velvet2RootListController 23 | 24 | 25 | -------------------------------------------------------------------------------- /headers/CoreServices.h: -------------------------------------------------------------------------------- 1 | @interface _LSQueryResult : NSObject 2 | @end 3 | 4 | @interface LSResourceProxy : _LSQueryResult 5 | @property (setter=_setLocalizedName:,nonatomic,copy) NSString * localizedName; 6 | @end 7 | 8 | @interface LSBundleProxy : LSResourceProxy 9 | @end 10 | 11 | @interface LSApplicationProxy : LSBundleProxy 12 | @property (nonatomic,readonly) NSString * applicationIdentifier; 13 | @property (nonatomic,readonly) NSString * applicationType; 14 | @property (nonatomic,readonly) NSArray * appTags; 15 | @property (getter=isLaunchProhibited,nonatomic,readonly) BOOL launchProhibited; 16 | @property (getter=isPlaceholder,nonatomic,readonly) BOOL placeholder; 17 | @property (getter=isRemovedSystemApp,nonatomic,readonly) BOOL removedSystemApp; 18 | @end 19 | 20 | @interface LSApplicationWorkspace : NSObject 21 | +(id)defaultWorkspace; 22 | -(id)allInstalledApplications; 23 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2PreviewView.h: -------------------------------------------------------------------------------- 1 | @interface Velvet2PreviewView : UIView 2 | @property (nonatomic,copy) NSString * identifier; 3 | @property (nonatomic,retain) UIView *notificationView; 4 | @property (nonatomic,retain) MTMaterialView *materialView; 5 | @property (nonatomic,retain) UIView *velvetView; 6 | @property (nonatomic,retain) UILabel *titleLabel; 7 | @property (nonatomic,retain) UILabel *messageLabel; 8 | @property (nonatomic,retain) UILabel *dateLabel; 9 | @property (nonatomic,retain) UIImageView *appIcon; 10 | @property (nonatomic,copy) NSString * currentIconIdentifier; 11 | @property (nonatomic, assign) BOOL disableAnimations; 12 | -(instancetype)initWithFrame:(CGRect)frame notificationWidth:(CGFloat)width identifier:(NSString *)identifier; 13 | -(void)updatePreview; 14 | -(void)updateAppIconWithIdentifier:(NSString*)identifier; 15 | -(void)handleTouch:(UITapGestureRecognizer *)recognizer; 16 | @end -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2Colorizer.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "Velvet2PrefsManager.h" 3 | 4 | @interface Velvet2Colorizer : NSObject 5 | 6 | @property (nonatomic, retain) NSString *identifier; 7 | @property (nonatomic, retain) Velvet2PrefsManager *manager; 8 | @property (nonatomic, retain) UIImage *appIcon; 9 | @property (nonatomic, retain) UIColor *iconColor; 10 | 11 | - (instancetype)initWithIdentifier:(NSString *)identifier; 12 | - (void)colorBackground:(UIView *)backgroundView; 13 | - (void)setBackgroundBlur:(UIView *)materialView; 14 | - (void)colorBorder:(UIView *)borderView; 15 | - (void)colorShadow:(UIView *)shadowView; 16 | - (void)colorLine:(UIView *)lineView inFrame:(CGRect)frame; 17 | - (void)colorTitle:(UILabel*)title; 18 | - (void)colorMessage:(UILabel*)message; 19 | - (void)colorDate:(UILabel*)date; 20 | - (void)setAppIconCornerRadius:(UIView*)appIcon; 21 | - (void)setAppearance:(UIView*)view; 22 | - (void)toggleAppIconVisibility:(UIView*)appIcon withTitle:(UILabel*)title message:(UILabel*)message footer:(UILabel*)footer alwaysUpdate:(BOOL)alwaysUpdate; 23 | 24 | @end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NoisyFlake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /headers/HeadersPreferences.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | #import "CoreServices.h" 7 | #import "Log.h" 8 | #import "MaterialKit.h" 9 | #import "NSTask.h" 10 | #import "Preferences.h" 11 | #import "QuartzCore.h" 12 | #import "UIColor+Private.h" 13 | #import "UIImage+Private.h" 14 | #import "UIView+Private.h" 15 | #import "UIViewController+Private.h" 16 | #import "Velvet2/ColorDetection.h" 17 | #import "Velvet2/UIColor+Velvet.h" 18 | #import "Velvet2/Velvet2AppearanceCell.h" 19 | #import "Velvet2/Velvet2AppSelectController.h" 20 | #import "Velvet2/Velvet2Button.h" 21 | #import "Velvet2/Velvet2Colorizer.h" 22 | #import "Velvet2/Velvet2ColorPicker.h" 23 | #import "Velvet2/Velvet2CustomizationController.h" 24 | #import "Velvet2/Velvet2LinkCell.h" 25 | #import "Velvet2/Velvet2PrefsManager.h" 26 | #import "Velvet2/Velvet2PreviewController.h" 27 | #import "Velvet2/Velvet2PreviewView.h" 28 | #import "Velvet2/Velvet2RootListController.h" 29 | #import "Velvet2/Velvet2SettingsController.h" 30 | #import "Velvet2/Velvet2Slider.h" 31 | #import "Velvet2/Velvet2Switch.h" 32 | 33 | #define kVelvetColor [UIColor colorWithRed: 0.38 green: 0.76 blue: 1.00 alpha: 1.00] -------------------------------------------------------------------------------- /headers/Velvet2/Velvet2AppearanceCell.h: -------------------------------------------------------------------------------- 1 | @interface Velvet2AppearanceCell : PSTableCell 2 | @property(nonatomic, retain) UIStackView *containerStackView; 3 | @property(nonatomic, retain) NSArray *options; 4 | - (void)updateSelected:(NSString *)type; 5 | @end 6 | 7 | @interface Velvet2AppearanceStackView : UIStackView 8 | @property(nonatomic, retain) Velvet2AppearanceCell *hostController; 9 | @property(nonatomic, retain) PSSpecifier *specifier; 10 | 11 | @property(nonatomic, retain) UIImageView *iconView; 12 | @property(nonatomic, retain) UILabel *captionLabel; 13 | @property(nonatomic, retain) UIButton *checkmarkButton; 14 | 15 | @property(nonatomic, retain) UIImpactFeedbackGenerator *feedbackGenerator; 16 | @property(nonatomic, retain) UILongPressGestureRecognizer *tapGestureRecognizer; 17 | 18 | @property(nonatomic, retain) NSString *type; 19 | @property(nonatomic, retain) NSString *defaultsIdentifier; 20 | @property(nonatomic, retain) NSString *postNotification; 21 | @property(nonatomic, retain) NSString *key; 22 | @property(nonatomic, retain) NSUserDefaults *defaults; 23 | 24 | - (Velvet2AppearanceStackView *)initWithType:(NSString *)type forController:(Velvet2AppearanceCell *)controller withImage:(UIImage *)image andText:(NSString *)text andSpecifier:(PSSpecifier *)specifier; 25 | - (void)buttonTapped:(UILongPressGestureRecognizer *)sender; 26 | @end -------------------------------------------------------------------------------- /headers/UserNotificationsUIKit.h: -------------------------------------------------------------------------------- 1 | @interface NCHitTestTransparentView : UIView 2 | @end 3 | 4 | @interface NCNotificationViewControllerView : NCHitTestTransparentView { 5 | UIView* _stackDimmingView; 6 | } 7 | @end 8 | 9 | @interface NCNotificationViewController : UIViewController { 10 | UIView* _contentSizeManagingView; 11 | } 12 | @property (nonatomic,retain) NCNotificationRequest * notificationRequest; 13 | @end 14 | 15 | @interface NCBadgedIconView : UIView 16 | @property (nonatomic,retain) UIView * iconView; 17 | @end 18 | 19 | @interface NCNotificationSeamlessContentView : UIView { 20 | UILabel* _primaryTextLabel; 21 | UIView* _secondaryTextElement; 22 | UILabel* _dateLabel; 23 | NCBadgedIconView* _badgedIconView; 24 | } 25 | @property (nonatomic,copy) UIImage * prominentIcon; 26 | @property (nonatomic,copy) UIImage * subordinateIcon; 27 | -(void)velvetUpdateStyle; 28 | @end 29 | 30 | @interface NCNotificationShortLookView : UIView { 31 | NCNotificationSeamlessContentView* _notificationContentView; 32 | } 33 | @property (nonatomic,readonly) MTMaterialView * backgroundMaterialView; 34 | @end 35 | 36 | @interface NCNotificationShortLookViewController : NCNotificationViewController 37 | @property (nonatomic,readonly) UIView * viewForPreview; 38 | @property (nonatomic,retain) UIView *velvetView; 39 | -(void)velvetUpdateStyle; 40 | @end 41 | 42 | @interface NCNotificationStructuredListViewController : UIViewController 43 | @end 44 | 45 | @interface NCNotificationSummaryContentView : UIView 46 | @end -------------------------------------------------------------------------------- /preferences/Velvet2SettingsController.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2SettingsController 4 | 5 | - (NSArray *)specifiers { 6 | if (!_specifiers) { 7 | _specifiers = [self visibleSpecifiersFromPlist:@"Settings"]; 8 | 9 | NSMutableArray *mutableSpecifiers = [_specifiers mutableCopy]; 10 | 11 | if ([self.identifier isEqual:@"com.noisyflake.velvetFocus"]) { 12 | for (PSSpecifier *specifier in [mutableSpecifiers reverseObjectEnumerator]) { 13 | NSString *key = specifier.properties[@"key"]; 14 | if ([key isEqual:@"Date"] || [key isEqual:@"appIconCornerRadiusCircle"]) { 15 | specifier.properties[@"enabled"] = @NO; 16 | } 17 | } 18 | 19 | _specifiers = mutableSpecifiers; 20 | } 21 | } 22 | 23 | if (self.identifier && self.identifierName) { 24 | self.title = self.identifierName; 25 | } 26 | 27 | return _specifiers; 28 | } 29 | 30 | -(void)showController:(id)controller { 31 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2CustomizationController")]) { 32 | NSIndexPath *selectedPath = self.table.indexPathForSelectedRow; 33 | PSTableCell *selectedCell = [self.table cellForRowAtIndexPath:selectedPath]; 34 | PSSpecifier *specifier = selectedCell.specifier; 35 | 36 | ((Velvet2CustomizationController *)controller).identifier = self.identifier; 37 | ((Velvet2CustomizationController *)controller).currentKey = specifier.properties[@"key"]; 38 | } 39 | 40 | return [super showController:controller]; 41 | } 42 | 43 | @end -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2LinkCell.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2LinkCell 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | if (specifier.properties[@"systemIcon"]) { 9 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 10 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 11 | [specifier setProperty:image forKey:@"iconImage"]; 12 | 13 | self.imageView.tintColor = kVelvetColor; 14 | } 15 | } 16 | 17 | return self; 18 | } 19 | 20 | -(void)layoutSubviews { 21 | [super layoutSubviews]; 22 | 23 | if (self.specifier.properties[@"systemIcon"]) { 24 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 25 | } 26 | 27 | PSListController *controller = [self _viewControllerForAncestor]; 28 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 29 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 30 | 31 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 32 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 33 | } 34 | } 35 | } 36 | @end -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2Button.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2Button 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | if (specifier.properties[@"systemIcon"]) { 9 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 10 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 11 | [specifier setProperty:image forKey:@"iconImage"]; 12 | 13 | self.imageView.tintColor = kVelvetColor; 14 | } 15 | } 16 | 17 | return self; 18 | } 19 | 20 | -(void)layoutSubviews { 21 | [super layoutSubviews]; 22 | 23 | self.textLabel.textColor = UIColor.labelColor; 24 | self.textLabel.highlightedTextColor = UIColor.labelColor; 25 | 26 | if (self.specifier.properties[@"systemIcon"]) { 27 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 28 | } 29 | 30 | PSListController *controller = [self _viewControllerForAncestor]; 31 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 32 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 33 | 34 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 35 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 36 | } 37 | } 38 | } 39 | @end -------------------------------------------------------------------------------- /preferences/Resources/Root.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSSwitchCell; 5 | cellClass = Velvet2Switch; 6 | cellSubtitleText = "Requires a Respring"; 7 | default = 1; 8 | defaults = "com.noisyflake.velvet2"; 9 | key = enabled; 10 | label = "Enable Tweak"; 11 | systemIcon = "power"; 12 | set = "setTweakEnabled:specifier:"; 13 | }, 14 | { 15 | cell = PSGroupCell; 16 | label = "Settings"; 17 | }, 18 | { 19 | cell = PSLinkCell; 20 | cellClass = Velvet2SubtitleLinkCell; 21 | cellSubtitleText = "Apply to all notifications"; 22 | isController = 1; 23 | detail = Velvet2SettingsController; 24 | label = "Global Settings"; 25 | systemIcon = "gear"; 26 | }, 27 | { 28 | cell = PSLinkCell; 29 | cellClass = Velvet2SubtitleLinkCell; 30 | cellSubtitleText = "Overwrite Global Settings"; 31 | isController = 1; 32 | detail = Velvet2AppSelectController; 33 | label = "Per-App Settings"; 34 | systemIcon = "ellipsis"; 35 | }, 36 | { 37 | cell = PSButtonCell; 38 | cellClass = Velvet2Button; 39 | action = resetSettings; 40 | label = "Reset Settings"; 41 | confirmation = { 42 | title = "Reset Settings"; 43 | prompt = "Reset all settings to their default value.\nThis action cannot be undone."; 44 | cancelTitle = "Cancel"; 45 | }; 46 | isDestructive = 1; 47 | systemIcon = "gobackward"; 48 | }, 49 | { 50 | cell = PSGroupCell; 51 | label = "Developer"; 52 | }, 53 | { 54 | cell = PSButtonCell; 55 | cellClass = Velvet2Button; 56 | action = twitter; 57 | label = "Follow on Twitter"; 58 | systemIcon = "person.crop.circle.badge.plus"; 59 | }, 60 | { 61 | cell = PSButtonCell; 62 | cellClass = Velvet2Button; 63 | action = paypal; 64 | label = "Buy me a coffee"; 65 | systemIcon = "gift"; 66 | }, 67 | 68 | ); 69 | title = ""; 70 | } -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2Switch.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2Switch 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | [((UISwitch *)[self control]) setOnTintColor:kVelvetColor]; 9 | 10 | if (specifier.properties[@"systemIcon"]) { 11 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 12 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 13 | [specifier setProperty:image forKey:@"iconImage"]; 14 | 15 | self.imageView.tintColor = kVelvetColor; 16 | } 17 | } 18 | 19 | return self; 20 | } 21 | 22 | -(void)layoutSubviews { 23 | [super layoutSubviews]; 24 | 25 | if (self.specifier.properties[@"systemIcon"]) { 26 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 27 | self.detailTextLabel.frame = CGRectMake(60, self.detailTextLabel.frame.origin.y, self.detailTextLabel.frame.size.width, self.detailTextLabel.frame.size.height); 28 | } 29 | 30 | PSListController *controller = [self _viewControllerForAncestor]; 31 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 32 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 33 | 34 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 35 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 36 | } 37 | } 38 | 39 | } 40 | @end -------------------------------------------------------------------------------- /preferences/Resources/CustomShadow.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Show a custom glow effect"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = shadowEnabled; 13 | label = "Enable Glow"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSSliderCell; 18 | cellClass = Velvet2Slider; 19 | defaults = "com.noisyflake.velvet2"; 20 | key = shadowWidth; 21 | min = 1; 22 | max = 10; 23 | showValue = 1; 24 | require = "shadowEnabled"; 25 | label = "Size"; 26 | systemIcon = "textformat.size"; 27 | }, 28 | { 29 | cell = PSGroupCell; 30 | label = "Type"; 31 | require = "shadowEnabled"; 32 | }, 33 | { 34 | cell = PSDefaultCell; 35 | cellClass = Velvet2AppearanceCell; 36 | options = ( 37 | { 38 | text = "Icon Color"; 39 | value = "icon"; 40 | systemIcon = "wand.and.stars"; 41 | }, 42 | { 43 | text = "Static Color"; 44 | value = "color"; 45 | systemIcon = "eyedropper"; 46 | } 47 | ); 48 | defaults = "com.noisyflake.velvet2"; 49 | key = "shadowType"; 50 | height = 130; 51 | require = "shadowEnabled"; 52 | }, 53 | { 54 | cell = PSGroupCell; 55 | label = "Options"; 56 | require = "shadowEnabled"; 57 | }, 58 | { 59 | cell = PSSliderCell; 60 | cellClass = Velvet2Slider; 61 | defaults = "com.noisyflake.velvet2"; 62 | key = shadowIconAlpha; 63 | min = 0; 64 | max = 100; 65 | showValue = 1; 66 | require="shadowEnabled&shadowType=icon"; 67 | label="Opacity"; 68 | systemIcon = "eye"; 69 | }, 70 | { 71 | cell = PSLinkCell; 72 | defaults = "com.noisyflake.velvet2"; 73 | cellClass = Velvet2ColorPicker; 74 | key = "shadowColor"; 75 | label = "Color"; 76 | require = "shadowEnabled&shadowType=color"; 77 | systemIcon = "drop.circle"; 78 | } 79 | ); 80 | title = "Glow"; 81 | } -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2SubtitleLinkCell.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2SubtitleLinkCell 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | if (specifier.properties[@"systemIcon"]) { 9 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 10 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 11 | [specifier setProperty:image forKey:@"iconImage"]; 12 | 13 | self.imageView.tintColor = kVelvetColor; 14 | } 15 | 16 | if (specifier.properties[@"cellSubtitleText"]) { 17 | self.detailTextLabel.text = specifier.properties[@"cellSubtitleText"]; 18 | } 19 | } 20 | 21 | return self; 22 | } 23 | 24 | -(void)layoutSubviews { 25 | [super layoutSubviews]; 26 | 27 | if (self.specifier.properties[@"systemIcon"]) { 28 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 29 | self.detailTextLabel.frame = CGRectMake(60, self.detailTextLabel.frame.origin.y, self.detailTextLabel.frame.size.width, self.detailTextLabel.frame.size.height); 30 | } 31 | 32 | PSListController *controller = [self _viewControllerForAncestor]; 33 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 34 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 35 | 36 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 37 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 38 | } 39 | } 40 | } 41 | @end -------------------------------------------------------------------------------- /src/UIColor+Velvet.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersTweak.h" 2 | 3 | @implementation UIColor (Velvet) 4 | + (UIColor *)colorFromGradient:(NSArray*)colors withDirection:(NSString *)direction inFrame:(CGRect)frame flipY:(BOOL)flipY { 5 | CAGradientLayer *gradientLayer = [CAGradientLayer layer]; 6 | gradientLayer.frame = frame; 7 | gradientLayer.colors = colors; 8 | 9 | gradientLayer.startPoint = [direction isEqual:@"cornerTop"] ? CGPointMake(0,1) : CGPointMake(0, 0); 10 | gradientLayer.endPoint = [direction isEqual:@"cornerBottom"] ? CGPointMake(1,1) : [direction isEqual:@"cornerTop"] ? CGPointMake(1,0) : [direction isEqual:@"bottom"] ? CGPointMake(0,1) : CGPointMake(1,0); 11 | 12 | // FlipY is necessary when working directly on a CALayer, since CoreAnimation renders upside-down 13 | if (flipY) { 14 | gradientLayer.startPoint = CGPointMake(gradientLayer.startPoint.x, gradientLayer.startPoint.y == 0 ? 1 : 0); 15 | gradientLayer.endPoint = CGPointMake(gradientLayer.endPoint.x, gradientLayer.endPoint.y == 0 ? 1 : 0); 16 | } 17 | 18 | UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:gradientLayer.frame.size]; 19 | UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull context) { 20 | [gradientLayer renderInContext:context.CGContext]; 21 | }]; 22 | 23 | return [UIColor colorWithPatternImage:image]; 24 | } 25 | 26 | + (UIColor *)colorFromGradient:(NSArray*)colors withDirection:(NSString *)direction inFrame:(CGRect)frame { 27 | return [UIColor colorFromGradient:colors withDirection:direction inFrame:frame flipY:NO]; 28 | } 29 | 30 | + (UIColor *)colorFromP3String:(NSString *)string { 31 | NSArray *components = [string componentsSeparatedByString:@" "]; 32 | if ([components count] != 4) return UIColor.clearColor; 33 | 34 | return [UIColor colorWithDisplayP3Red:[components[0] floatValue] green:[components[1] floatValue] blue:[components[2] floatValue] alpha:[components[3] floatValue]]; 35 | } 36 | @end -------------------------------------------------------------------------------- /preferences/Resources/CustomDate.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Use a custom date color"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = dateEnabled; 13 | label = "Enable Date"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSGroupCell; 18 | label = "Type"; 19 | require = "dateEnabled"; 20 | }, 21 | { 22 | cell = PSDefaultCell; 23 | cellClass = Velvet2AppearanceCell; 24 | options = ( 25 | { 26 | text = "Icon Color"; 27 | value = "icon"; 28 | systemIcon = "wand.and.stars"; 29 | }, 30 | { 31 | text = "Static Color"; 32 | value = "color"; 33 | systemIcon = "eyedropper"; 34 | }, 35 | { 36 | text = "Gradient"; 37 | value = "gradient"; 38 | systemIcon = "humidity.fill"; 39 | }, 40 | ); 41 | defaults = "com.noisyflake.velvet2"; 42 | key = "dateType"; 43 | height = 130; 44 | require = "dateEnabled"; 45 | }, 46 | { 47 | cell = PSGroupCell; 48 | label = "Options"; 49 | require = "dateEnabled"; 50 | }, 51 | { 52 | cell = PSSliderCell; 53 | cellClass = Velvet2Slider; 54 | defaults = "com.noisyflake.velvet2"; 55 | key = dateIconAlpha; 56 | min = 0; 57 | max = 100; 58 | showValue = 1; 59 | require = "dateEnabled&dateType=icon"; 60 | label = "Opacity"; 61 | systemIcon = "eye"; 62 | }, 63 | { 64 | cell = PSLinkCell; 65 | defaults = "com.noisyflake.velvet2"; 66 | cellClass = Velvet2ColorPicker; 67 | key = "dateColor"; 68 | label = "Color"; 69 | require = "dateEnabled&dateType=color"; 70 | systemIcon = "drop.circle"; 71 | }, 72 | { 73 | cell = PSLinkListCell; 74 | cellClass = "Velvet2LinkCell"; 75 | defaults = "com.noisyflake.velvet2"; 76 | key = "dateGradientDirection"; 77 | detail = PSListItemsController; 78 | label = "Gradient Direction"; 79 | validValues = ("cornerTop", "right", "bottom", "cornerBottom"); 80 | validTitles = ("\U2197", "\U2192", "\U2193", "\U2198"); 81 | require = "dateEnabled&dateType=gradient"; 82 | systemIcon = "arrow.left.arrow.right"; 83 | }, 84 | { 85 | cell = PSLinkCell; 86 | defaults = "com.noisyflake.velvet2"; 87 | cellClass = Velvet2ColorPicker; 88 | key = "dateGradient1"; 89 | label = "Color 1"; 90 | require = "dateEnabled&dateType=gradient"; 91 | systemIcon = "drop.circle"; 92 | }, 93 | { 94 | cell = PSLinkCell; 95 | defaults = "com.noisyflake.velvet2"; 96 | cellClass = Velvet2ColorPicker; 97 | key = "dateGradient2"; 98 | label = "Color 2"; 99 | require = "dateEnabled&dateType=gradient"; 100 | systemIcon = "drop.circle"; 101 | }, 102 | ); 103 | title = "Date"; 104 | } -------------------------------------------------------------------------------- /preferences/Resources/CustomTitle.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Use a custom title color"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = titleEnabled; 13 | label = "Enable Title"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSGroupCell; 18 | label = "Type"; 19 | require = "titleEnabled"; 20 | }, 21 | { 22 | cell = PSDefaultCell; 23 | cellClass = Velvet2AppearanceCell; 24 | options = ( 25 | { 26 | text = "Icon Color"; 27 | value = "icon"; 28 | systemIcon = "wand.and.stars"; 29 | }, 30 | { 31 | text = "Static Color"; 32 | value = "color"; 33 | systemIcon = "eyedropper"; 34 | }, 35 | { 36 | text = "Gradient"; 37 | value = "gradient"; 38 | systemIcon = "humidity.fill"; 39 | }, 40 | ); 41 | defaults = "com.noisyflake.velvet2"; 42 | key = "titleType"; 43 | height = 130; 44 | require = "titleEnabled"; 45 | }, 46 | { 47 | cell = PSGroupCell; 48 | label = "Options"; 49 | require = "titleEnabled"; 50 | }, 51 | { 52 | cell = PSSliderCell; 53 | cellClass = Velvet2Slider; 54 | defaults = "com.noisyflake.velvet2"; 55 | key = titleIconAlpha; 56 | min = 0; 57 | max = 100; 58 | showValue = 1; 59 | require = "titleEnabled&titleType=icon"; 60 | label = "Opacity"; 61 | systemIcon = "eye"; 62 | }, 63 | { 64 | cell = PSLinkCell; 65 | defaults = "com.noisyflake.velvet2"; 66 | cellClass = Velvet2ColorPicker; 67 | key = "titleColor"; 68 | label = "Color"; 69 | require = "titleEnabled&titleType=color"; 70 | systemIcon = "drop.circle"; 71 | }, 72 | { 73 | cell = PSLinkListCell; 74 | cellClass = "Velvet2LinkCell"; 75 | defaults = "com.noisyflake.velvet2"; 76 | key = "titleGradientDirection"; 77 | detail = PSListItemsController; 78 | label = "Gradient Direction"; 79 | validValues = ("cornerTop", "right", "bottom", "cornerBottom"); 80 | validTitles = ("\U2197", "\U2192", "\U2193", "\U2198"); 81 | require = "titleEnabled&titleType=gradient"; 82 | systemIcon = "arrow.left.arrow.right"; 83 | }, 84 | { 85 | cell = PSLinkCell; 86 | defaults = "com.noisyflake.velvet2"; 87 | cellClass = Velvet2ColorPicker; 88 | key = "titleGradient1"; 89 | label = "Color 1"; 90 | require = "titleEnabled&titleType=gradient"; 91 | systemIcon = "drop.circle"; 92 | }, 93 | { 94 | cell = PSLinkCell; 95 | defaults = "com.noisyflake.velvet2"; 96 | cellClass = Velvet2ColorPicker; 97 | key = "titleGradient2"; 98 | label = "Color 2"; 99 | require = "titleEnabled&titleType=gradient"; 100 | systemIcon = "drop.circle"; 101 | }, 102 | ); 103 | title = "Title"; 104 | } -------------------------------------------------------------------------------- /preferences/Resources/CustomMessage.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Use a custom message color"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = messageEnabled; 13 | label = "Enable Message"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSGroupCell; 18 | label = "Type"; 19 | require = "messageEnabled"; 20 | }, 21 | { 22 | cell = PSDefaultCell; 23 | cellClass = Velvet2AppearanceCell; 24 | options = ( 25 | { 26 | text = "Icon Color"; 27 | value = "icon"; 28 | systemIcon = "wand.and.stars"; 29 | }, 30 | { 31 | text = "Static Color"; 32 | value = "color"; 33 | systemIcon = "eyedropper"; 34 | }, 35 | { 36 | text = "Gradient"; 37 | value = "gradient"; 38 | systemIcon = "humidity.fill"; 39 | }, 40 | ); 41 | defaults = "com.noisyflake.velvet2"; 42 | key = "messageType"; 43 | height = 130; 44 | require = "messageEnabled"; 45 | }, 46 | { 47 | cell = PSGroupCell; 48 | label = "Options"; 49 | require = "messageEnabled"; 50 | }, 51 | { 52 | cell = PSSliderCell; 53 | cellClass = Velvet2Slider; 54 | defaults = "com.noisyflake.velvet2"; 55 | key = messageIconAlpha; 56 | min = 0; 57 | max = 100; 58 | showValue = 1; 59 | require = "messageEnabled&messageType=icon"; 60 | label = "Opacity"; 61 | systemIcon = "eye"; 62 | }, 63 | { 64 | cell = PSLinkCell; 65 | defaults = "com.noisyflake.velvet2"; 66 | cellClass = Velvet2ColorPicker; 67 | key = "messageColor"; 68 | label = "Color"; 69 | require = "messageEnabled&messageType=color"; 70 | systemIcon = "drop.circle"; 71 | }, 72 | { 73 | cell = PSLinkListCell; 74 | cellClass = "Velvet2LinkCell"; 75 | defaults = "com.noisyflake.velvet2"; 76 | key = "messageGradientDirection"; 77 | detail = PSListItemsController; 78 | label = "Gradient Direction"; 79 | validValues = ("cornerTop", "right", "bottom", "cornerBottom"); 80 | validTitles = ("\U2197", "\U2192", "\U2193", "\U2198"); 81 | require = "messageEnabled&messageType=gradient"; 82 | systemIcon = "arrow.left.arrow.right"; 83 | }, 84 | { 85 | cell = PSLinkCell; 86 | defaults = "com.noisyflake.velvet2"; 87 | cellClass = Velvet2ColorPicker; 88 | key = "messageGradient1"; 89 | label = "Color 1"; 90 | require = "messageEnabled&messageType=gradient"; 91 | systemIcon = "drop.circle"; 92 | }, 93 | { 94 | cell = PSLinkCell; 95 | defaults = "com.noisyflake.velvet2"; 96 | cellClass = Velvet2ColorPicker; 97 | key = "messageGradient2"; 98 | label = "Color 2"; 99 | require = "messageEnabled&messageType=gradient"; 100 | systemIcon = "drop.circle"; 101 | }, 102 | ); 103 | title = "Message"; 104 | } -------------------------------------------------------------------------------- /preferences/Resources/CustomBorder.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Show a custom border"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = borderEnabled; 13 | label = "Enable Border"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSSliderCell; 18 | cellClass = Velvet2Slider; 19 | defaults = "com.noisyflake.velvet2"; 20 | key = borderWidth; 21 | min = 1; 22 | max = 5; 23 | showValue = 1; 24 | require = "borderEnabled"; 25 | label = "Size"; 26 | systemIcon = "textformat.size"; 27 | }, 28 | { 29 | cell = PSGroupCell; 30 | label = "Type"; 31 | require = "borderEnabled"; 32 | }, 33 | { 34 | cell = PSDefaultCell; 35 | cellClass = Velvet2AppearanceCell; 36 | options = ( 37 | { 38 | text = "Icon Color"; 39 | value = "icon"; 40 | systemIcon = "wand.and.stars"; 41 | }, 42 | { 43 | text = "Static Color"; 44 | value = "color"; 45 | systemIcon = "eyedropper"; 46 | }, 47 | { 48 | text = "Gradient"; 49 | value = "gradient"; 50 | systemIcon = "humidity.fill"; 51 | }, 52 | ); 53 | defaults = "com.noisyflake.velvet2"; 54 | key = "borderType"; 55 | height = 130; 56 | require = "borderEnabled"; 57 | }, 58 | { 59 | cell = PSGroupCell; 60 | label = "Options"; 61 | require = "borderEnabled"; 62 | }, 63 | { 64 | cell = PSSliderCell; 65 | cellClass = Velvet2Slider; 66 | defaults = "com.noisyflake.velvet2"; 67 | key = borderIconAlpha; 68 | min = 0; 69 | max = 100; 70 | showValue = 1; 71 | require = "borderEnabled&borderType=icon"; 72 | label = "Opacity"; 73 | systemIcon = "eye"; 74 | }, 75 | { 76 | cell = PSLinkCell; 77 | defaults = "com.noisyflake.velvet2"; 78 | cellClass = Velvet2ColorPicker; 79 | key = "borderColor"; 80 | label = "Color"; 81 | require = "borderEnabled&borderType=color"; 82 | systemIcon = "drop.circle"; 83 | }, 84 | { 85 | cell = PSLinkListCell; 86 | cellClass = "Velvet2LinkCell"; 87 | defaults = "com.noisyflake.velvet2"; 88 | key = "borderGradientDirection"; 89 | detail = PSListItemsController; 90 | label = "Gradient Direction"; 91 | validValues = ("cornerTop", "right", "bottom", "cornerBottom"); 92 | validTitles = ("\U2197", "\U2192", "\U2193", "\U2198"); 93 | require = "borderEnabled&borderType=gradient"; 94 | systemIcon = "arrow.left.arrow.right"; 95 | }, 96 | { 97 | cell = PSLinkCell; 98 | defaults = "com.noisyflake.velvet2"; 99 | cellClass = Velvet2ColorPicker; 100 | key = "borderGradient1"; 101 | label = "Color 1"; 102 | require = "borderEnabled&borderType=gradient"; 103 | systemIcon = "drop.circle"; 104 | }, 105 | { 106 | cell = PSLinkCell; 107 | defaults = "com.noisyflake.velvet2"; 108 | cellClass = Velvet2ColorPicker; 109 | key = "borderGradient2"; 110 | label = "Color 2"; 111 | require = "borderEnabled&borderType=gradient"; 112 | systemIcon = "drop.circle"; 113 | }, 114 | ); 115 | title = "Border"; 116 | } -------------------------------------------------------------------------------- /src/Velvet2PrefsManager.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersTweak.h" 2 | 3 | @implementation Velvet2PrefsManager 4 | 5 | static void sendUpdateNotification() { 6 | // Send the notification to our hooks 7 | [[NSNotificationCenter defaultCenter] postNotificationName:@"com.noisyflake.velvet2/updateStyle" object:nil]; 8 | } 9 | 10 | + (instancetype)sharedInstance { 11 | static Velvet2PrefsManager *sharedInstance = nil; 12 | static dispatch_once_t onceToken; 13 | dispatch_once(&onceToken, ^{ 14 | sharedInstance = [[Velvet2PrefsManager alloc] initWithSuiteName:@"com.noisyflake.velvet2"]; 15 | }); 16 | return sharedInstance; 17 | } 18 | 19 | - (instancetype)initWithSuiteName:(NSString *)suitename { 20 | Velvet2PrefsManager *prefs = [super initWithSuiteName:suitename]; 21 | 22 | [prefs registerDefaults:@{ 23 | @"enabled": @YES, 24 | @"appearance": @"default", 25 | @"backgroundEnabled": @NO, 26 | @"backgroundBlurHidden" : @NO, 27 | @"backgroundType": @"icon", 28 | @"backgroundIconAlpha": @50, 29 | @"backgroundGradientDirection": @"right", 30 | @"borderEnabled": @NO, 31 | @"borderWidth": @2, 32 | @"borderType": @"icon", 33 | @"borderIconAlpha": @100, 34 | @"borderGradientDirection": @"right", 35 | @"shadowEnabled": @NO, 36 | @"shadowWidth": @5, 37 | @"shadowType": @"icon", 38 | @"shadowIconAlpha": @100, 39 | @"lineEnabled": @NO, 40 | @"linePosition": @"left", 41 | @"lineWidth": @3, 42 | @"lineType": @"icon", 43 | @"lineIconAlpha": @100, 44 | @"lineGradientDirection": @"right", 45 | @"titleEnabled": @NO, 46 | @"titleType": @"icon", 47 | @"titleIconAlpha": @100, 48 | @"titleGradientDirection": @"right", 49 | @"messageEnabled": @NO, 50 | @"messageType": @"icon", 51 | @"messageIconAlpha": @100, 52 | @"messageGradientDirection": @"right", 53 | @"dateEnabled": @NO, 54 | @"dateType": @"icon", 55 | @"dateIconAlpha": @100, 56 | @"dateGradientDirection": @"right", 57 | @"cornerRadiusEnabled": @NO, 58 | @"cornerRadiusCustom": @19, 59 | @"appIconHidden": @NO, 60 | @"appIconCornerRadiusCircle": @NO, 61 | @"stackDimmingViewHidden": @NO 62 | }]; 63 | 64 | CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)sendUpdateNotification, CFSTR("com.noisyflake.velvet2/preferenceUpdate"), NULL, CFNotificationSuspensionBehaviorCoalesce); 65 | 66 | return prefs; 67 | } 68 | 69 | - (id)settingForKey:(NSString *)key withIdentifier:(NSString *)identifier { 70 | if (identifier) { 71 | id result = [self objectForKey:[NSString stringWithFormat:@"%@_%@", key, identifier]]; 72 | if (result) return result; 73 | } 74 | 75 | return [self objectForKey:key]; 76 | } 77 | 78 | - (UIColor *)colorForKey:(NSString *)key withIdentifier:(NSString *)identifier { 79 | NSString *colorString = [self settingForKey:key withIdentifier:identifier]; 80 | return [UIColor colorFromP3String:colorString]; 81 | } 82 | 83 | - (CGFloat)alphaValueForKey:(NSString *)key withIdentifier:(NSString *)identifier { 84 | NSString *string = [self settingForKey:key withIdentifier:identifier]; 85 | return [string floatValue] / 100; 86 | } 87 | @end -------------------------------------------------------------------------------- /preferences/Resources/CustomBackground.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Use a custom background color"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = backgroundEnabled; 13 | label = "Enable Background"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSGroupCell; 18 | label = "Type"; 19 | require = "backgroundEnabled"; 20 | }, 21 | { 22 | cell = PSDefaultCell; 23 | cellClass = Velvet2AppearanceCell; 24 | options = ( 25 | { 26 | text = "Icon Color"; 27 | value = "icon"; 28 | systemIcon = "wand.and.stars"; 29 | }, 30 | { 31 | text = "Static Color"; 32 | value = "color"; 33 | systemIcon = "eyedropper"; 34 | }, 35 | { 36 | text = "Gradient"; 37 | value = "gradient"; 38 | systemIcon = "humidity.fill"; 39 | }, 40 | ); 41 | defaults = "com.noisyflake.velvet2"; 42 | key = "backgroundType"; 43 | height = 130; 44 | require = "backgroundEnabled"; 45 | }, 46 | { 47 | cell = PSGroupCell; 48 | label = "Options"; 49 | require = "backgroundEnabled"; 50 | }, 51 | { 52 | cell = PSSliderCell; 53 | cellClass = Velvet2Slider; 54 | defaults = "com.noisyflake.velvet2"; 55 | key = backgroundIconAlpha; 56 | min = 0; 57 | max = 100; 58 | showValue = 1; 59 | require = "backgroundEnabled&backgroundType=icon"; 60 | label = "Opacity"; 61 | systemIcon = "eye"; 62 | }, 63 | { 64 | cell = PSLinkCell; 65 | defaults = "com.noisyflake.velvet2"; 66 | cellClass = Velvet2ColorPicker; 67 | key = "backgroundColor"; 68 | label = "Color"; 69 | require = "backgroundEnabled&backgroundType=color"; 70 | systemIcon = "drop.circle"; 71 | }, 72 | { 73 | cell = PSLinkListCell; 74 | cellClass = "Velvet2LinkCell"; 75 | defaults = "com.noisyflake.velvet2"; 76 | key = "backgroundGradientDirection"; 77 | detail = PSListItemsController; 78 | label = "Gradient Direction"; 79 | validValues = ("cornerTop", "right", "bottom", "cornerBottom"); 80 | validTitles = ("\U2197", "\U2192", "\U2193", "\U2198"); 81 | require = "backgroundEnabled&backgroundType=gradient"; 82 | systemIcon = "arrow.left.arrow.right"; 83 | }, 84 | { 85 | cell = PSLinkCell; 86 | defaults = "com.noisyflake.velvet2"; 87 | cellClass = Velvet2ColorPicker; 88 | key = "backgroundGradient1"; 89 | label = "Color 1"; 90 | require = "backgroundEnabled&backgroundType=gradient"; 91 | systemIcon = "drop.circle"; 92 | }, 93 | { 94 | cell = PSLinkCell; 95 | defaults = "com.noisyflake.velvet2"; 96 | cellClass = Velvet2ColorPicker; 97 | key = "backgroundGradient2"; 98 | label = "Color 2"; 99 | require = "backgroundEnabled&backgroundType=gradient"; 100 | systemIcon = "drop.circle"; 101 | }, 102 | { 103 | cell = PSGroupCell; 104 | label = "Misc."; 105 | footerText = "This option disables the Glow feature"; 106 | }, 107 | { 108 | cell = PSSwitchCell; 109 | cellClass = Velvet2Switch; 110 | cellSubtitleText = "Removes the background blur"; 111 | default = 1; 112 | defaults = "com.noisyflake.velvet2"; 113 | key = backgroundBlurHidden; 114 | label = "Hide Background Blur"; 115 | systemIcon = "square.dashed"; 116 | }, 117 | ); 118 | title = "Background"; 119 | } -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2Slider.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2Slider 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | [((PSSegmentableSlider *)[self control]) setMinimumTrackTintColor:kVelvetColor]; 9 | 10 | if (specifier.properties[@"label"]) { 11 | self.nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(specifier.properties[@"systemIcon"] ? 60 : 15, 11, 0, 0)]; 12 | self.nameLabel.text = specifier.properties[@"label"]; 13 | [self.nameLabel sizeToFit]; 14 | [self.contentView insertSubview:self.nameLabel atIndex:0]; 15 | } 16 | 17 | if (specifier.properties[@"systemIcon"]) { 18 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 19 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 20 | [specifier setProperty:image forKey:@"iconImage"]; 21 | 22 | self.imageView.tintColor = kVelvetColor; 23 | } 24 | 25 | // iOS 16 fix 26 | for (NSLayoutConstraint *c in self.control.constraints) { 27 | [self.control removeConstraint:c]; 28 | } 29 | 30 | [self.control layoutIfNeeded]; 31 | } 32 | 33 | return self; 34 | } 35 | 36 | - (void)layoutSubviews { 37 | [super layoutSubviews]; 38 | 39 | PSListController *controller = [self _viewControllerForAncestor]; 40 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 41 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 42 | 43 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 44 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 45 | } 46 | } 47 | 48 | // Before iOS 16, we could simply set the frame of the control. However, iOS 16 introduces constraints that don't seem to be easily fixable, so instead we 49 | // modify the subview (visualElement), since it isn't affected by these constraints 50 | 51 | UISlider *slider = (UISlider *)self.control; 52 | UIView *visualElement = slider.subviews[0]; 53 | 54 | if (self.nameLabel) { 55 | self.nameLabel.frame = CGRectMake(self.specifier.properties[@"systemIcon"] ? 60 : 15, self.nameLabel.frame.origin.y, self.nameLabel.frame.size.width, self.nameLabel.frame.size.height); 56 | [visualElement setFrame:CGRectMake(0 + self.nameLabel.frame.size.width + (self.specifier.properties[@"systemIcon"] ? 60 : 10), visualElement.frame.origin.y, slider.frame.size.width - self.nameLabel.frame.size.width - (self.specifier.properties[@"systemIcon"] ? 60 : 10), visualElement.frame.size.height)]; 57 | } 58 | 59 | if (![self.specifier.properties[@"showValue"] boolValue]) return; 60 | 61 | [visualElement setFrame:CGRectMake(visualElement.frame.origin.x, visualElement.frame.origin.y, visualElement.frame.size.width - 7, visualElement.frame.size.height)]; 62 | 63 | for (UIView *subview in visualElement.subviews) { 64 | if ([subview isKindOfClass:NSClassFromString(@"UILabel")]) { 65 | UILabel *label = (UILabel *)subview; 66 | label.frame = CGRectMake(label.frame.origin.x, label.frame.origin.y, label.frame.size.width + 15, label.frame.size.height); 67 | } 68 | } 69 | 70 | } 71 | @end -------------------------------------------------------------------------------- /preferences/Resources/CustomLine.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | }, 6 | { 7 | cell = PSSwitchCell; 8 | cellClass = Velvet2Switch; 9 | cellSubtitleText = "Show a custom line at the edge"; 10 | default = 1; 11 | defaults = "com.noisyflake.velvet2"; 12 | key = lineEnabled; 13 | label = "Enable Line"; 14 | systemIcon = "power"; 15 | }, 16 | { 17 | cell = PSLinkListCell; 18 | cellClass = "Velvet2LinkCell"; 19 | defaults = "com.noisyflake.velvet2"; 20 | key = "linePosition"; 21 | detail = PSListItemsController; 22 | label = "Position"; 23 | validValues = ("top", "right", "bottom", "left", "topBottom", "leftRight"); 24 | validTitles = ("Top", "Right", "Bottom", "Left", "Top & Bottom", "Left & Right"); 25 | require = "lineEnabled"; 26 | systemIcon = "mappin.and.ellipse"; 27 | }, 28 | { 29 | cell = PSSliderCell; 30 | cellClass = Velvet2Slider; 31 | defaults = "com.noisyflake.velvet2"; 32 | key = lineWidth; 33 | min = 1; 34 | max = 5; 35 | showValue = 1; 36 | require = "lineEnabled"; 37 | label = "Size"; 38 | systemIcon = "textformat.size"; 39 | }, 40 | { 41 | cell = PSGroupCell; 42 | label = "Type"; 43 | require = "lineEnabled"; 44 | }, 45 | { 46 | cell = PSDefaultCell; 47 | cellClass = Velvet2AppearanceCell; 48 | options = ( 49 | { 50 | text = "Icon Color"; 51 | value = "icon"; 52 | systemIcon = "wand.and.stars"; 53 | }, 54 | { 55 | text = "Static Color"; 56 | value = "color"; 57 | systemIcon = "eyedropper"; 58 | }, 59 | { 60 | text = "Gradient"; 61 | value = "gradient"; 62 | systemIcon = "humidity.fill"; 63 | }, 64 | ); 65 | defaults = "com.noisyflake.velvet2"; 66 | key = "lineType"; 67 | height = 130; 68 | require = "lineEnabled"; 69 | }, 70 | { 71 | cell = PSGroupCell; 72 | label = "Options"; 73 | require = "lineEnabled"; 74 | }, 75 | { 76 | cell = PSSliderCell; 77 | cellClass = Velvet2Slider; 78 | defaults = "com.noisyflake.velvet2"; 79 | key = lineIconAlpha; 80 | min = 0; 81 | max = 100; 82 | showValue = 1; 83 | require = "lineEnabled&lineType=icon"; 84 | label = "Opacity"; 85 | systemIcon = "eye"; 86 | }, 87 | { 88 | cell = PSLinkCell; 89 | defaults = "com.noisyflake.velvet2"; 90 | cellClass = Velvet2ColorPicker; 91 | key = "lineColor"; 92 | label = "Color"; 93 | require = "lineEnabled&lineType=color"; 94 | systemIcon = "drop.circle"; 95 | }, 96 | { 97 | cell = PSLinkListCell; 98 | cellClass = "Velvet2LinkCell"; 99 | defaults = "com.noisyflake.velvet2"; 100 | key = "lineGradientDirection"; 101 | detail = PSListItemsController; 102 | label = "Gradient Direction"; 103 | validValues = ("cornerTop", "right", "bottom", "cornerBottom"); 104 | validTitles = ("\U2197", "\U2192", "\U2193", "\U2198"); 105 | require = "lineEnabled&lineType=gradient"; 106 | systemIcon = "arrow.left.arrow.right"; 107 | }, 108 | { 109 | cell = PSLinkCell; 110 | defaults = "com.noisyflake.velvet2"; 111 | cellClass = Velvet2ColorPicker; 112 | key = "lineGradient1"; 113 | label = "Color 1"; 114 | require = "lineEnabled&lineType=gradient"; 115 | systemIcon = "drop.circle"; 116 | }, 117 | { 118 | cell = PSLinkCell; 119 | defaults = "com.noisyflake.velvet2"; 120 | cellClass = Velvet2ColorPicker; 121 | key = "lineGradient2"; 122 | label = "Color 2"; 123 | require = "lineEnabled&lineType=gradient"; 124 | systemIcon = "drop.circle"; 125 | }, 126 | ); 127 | title = "Line"; 128 | } -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2ColorPicker.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2ColorPicker 4 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)identifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier specifier:specifier]; 6 | 7 | [specifier setTarget:self]; 8 | [specifier setButtonAction:@selector(openColorPicker)]; 9 | 10 | self.cellColorDisplay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 31, 31)]; 11 | self.cellColorDisplay.layer.cornerRadius = self.cellColorDisplay.frame.size.height / 2; 12 | self.cellColorDisplay.layer.borderWidth = 1; 13 | self.cellColorDisplay.layer.borderColor = UIColor.systemGrayColor.CGColor; 14 | [self setAccessoryView:self.cellColorDisplay]; 15 | 16 | if (specifier.properties[@"systemIcon"]) { 17 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 18 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 19 | [specifier setProperty:image forKey:@"iconImage"]; 20 | 21 | self.imageView.tintColor = kVelvetColor; 22 | } 23 | 24 | return self; 25 | } 26 | 27 | -(void)layoutSubviews { 28 | [super layoutSubviews]; 29 | 30 | if (self.specifier.properties[@"systemIcon"]) { 31 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 32 | self.detailTextLabel.frame = CGRectMake(60, self.detailTextLabel.frame.origin.y, self.detailTextLabel.frame.size.width, self.detailTextLabel.frame.size.height); 33 | } 34 | 35 | PSListController *controller = [self _viewControllerForAncestor]; 36 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 37 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 38 | 39 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 40 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 41 | } 42 | } 43 | } 44 | 45 | -(void)didMoveToSuperview { 46 | [super didMoveToSuperview]; 47 | 48 | PSViewController *psController = (PSViewController*)[self _viewControllerForAncestor]; 49 | NSString *colorString = [psController readPreferenceValue:self.specifier]; 50 | if (colorString) { 51 | self.selectedColor = [UIColor colorFromP3String:colorString]; 52 | // self.detailTextLabel.text = self.selectedColor.pkaxApproximateColorDescription.capitalizedString; 53 | } 54 | 55 | self.cellColorDisplay.backgroundColor = self.selectedColor; 56 | } 57 | 58 | -(void)openColorPicker { 59 | PSViewController *psController = (PSViewController*)[self _viewControllerForAncestor]; 60 | 61 | UIColorPickerViewController *colorPicker = [UIColorPickerViewController new]; 62 | colorPicker.delegate = self; 63 | colorPicker.supportsAlpha = YES; 64 | colorPicker.selectedColor = self.selectedColor; 65 | [psController presentViewController:colorPicker animated:YES completion:nil]; 66 | } 67 | 68 | - (void)colorPickerViewController:(UIColorPickerViewController *)viewController didSelectColor:(UIColor *)color continuously:(BOOL)continuously { 69 | if (!continuously) { 70 | self.selectedColor = color; 71 | } 72 | } 73 | 74 | - (void)colorPickerViewControllerDidFinish:(UIColorPickerViewController *)viewController { 75 | self.cellColorDisplay.backgroundColor = self.selectedColor; 76 | 77 | PSViewController *psController = (PSViewController*)[self _viewControllerForAncestor]; 78 | NSString *colorString = [CIColor colorWithCGColor:self.selectedColor.CGColor].stringRepresentation; 79 | [psController setPreferenceValue:colorString specifier:self.specifier]; 80 | } 81 | 82 | @end -------------------------------------------------------------------------------- /preferences/Velvet2RootListController.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2RootListController 4 | 5 | - (NSArray *)specifiers { 6 | if (!_specifiers) { 7 | _specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self]; 8 | } 9 | 10 | return _specifiers; 11 | } 12 | 13 | -(void)viewDidLayoutSubviews { 14 | [super viewDidLayoutSubviews]; 15 | 16 | [self setupHeader]; 17 | [self setupFooterVersion]; 18 | } 19 | 20 | -(void)setupHeader { 21 | UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 140)]; 22 | 23 | UIImage *image = [UIImage imageNamed:@"velvet-header-icon.png" inBundle:[NSBundle bundleForClass:NSClassFromString(@"Velvet2RootListController")] compatibleWithTraitCollection:nil]; 24 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 30 - 4, self.view.bounds.size.width, 80)]; 25 | imageView.contentMode = UIViewContentModeScaleAspectFit; 26 | [imageView setImage:image]; 27 | 28 | [header addSubview:imageView]; 29 | self.table.tableHeaderView = header; 30 | } 31 | 32 | -(void)setupFooterVersion { 33 | NSString *firstLine = [NSString stringWithFormat:@"Velvet %@", PACKAGE_VERSION]; 34 | 35 | NSMutableAttributedString *fullFooter = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\nwith \u2665 by NoisyFlake", firstLine]]; 36 | 37 | [fullFooter beginEditing]; 38 | [fullFooter addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:18] range:NSMakeRange(0, [firstLine length])]; 39 | [fullFooter endEditing]; 40 | 41 | UILabel *footerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 100)]; 42 | footerLabel.font = [UIFont systemFontOfSize:13]; 43 | footerLabel.textColor = UIColor.systemGrayColor; 44 | footerLabel.numberOfLines = 2; 45 | footerLabel.attributedText = fullFooter; 46 | footerLabel.textAlignment = NSTextAlignmentCenter; 47 | self.table.tableFooterView = footerLabel; 48 | } 49 | 50 | -(void)resetSettings { 51 | [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:@"com.noisyflake.velvet2"]; 52 | [self reload]; 53 | 54 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)@"com.noisyflake.velvet2/preferenceUpdate", NULL, NULL, YES); 55 | } 56 | 57 | -(void)twitter { 58 | NSURL *tweetbot = [NSURL URLWithString:@"tweetbot://NoisyFlake/user_profile/NoisyFlake"]; 59 | NSURL *twitterrific = [NSURL URLWithString:@"twitterrific://profile?screen_name=NoisyFlake"]; 60 | NSURL *twitter = [NSURL URLWithString:@"twitter://user?screen_name=NoisyFlake"]; 61 | NSURL *web = [NSURL URLWithString:@"http://www.twitter.com/NoisyFlake"]; 62 | 63 | if ([[UIApplication sharedApplication] canOpenURL:tweetbot]) { 64 | [[UIApplication sharedApplication] openURL:tweetbot options:@{} completionHandler:nil]; 65 | } else if ([[UIApplication sharedApplication] canOpenURL:twitterrific]) { 66 | [[UIApplication sharedApplication] openURL:twitterrific options:@{} completionHandler:nil]; 67 | } else if ([[UIApplication sharedApplication] canOpenURL:twitter]) { 68 | [[UIApplication sharedApplication] openURL:twitter options:@{} completionHandler:nil]; 69 | } else { 70 | [[UIApplication sharedApplication] openURL:web options:@{} completionHandler:nil]; 71 | } 72 | } 73 | 74 | -(void)paypal { 75 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://www.paypal.me/NoisyFlake"] options:@{} completionHandler:nil]; 76 | } 77 | 78 | -(void)setTweakEnabled:(id)value specifier:(PSSpecifier *)specifier { 79 | [self setPreferenceValue:value specifier:specifier]; 80 | 81 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Respring" style:UIBarButtonItemStylePlain target:self action:@selector(respring)]; 82 | } 83 | 84 | -(void)respring { 85 | pid_t pid; 86 | const char* args[] = {"sbreload", NULL}; 87 | posix_spawn(&pid, ROOT_PATH("/usr/bin/sbreload"), NULL, NULL, (char* const*)args, NULL); 88 | } 89 | @end 90 | -------------------------------------------------------------------------------- /preferences/Velvet2AppSelectController.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2AppSelectController 4 | 5 | - (NSArray *)specifiers { 6 | if (!_specifiers) { 7 | NSMutableArray *mutableSpecifiers = [NSMutableArray new]; 8 | 9 | LSApplicationWorkspace *workspace = [NSClassFromString(@"LSApplicationWorkspace") defaultWorkspace]; 10 | NSArray *apps = [workspace allInstalledApplications]; 11 | 12 | for (LSApplicationProxy *app in apps) { 13 | if ([app.applicationType isEqual:@"User"] || ([app.applicationType isEqual:@"System"] && ![app.appTags containsObject:@"hidden"] && !app.launchProhibited && !app.placeholder && !app.removedSystemApp)) { 14 | 15 | if ([app.applicationIdentifier isEqual:@"com.apple.webapp"]) continue; 16 | 17 | PSSpecifier* specifier = [PSSpecifier preferenceSpecifierNamed:app.localizedName 18 | target:self 19 | set:@selector(setPreferenceValue:specifier:) 20 | get:@selector(readPreferenceValue:) 21 | detail:Nil 22 | cell:PSLinkCell 23 | edit:Nil]; 24 | 25 | [specifier setProperty:app.applicationIdentifier forKey:@"key"]; 26 | [specifier setProperty:app.localizedName forKey:@"label"]; 27 | [specifier setProperty:@"com.noisyflake.velvet2" forKey:@"defaults"]; 28 | [specifier setProperty:@"1" forKey:@"isController"]; 29 | specifier.detailControllerClass = NSClassFromString(@"Velvet2SettingsController"); 30 | 31 | [mutableSpecifiers addObject:specifier]; 32 | 33 | UIImage *icon = [UIImage _applicationIconImageForBundleIdentifier:app.applicationIdentifier format:0 scale:UIScreen.mainScreen.scale]; 34 | 35 | CGImageRef cgIcon = icon.CGImage; 36 | CGFloat scale = (CGImageGetWidth(cgIcon) + CGImageGetHeight(cgIcon)) / (CGFloat)(29 + 29); 37 | UIImage *iconResized = [UIImage imageWithCGImage:cgIcon scale:scale orientation:0]; 38 | 39 | if (iconResized) { 40 | [specifier setProperty:iconResized forKey:@"iconImage"]; 41 | } 42 | 43 | } 44 | } 45 | 46 | // Add Focus Specifier 47 | PSSpecifier* specifier = [PSSpecifier preferenceSpecifierNamed:@"Focus" 48 | target:self 49 | set:@selector(setPreferenceValue:specifier:) 50 | get:@selector(readPreferenceValue:) 51 | detail:Nil 52 | cell:PSLinkCell 53 | edit:Nil]; 54 | 55 | [specifier setProperty:@"com.noisyflake.velvetFocus" forKey:@"key"]; 56 | [specifier setProperty:@"Focus" forKey:@"label"]; 57 | [specifier setProperty:@"com.noisyflake.velvet2" forKey:@"defaults"]; 58 | [specifier setProperty:@"1" forKey:@"isController"]; 59 | specifier.detailControllerClass = NSClassFromString(@"Velvet2SettingsController"); 60 | 61 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:24]]; 62 | UIImage *icon = [[UIImage systemImageNamed:@"moon.fill" withConfiguration:config] imageWithTintColor:[UIColor colorWithRed: 0.34 green: 0.34 blue: 0.81 alpha: 1.00] renderingMode:UIImageRenderingModeAlwaysOriginal]; 63 | [specifier setProperty:icon forKey:@"iconImage"]; 64 | 65 | [mutableSpecifiers addObject:specifier]; 66 | 67 | 68 | // Sort specifiers alphabetically 69 | [mutableSpecifiers sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)]]]; 70 | 71 | _specifiers = mutableSpecifiers; 72 | } 73 | 74 | return _specifiers; 75 | } 76 | 77 | -(void)showController:(id)controller { 78 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2SettingsController")]) { 79 | NSIndexPath *selectedPath = self.table.indexPathForSelectedRow; 80 | PSTableCell *selectedCell = [self.table cellForRowAtIndexPath:selectedPath]; 81 | PSSpecifier *specifier = selectedCell.specifier; 82 | 83 | ((Velvet2SettingsController *)controller).identifier = specifier.properties[@"key"]; 84 | ((Velvet2SettingsController *)controller).identifierName = specifier.name; 85 | } 86 | 87 | return [super showController:controller]; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /headers/Velvet2/ColorDetection.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | // Local maxima as found during the image analysis. We need this class for ordering by cell hit count. 5 | @interface CCLocalMaximum : NSObject 6 | 7 | // Hit count of the cell 8 | @property (assign, nonatomic) unsigned int hitCount; 9 | 10 | // Linear index of the cell 11 | @property (assign, nonatomic) unsigned int cellIndex; 12 | 13 | // Average color of cell 14 | @property (assign, nonatomic) double red; 15 | @property (assign, nonatomic) double green; 16 | @property (assign, nonatomic) double blue; 17 | 18 | // Maximum color component value of average color 19 | @property (assign, nonatomic) double brightness; 20 | 21 | @end 22 | 23 | // Flags that determine how the colors are extract 24 | typedef enum CCFlags: NSUInteger 25 | { 26 | // This ignores all pixels that are darker than a threshold 27 | CCOnlyBrightColors = 1 << 0, 28 | 29 | // This ignores all pixels that are brighter than a threshold 30 | CCOnlyDarkColors = 1 << 1, 31 | 32 | // This filters the result array so that only distinct colors are returned 33 | CCOnlyDistinctColors = 1 << 2, 34 | 35 | // This orders the result array by color brightness (first color has highest brightness). If not set, 36 | // colors are ordered by frequency (first color is "most frequent"). 37 | CCOrderByBrightness = 1 << 3, 38 | 39 | // This orders the result array by color darkness (first color has lowest brightness). If not set, 40 | // colors are ordered by frequency (first color is "most frequent"). 41 | CCOrderByDarkness = 1 << 4, 42 | 43 | // Removes colors from the result if they are too close to white 44 | CCAvoidWhite = 1 << 5, 45 | 46 | // Removes colors from the result if they are too close to black 47 | CCAvoidBlack = 1 << 6 48 | 49 | } CCFlags; 50 | 51 | // The color cube is made out of these cells 52 | typedef struct CCCubeCell { 53 | 54 | // Count of hits (dividing the accumulators by this value gives the average) 55 | unsigned int hitCount; 56 | 57 | // Accumulators for color components 58 | double redAcc; 59 | double greenAcc; 60 | double blueAcc; 61 | 62 | } CCCubeCell; 63 | 64 | // This class implements a simple method to extract the most dominant colors of an image. 65 | // How it does it: It projects all pixels of the image into a three dimensional grid (the "cube"), 66 | // then finds local maximas in that grid and returns the average colors of these "maximum cells" 67 | // ordered by their hit count (kind of "color frequency"). 68 | // You should call these methods on a background thread. Depending on the image size they can take 69 | // some time. 70 | @interface CCColorCube : NSObject 71 | 72 | // Extracts and returns dominant colors of the image (the array contains UIColor objects). Result might be empty. 73 | - (NSArray * _Nullable )extractColorsFromImage:(UIImage * _Nonnull)image flags:(CCFlags)flags; 74 | 75 | // Same as above but avoids colors too close to the specified one. 76 | // IMPORTANT: The avoidColor must be in RGB, so create it with colorWithRed method of UIColor! 77 | - (NSArray * _Nullable )extractColorsFromImage:(UIImage * _Nonnull)image flags:(CCFlags)flags avoidColor:(UIColor * _Nonnull)avoidColor; 78 | 79 | // Tries to get count bright colors from the image, avoiding the specified one (only if avoidColor is non-nil). 80 | // IMPORTANT: The avoidColor (if set) must be in RGB, so create it with colorWithRed method of UIColor! 81 | // Might return less than count colors! 82 | - (NSArray * _Nullable )extractBrightColorsFromImage:(UIImage * _Nonnull)image avoidColor:(UIColor * _Nonnull)avoidColor count:(NSUInteger)count; 83 | 84 | // Tries to get count dark colors from the image, avoiding the specified one (only if avoidColor is non-nil). 85 | // IMPORTANT: The avoidColor (if set) must be in RGB, so create it with colorWithRed method of UIColor! 86 | // Might return less than count colors! 87 | - (NSArray * _Nullable )extractDarkColorsFromImage:(UIImage * _Nonnull)image avoidColor:(UIColor * _Nonnull)avoidColor count:(NSUInteger)count; 88 | 89 | // Tries to get count colors from the image 90 | // Might return less than count colors! 91 | - (NSArray * _Nullable )extractColorsFromImage:(UIImage * _Nonnull)image flags:(CCFlags)flags count:(NSUInteger)count; 92 | 93 | @end -------------------------------------------------------------------------------- /preferences/Resources/Settings.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSGroupCell; 5 | label = "Appearance"; 6 | }, 7 | { 8 | cell = PSDefaultCell; 9 | cellClass = Velvet2AppearanceCell; 10 | options = ( 11 | { 12 | text = "System"; 13 | value = "default"; 14 | systemIcon = "circle.righthalf.filled"; 15 | }, 16 | { 17 | text = "Light"; 18 | value = "light"; 19 | systemIcon = "sun.max"; 20 | }, 21 | { 22 | text = "Dark"; 23 | value = "dark"; 24 | systemIcon = "moon.stars"; 25 | } 26 | ); 27 | defaults = "com.noisyflake.velvet2"; 28 | key = "appearance"; 29 | height = 120; 30 | }, 31 | { 32 | cell = PSGroupCell; 33 | label = "Surface"; 34 | }, 35 | { 36 | cell = PSLinkCell; 37 | cellClass = Velvet2LinkCell; 38 | isController = 1; 39 | detail = Velvet2CustomizationController; 40 | key = "Background"; 41 | label = "Background"; 42 | systemIcon = "rectangle.fill"; 43 | }, 44 | { 45 | cell = PSLinkCell; 46 | cellClass = Velvet2LinkCell; 47 | isController = 1; 48 | detail = Velvet2CustomizationController; 49 | key = "Border"; 50 | label = "Border"; 51 | systemIcon = "rectangle"; 52 | }, 53 | { 54 | cell = PSLinkCell; 55 | cellClass = Velvet2LinkCell; 56 | isController = 1; 57 | detail = Velvet2CustomizationController; 58 | key = "Line"; 59 | label = "Line"; 60 | systemIcon = "line.horizontal.star.fill.line.horizontal"; 61 | }, 62 | { 63 | cell = PSLinkCell; 64 | cellClass = Velvet2LinkCell; 65 | isController = 1; 66 | detail = Velvet2CustomizationController; 67 | key = "Shadow"; 68 | label = "Glow"; 69 | systemIcon = "sparkles"; 70 | }, 71 | { 72 | cell = PSGroupCell; 73 | label = "Label"; 74 | }, 75 | { 76 | cell = PSLinkCell; 77 | cellClass = Velvet2LinkCell; 78 | isController = 1; 79 | detail = Velvet2CustomizationController; 80 | key = "Title"; 81 | label = "Title"; 82 | systemIcon = "character.bubble"; 83 | }, 84 | { 85 | cell = PSLinkCell; 86 | cellClass = Velvet2LinkCell; 87 | isController = 1; 88 | detail = Velvet2CustomizationController; 89 | key = "Message"; 90 | label = "Message"; 91 | systemIcon = "text.bubble"; 92 | }, 93 | { 94 | cell = PSLinkCell; 95 | cellClass = Velvet2LinkCell; 96 | isController = 1; 97 | detail = Velvet2CustomizationController; 98 | key = "Date"; 99 | label = "Date"; 100 | systemIcon = "calendar"; 101 | }, 102 | { 103 | cell = PSGroupCell; 104 | label = "App Icon"; 105 | }, 106 | { 107 | cell = PSSwitchCell; 108 | cellClass = Velvet2Switch; 109 | cellSubtitleText = "Don't display any icon"; 110 | default = 0; 111 | defaults = "com.noisyflake.velvet2"; 112 | key = appIconHidden; 113 | label = "Hide App Icon"; 114 | systemIcon = "square.slash"; 115 | }, 116 | { 117 | cell = PSSwitchCell; 118 | cellClass = Velvet2Switch; 119 | cellSubtitleText = "Display app icon as a circle"; 120 | default = 0; 121 | defaults = "com.noisyflake.velvet2"; 122 | key = appIconCornerRadiusCircle; 123 | label = "Round App Icon"; 124 | systemIcon = "circle.square"; 125 | require = "!appIconHidden"; 126 | }, 127 | { 128 | cell = PSGroupCell; 129 | label = "Extras"; 130 | }, 131 | { 132 | cell = PSSwitchCell; 133 | cellClass = Velvet2Switch; 134 | cellSubtitleText = "Custom background corner radius"; 135 | default = 1; 136 | defaults = "com.noisyflake.velvet2"; 137 | key = cornerRadiusEnabled; 138 | label = "Corner Radius"; 139 | systemIcon = "rectangle.roundedtop"; 140 | }, 141 | { 142 | cell = PSSliderCell; 143 | cellClass = Velvet2Slider; 144 | defaults = "com.noisyflake.velvet2"; 145 | key = cornerRadiusCustom; 146 | min = 0; 147 | max = 60; 148 | showValue = 1; 149 | require="cornerRadiusEnabled"; 150 | }, 151 | { 152 | cell = PSGroupCell; 153 | require = "cornerRadiusEnabled"; 154 | }, 155 | { 156 | cell = PSSwitchCell; 157 | cellClass = Velvet2Switch; 158 | cellSubtitleText = "Don't dim notifications in a stack"; 159 | default = 0; 160 | defaults = "com.noisyflake.velvet2"; 161 | key = stackDimmingViewHidden; 162 | label = "No Stack Dimming"; 163 | systemIcon = "rectangle.stack"; 164 | } 165 | ); 166 | title = "Global Settings"; 167 | } -------------------------------------------------------------------------------- /preferences/Velvet2PreviewController.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersPreferences.h" 2 | @interface UINavigationItem (Velvet) 3 | @property (assign,nonatomic) UINavigationBar * navigationBar; 4 | @end 5 | 6 | @implementation Velvet2PreviewController 7 | 8 | - (NSMutableArray*)visibleSpecifiersFromPlist:(NSString*)plist { 9 | Velvet2PrefsManager *manager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 10 | 11 | NSMutableArray *mutableSpecifiers = [[self loadSpecifiersFromPlistName:plist target:self] mutableCopy]; 12 | 13 | for (PSSpecifier *specifier in [mutableSpecifiers reverseObjectEnumerator]) { 14 | NSString *requirement = specifier.properties[@"require"]; 15 | 16 | if (requirement) { 17 | NSArray *requirementList = [requirement componentsSeparatedByString:@"&"]; 18 | for (NSString *singleRequirement in requirementList) { 19 | if ([singleRequirement containsString:@"="]) { 20 | NSArray *kv = [singleRequirement componentsSeparatedByString:@"="]; 21 | if (![[manager settingForKey:kv[0] withIdentifier:self.identifier] isEqual:kv[1]]) { 22 | [mutableSpecifiers removeObject:specifier]; 23 | break; 24 | } 25 | } else { 26 | NSString *firstChar = [singleRequirement substringToIndex:1]; 27 | 28 | if ([firstChar isEqualToString:@"!"]) { 29 | if ([[manager settingForKey:[singleRequirement substringFromIndex:1] withIdentifier:self.identifier] boolValue]) { 30 | [mutableSpecifiers removeObject:specifier]; 31 | break; 32 | } 33 | } else { 34 | if (![[manager settingForKey:singleRequirement withIdentifier:self.identifier] boolValue]) { 35 | [mutableSpecifiers removeObject:specifier]; 36 | break; 37 | } 38 | } 39 | 40 | } 41 | } 42 | } 43 | } 44 | 45 | return mutableSpecifiers; 46 | } 47 | 48 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier*)specifier { 49 | BOOL skipSaving = NO; 50 | if (self.identifier) { 51 | NSString *fullKey = [NSString stringWithFormat:@"%@_%@", specifier.properties[@"key"], self.identifier]; 52 | 53 | Velvet2PrefsManager *manager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 54 | NSString *globalValue = [manager settingForKey:specifier.properties[@"key"] withIdentifier:nil]; 55 | 56 | if ([globalValue isEqual:value]) { 57 | skipSaving = YES; 58 | [manager removeObjectForKey:fullKey]; 59 | } 60 | 61 | specifier.properties[@"key"] = fullKey; 62 | } 63 | 64 | 65 | if (!skipSaving) { 66 | [super setPreferenceValue:value specifier:specifier]; 67 | } 68 | 69 | [self.preview updatePreview]; 70 | 71 | // This wouldn't usually be necessary, but I don't want to write the update string into every Specifier in the plist 72 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)@"com.noisyflake.velvet2/preferenceUpdate", NULL, NULL, YES); 73 | 74 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){ 75 | [self reloadSpecifiers]; 76 | }); 77 | } 78 | 79 | -(id)readPreferenceValue:(PSSpecifier*)specifier { 80 | Velvet2PrefsManager *manager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 81 | return [manager settingForKey:specifier.properties[@"key"] withIdentifier:self.identifier]; 82 | } 83 | 84 | -(void)viewDidLayoutSubviews { 85 | [super viewDidLayoutSubviews]; 86 | 87 | if ([self.view.subviews count] > 1) return; 88 | 89 | CGFloat notificationWidth = self.table.subviews[0].frame.size.width; 90 | 91 | CGRect previewFrame = CGRectMake(0, self._contentOverlayInsets.top, self.view.frame.size.width, 93 + 30 + 8); 92 | self.preview = [[Velvet2PreviewView alloc] initWithFrame:previewFrame notificationWidth:notificationWidth identifier:self.identifier]; 93 | 94 | if ([self.navigationController.previousViewController isKindOfClass:NSClassFromString(@"Velvet2SettingsController")]) { 95 | // Use the same icon that our parent displayed 96 | [self.preview updateAppIconWithIdentifier:((Velvet2SettingsController *)self.navigationController.previousViewController).preview.currentIconIdentifier]; 97 | } 98 | 99 | [self.view insertSubview:self.preview atIndex:1]; 100 | 101 | // Need additional space if we don't have a group label first 102 | CGFloat additionalHeight = [self specifierAtIndex:0].name ? 0 : 32; 103 | 104 | self.table.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, notificationWidth, 93 + additionalHeight + 16 + 12)]; 105 | } 106 | 107 | -(BOOL)appSettingForKeyExists:(NSString *)key { 108 | Velvet2PrefsManager *manager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 109 | return [manager objectForKey:[NSString stringWithFormat:@"%@_%@", key, self.identifier]] != nil; 110 | } 111 | @end -------------------------------------------------------------------------------- /preferences/CustomCells/Velvet2AppearanceCell.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2AppearanceStackView 4 | 5 | - (Velvet2AppearanceStackView *)initWithType:(NSString *)type forController:(Velvet2AppearanceCell *)controller withImage:(UIImage *)image andText:(NSString *)text andSpecifier:(PSSpecifier *)specifier { 6 | self = [super init]; 7 | if (self) { 8 | self.type = type; 9 | self.hostController = controller; 10 | 11 | self.key = specifier.properties[@"key"]; 12 | self.postNotification = specifier.properties[@"PostNotification"]; 13 | self.specifier = specifier; 14 | 15 | self.feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; 16 | [self.feedbackGenerator prepare]; 17 | 18 | self.axis = UILayoutConstraintAxisVertical; 19 | self.alignment = UIStackViewAlignmentCenter; 20 | self.distribution = UIStackViewDistributionEqualSpacing; 21 | self.spacing = 8; 22 | self.translatesAutoresizingMaskIntoConstraints = false; 23 | 24 | self.iconView = [[UIImageView alloc] init]; 25 | self.iconView.clipsToBounds = YES; 26 | self.iconView.contentMode = UIViewContentModeScaleAspectFit; 27 | self.iconView.translatesAutoresizingMaskIntoConstraints = false; 28 | self.iconView.image = image; 29 | self.iconView.tintColor = self.checkmarkButton.selected ? kVelvetColor : UIColor.systemGrayColor; 30 | 31 | [self addArrangedSubview:self.iconView]; 32 | [self.iconView.widthAnchor constraintEqualToConstant:60].active = true; 33 | 34 | self.captionLabel = [[UILabel alloc] init]; 35 | self.captionLabel.text = text; 36 | self.captionLabel.textColor = UIColor.labelColor; 37 | [self.captionLabel setFont:[UIFont systemFontOfSize:17.0f]]; 38 | [self.captionLabel.heightAnchor constraintEqualToConstant:20].active = true; 39 | 40 | [self addArrangedSubview:self.captionLabel]; 41 | 42 | self.checkmarkButton = [UIButton buttonWithType:UIButtonTypeCustom]; 43 | self.checkmarkButton.translatesAutoresizingMaskIntoConstraints = false; 44 | 45 | [self.checkmarkButton.heightAnchor constraintEqualToConstant:22].active = true; 46 | [self.checkmarkButton.widthAnchor constraintEqualToConstant:22].active = true; 47 | 48 | [self.checkmarkButton setImage:[[UIImage kitImageNamed:@"UIRemoveControlMultiNotCheckedImage.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal]; 49 | [self.checkmarkButton setImage:[[UIImage kitImageNamed:@"UITintedCircularButtonCheckmark.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected]; 50 | [self.checkmarkButton addTarget:self action:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside]; 51 | [self addArrangedSubview:self.checkmarkButton]; 52 | 53 | self.tapGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(buttonTapped:)]; 54 | self.tapGestureRecognizer.minimumPressDuration = 0; 55 | [self setUserInteractionEnabled:true]; 56 | [self addGestureRecognizer:self.tapGestureRecognizer]; 57 | } 58 | 59 | return self; 60 | } 61 | 62 | - (void)buttonTapped:(UILongPressGestureRecognizer *)sender { 63 | if(sender.state == UIGestureRecognizerStateBegan) { 64 | [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 65 | self.alpha = 0.5; 66 | } completion:^(BOOL finished) {}]; 67 | } else if (sender.state == UIGestureRecognizerStateEnded) { 68 | [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 69 | self.alpha = 1; 70 | [self.hostController updateSelected:self.type]; 71 | } completion:^(BOOL finished) {}]; 72 | 73 | [self.feedbackGenerator impactOccurred]; 74 | 75 | PSViewController *psController = (PSViewController*)[self _viewControllerForAncestor]; 76 | [psController setPreferenceValue:self.type specifier:self.specifier]; 77 | } 78 | } 79 | 80 | @end 81 | 82 | @implementation Velvet2AppearanceCell 83 | 84 | - (Velvet2AppearanceCell *)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 85 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 86 | 87 | if (self) { 88 | self.options = specifier.properties[@"options"]; 89 | 90 | self.containerStackView = [[UIStackView alloc] init]; 91 | self.containerStackView.axis = UILayoutConstraintAxisHorizontal; 92 | self.containerStackView.alignment = UIStackViewAlignmentCenter; 93 | self.containerStackView.distribution = UIStackViewDistributionEqualSpacing; 94 | self.containerStackView.spacing = 25; 95 | self.containerStackView.translatesAutoresizingMaskIntoConstraints = false; 96 | 97 | for (NSDictionary *option in self.options) { 98 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:35]]; 99 | UIImage *image = [UIImage systemImageNamed:option[@"systemIcon"] withConfiguration:config]; 100 | 101 | Velvet2AppearanceStackView *stackView = [[Velvet2AppearanceStackView alloc] initWithType:option[@"value"] 102 | forController:self 103 | withImage:image 104 | andText:option[@"text"] 105 | andSpecifier:specifier]; 106 | [self.containerStackView addArrangedSubview:stackView]; 107 | [stackView.topAnchor constraintEqualToAnchor:self.containerStackView.topAnchor constant:16].active = true; 108 | [stackView.bottomAnchor constraintEqualToAnchor:self.containerStackView.bottomAnchor constant:-16].active = true; 109 | } 110 | 111 | [self.contentView addSubview:self.containerStackView]; 112 | 113 | [self.containerStackView.heightAnchor constraintEqualToAnchor:self.heightAnchor].active = true; 114 | [self.containerStackView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = true; 115 | [self.containerStackView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = true; 116 | } 117 | 118 | return self; 119 | } 120 | 121 | - (void)updateSelected:(NSString *)value { 122 | for (Velvet2AppearanceStackView *subview in self.containerStackView.arrangedSubviews) { 123 | subview.checkmarkButton.selected = [subview.type isEqual:value]; 124 | subview.checkmarkButton.tintColor = subview.checkmarkButton.selected ? kVelvetColor : UIColor.systemGrayColor; 125 | subview.iconView.tintColor = subview.checkmarkButton.selected ? kVelvetColor : UIColor.systemGrayColor; 126 | } 127 | } 128 | 129 | -(void)didMoveToSuperview { 130 | [super didMoveToSuperview]; 131 | 132 | // Hide value label 133 | self.detailTextLabel.text = nil; 134 | 135 | PSViewController *psController = (PSViewController*)[self _viewControllerForAncestor]; 136 | NSString *value = [psController readPreferenceValue:self.specifier]; 137 | 138 | [self updateSelected:value]; 139 | 140 | PSListController *controller = [self _viewControllerForAncestor]; 141 | if ([controller isKindOfClass:NSClassFromString(@"Velvet2PreviewController")]) { 142 | Velvet2PreviewController *previewController = (Velvet2PreviewController *)controller; 143 | 144 | if (previewController.identifier && [previewController appSettingForKeyExists:self.specifier.properties[@"key"]]) { 145 | self.backgroundColor = [kVelvetColor colorWithAlphaComponent:0.3]; 146 | } 147 | } 148 | } 149 | 150 | @end -------------------------------------------------------------------------------- /preferences/Velvet2PreviewView.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersPreferences.h" 2 | 3 | @implementation Velvet2PreviewView 4 | 5 | -(instancetype)initWithFrame:(CGRect)frame notificationWidth:(CGFloat)width identifier:(NSString *)identifier { 6 | self = [super initWithFrame:frame]; 7 | 8 | self.identifier = identifier; 9 | self.backgroundColor = UIColor.systemBackgroundColor; 10 | 11 | // Prevent animations from playing while loading 12 | self.disableAnimations = YES; 13 | 14 | UIView *notificationView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, 75.3)]; 15 | notificationView.center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2 - 6 - 12); 16 | [self insertSubview:notificationView atIndex:0]; 17 | self.notificationView = notificationView; 18 | 19 | MTMaterialView *materialView = [NSClassFromString(@"MTMaterialView") materialViewWithRecipe:1 configuration:0]; 20 | materialView.frame = CGRectMake(0, 0, width, 75.3); 21 | [notificationView insertSubview:materialView atIndex:0]; 22 | self.materialView = materialView; 23 | 24 | UIView *velvetView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, 75.3)]; 25 | [velvetView.layer insertSublayer:[CALayer layer] atIndex:0]; 26 | [velvetView.layer insertSublayer:[CALayer layer] atIndex:0]; 27 | velvetView.clipsToBounds = YES; 28 | [notificationView insertSubview:velvetView atIndex:1]; 29 | self.velvetView = velvetView; 30 | 31 | UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(materialView.frame.origin.x + 58, materialView.frame.origin.y + 10.3, 203, 18)]; 32 | title.text = @"Notification Title"; 33 | title.font = [UIFont boldSystemFontOfSize:15]; 34 | [notificationView insertSubview:title atIndex:2]; 35 | self.titleLabel = title; 36 | 37 | UILabel *message = [[UILabel alloc] initWithFrame:CGRectMake(materialView.frame.origin.x + 58, materialView.frame.origin.y + 28.6, materialView.frame.size.width - 15 - (materialView.frame.origin.x + 58), 36)]; 38 | message.text = @"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy est."; 39 | message.font = [message.font fontWithSize:15]; 40 | message.numberOfLines = 2; 41 | message.lineBreakMode = NSLineBreakByWordWrapping; 42 | [notificationView insertSubview:message atIndex:2]; 43 | self.messageLabel = message; 44 | 45 | UILabel *date = [[UILabel alloc] initWithFrame:CGRectMake(materialView.frame.origin.x + materialView.frame.size.width - 15 - 34.3, materialView.frame.origin.y + 11.3, 34.3, 16)]; 46 | date.text = @"now"; 47 | date.font = [date.font fontWithSize:13]; 48 | date.textAlignment = NSTextAlignmentRight; 49 | date.textColor = [UIColor.labelColor colorWithAlphaComponent:0.5]; 50 | [notificationView insertSubview:date atIndex:2]; 51 | self.dateLabel = date; 52 | 53 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(materialView.frame.origin.x + 10, materialView.frame.origin.y + 18.6, 38, 38)]; 54 | imageView.contentMode = UIViewContentModeScaleAspectFit; 55 | [notificationView insertSubview:imageView atIndex:2]; 56 | self.appIcon = imageView; 57 | 58 | [self updateAppIconWithIdentifier:self.identifier]; 59 | 60 | if (!self.identifier) { 61 | UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTouch:)]; 62 | [notificationView addGestureRecognizer:singleFingerTap]; 63 | } 64 | 65 | UILabel *hint = [[UILabel alloc] initWithFrame:CGRectMake(self.frame.origin.x, self.frame.size.height - 48, self.frame.size.width, 48)]; 66 | hint.text = self.identifier ? @"Settings that overwrite the global settings\nare highlighted with a blue background" : @"Tap notification to change the icon"; 67 | hint.numberOfLines = self.identifier ? 2 : 1; 68 | hint.font = [hint.font fontWithSize:13]; 69 | hint.textColor = self.identifier ? kVelvetColor : UIColor.systemGrayColor; 70 | hint.textAlignment = NSTextAlignmentCenter; 71 | [self insertSubview:hint atIndex:1]; 72 | 73 | return self; 74 | } 75 | 76 | -(void)updatePreview { 77 | NSString *identifier = self.identifier; 78 | Velvet2PrefsManager *prefsManager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 79 | 80 | Velvet2Colorizer *colorizer = [[Velvet2Colorizer alloc] initWithIdentifier:identifier]; 81 | colorizer.appIcon = self.appIcon.image; 82 | 83 | [UIView animateWithDuration:self.disableAnimations ? 0 : 0.5 animations:^{ 84 | CGFloat cornerRadius = [[prefsManager settingForKey:@"cornerRadiusEnabled" withIdentifier:identifier] boolValue] ? [[prefsManager settingForKey:@"cornerRadiusCustom" withIdentifier:identifier] floatValue] : 19; 85 | self.materialView.layer.continuousCorners = cornerRadius < self.materialView.frame.size.height / 2; 86 | self.materialView.layer.cornerRadius = MIN(cornerRadius, self.materialView.frame.size.height / 2); 87 | self.velvetView.layer.continuousCorners = cornerRadius < self.velvetView.frame.size.height / 2; 88 | self.velvetView.layer.cornerRadius = MIN(cornerRadius, self.velvetView.frame.size.height / 2); 89 | 90 | [colorizer setAppIconCornerRadius:self.appIcon]; 91 | [colorizer setBackgroundBlur:self.materialView]; 92 | [colorizer colorBackground:self.velvetView]; 93 | [colorizer colorBorder:self.velvetView]; 94 | [colorizer colorShadow:self.materialView]; 95 | [colorizer colorLine:self.velvetView inFrame:self.materialView.frame]; 96 | [colorizer colorTitle:self.titleLabel]; 97 | [colorizer colorMessage:self.messageLabel]; 98 | [colorizer colorDate:self.dateLabel]; 99 | [colorizer setAppearance:self.notificationView]; 100 | [colorizer toggleAppIconVisibility:self.appIcon withTitle:self.titleLabel message:self.messageLabel footer:nil alwaysUpdate:NO]; 101 | }]; 102 | } 103 | 104 | -(void)updateAppIconWithIdentifier:(NSString*)identifier { 105 | if (!identifier) { 106 | LSApplicationWorkspace *workspace = [NSClassFromString(@"LSApplicationWorkspace") defaultWorkspace]; 107 | NSArray *apps = [workspace allInstalledApplications]; 108 | 109 | while(!identifier) { 110 | uint32_t rnd = arc4random_uniform([apps count]); 111 | LSApplicationProxy *app = [apps objectAtIndex:rnd]; 112 | 113 | if ([app.applicationType isEqual:@"User"] || ([app.applicationType isEqual:@"System"] && ![app.appTags containsObject:@"hidden"] && !app.launchProhibited && !app.placeholder && !app.removedSystemApp)) { 114 | identifier = app.applicationIdentifier; 115 | } 116 | } 117 | } 118 | 119 | // Save the current identifier so we can use it in the sub- or parent controller 120 | Velvet2PrefsManager *manager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 121 | [manager setObject:identifier forKey:@"currentIcon"]; 122 | self.currentIconIdentifier = identifier; 123 | 124 | self.appIcon.image = [UIImage _applicationIconImageForBundleIdentifier:identifier format:2 scale:UIScreen.mainScreen.scale]; 125 | [self updatePreview]; 126 | } 127 | 128 | -(void)handleTouch:(UITapGestureRecognizer *)recognizer { 129 | AudioServicesPlaySystemSound(1519); 130 | 131 | [UIView animateWithDuration:0.05 animations:^{ 132 | self.notificationView.transform = CGAffineTransformMakeScale(1.05,1.05); 133 | } completion:^(BOOL finished) { 134 | [UIView animateWithDuration:0.05 animations:^{ 135 | self.notificationView.transform = CGAffineTransformMakeScale(1.0,1.0); 136 | }]; 137 | }]; 138 | 139 | [self updateAppIconWithIdentifier:nil]; 140 | } 141 | 142 | - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { 143 | [super traitCollectionDidChange:previousTraitCollection]; 144 | 145 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){ 146 | [self updatePreview]; 147 | }); 148 | } 149 | 150 | - (void)didMoveToWindow { 151 | [super didMoveToWindow]; 152 | 153 | Velvet2PrefsManager *manager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 154 | NSString *currentIcon = [manager settingForKey:@"currentIcon" withIdentifier:nil]; 155 | if (currentIcon) { 156 | // This causes the icon to update correctly in the SettingsController when returning from a sub-controller 157 | [self updateAppIconWithIdentifier:currentIcon]; 158 | } 159 | 160 | self.disableAnimations = NO; 161 | } 162 | 163 | @end -------------------------------------------------------------------------------- /src/Tweak.x: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersTweak.h" 2 | 3 | Velvet2PrefsManager *prefsManager; 4 | 5 | %hook NCNotificationShortLookViewController 6 | %property (nonatomic,retain) UIView *velvetView; 7 | 8 | -(void)viewDidLoad { 9 | %orig; 10 | 11 | NCNotificationShortLookView *view = (NCNotificationShortLookView *)self.viewForPreview; 12 | 13 | UIView *velvetView = [UIView new]; 14 | [velvetView.layer insertSublayer:[CALayer layer] atIndex:0]; 15 | [velvetView.layer insertSublayer:[CALayer layer] atIndex:0]; 16 | [view.backgroundMaterialView.superview insertSubview:velvetView atIndex:1]; 17 | 18 | self.velvetView = velvetView; 19 | self.velvetView.clipsToBounds = YES; 20 | 21 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(velvetUpdateStyle) name:@"com.noisyflake.velvet2/updateStyle" object:nil]; 22 | } 23 | 24 | -(void)viewDidLayoutSubviews { 25 | %orig; 26 | 27 | if (self.viewForPreview.frame.size.width == 0) return; 28 | 29 | [self velvetUpdateStyle]; 30 | } 31 | 32 | -(void)viewDidAppear:(BOOL)arg1 { 33 | %orig; 34 | 35 | Velvet2Colorizer *colorizer = [[Velvet2Colorizer alloc] initWithIdentifier:self.notificationRequest.sectionIdentifier]; 36 | NCNotificationShortLookView *view = (NCNotificationShortLookView *)self.viewForPreview; 37 | NCNotificationSeamlessContentView *contentView = [view valueForKey:@"notificationContentView"]; 38 | UIImage *appIcon = contentView.prominentIcon ?: contentView.subordinateIcon; 39 | NCBadgedIconView *badgedIconView = [contentView valueForKey:@"badgedIconView"]; 40 | UIView *appIconView = badgedIconView.iconView; 41 | 42 | colorizer.appIcon = appIcon; 43 | 44 | // For some reason, dateLabel and appIconView isn't fully initialized yet after viewDidLayoutSubviews 45 | [colorizer colorDate:[contentView valueForKey:@"dateLabel"]]; 46 | [colorizer setAppIconCornerRadius:appIconView]; 47 | } 48 | 49 | %new 50 | -(void)velvetUpdateStyle { 51 | NSString *identifier = self.notificationRequest.sectionIdentifier; 52 | 53 | // Initialize content variables 54 | NCNotificationShortLookView *view = (NCNotificationShortLookView *)self.viewForPreview; 55 | MTMaterialView *materialView = view.backgroundMaterialView; 56 | NCNotificationViewControllerView *controllerView = [self valueForKey:@"contentSizeManagingView"]; 57 | UIView *stackDimmingView = SYSTEM_VERSION_LESS_THAN(@"16.0") ? [controllerView valueForKey:@"stackDimmingView"] : [view valueForKey:@"stackDimmingOverlayView"]; 58 | NCNotificationSeamlessContentView *contentView = [view valueForKey:@"notificationContentView"]; 59 | UILabel *title = [contentView valueForKey:@"primaryTextLabel"]; 60 | UILabel *message = [contentView valueForKey:@"secondaryTextElement"]; 61 | UILabel *dateLabel = [contentView valueForKey:@"dateLabel"]; 62 | NCBadgedIconView *badgedIconView = [contentView valueForKey:@"badgedIconView"]; 63 | UIView *appIconView = badgedIconView.iconView; 64 | UIImage *appIcon = contentView.prominentIcon ?: contentView.subordinateIcon; 65 | 66 | self.velvetView.frame = materialView.frame; 67 | 68 | Velvet2Colorizer *colorizer = [[Velvet2Colorizer alloc] initWithIdentifier:identifier]; 69 | colorizer.appIcon = appIcon; 70 | 71 | CGFloat defaultCornerRadius = SYSTEM_VERSION_LESS_THAN(@"16.0") ? 19 : 23.5; 72 | CGFloat cornerRadius = [[prefsManager settingForKey:@"cornerRadiusEnabled" withIdentifier:identifier] boolValue] ? [[prefsManager settingForKey:@"cornerRadiusCustom" withIdentifier:identifier] floatValue] : defaultCornerRadius; 73 | materialView.layer.continuousCorners = cornerRadius < materialView.frame.size.height / 2; 74 | materialView.layer.cornerRadius = MIN(cornerRadius, materialView.frame.size.height / 2); 75 | self.velvetView.layer.continuousCorners = cornerRadius < self.velvetView.frame.size.height / 2; 76 | self.velvetView.layer.cornerRadius = MIN(cornerRadius, self.velvetView.frame.size.height / 2); 77 | 78 | view.layer.cornerRadius = MIN(cornerRadius, materialView.frame.size.height / 2); 79 | materialView.superview.layer.cornerRadius = MIN(cornerRadius, materialView.frame.size.height / 2); 80 | stackDimmingView.layer.cornerRadius = MIN(cornerRadius, materialView.frame.size.height / 2); 81 | 82 | stackDimmingView.hidden = [[prefsManager settingForKey:@"stackDimmingViewHidden" withIdentifier:identifier] boolValue]; 83 | 84 | [colorizer setAppIconCornerRadius:appIconView]; 85 | [colorizer colorBackground:self.velvetView]; 86 | [colorizer setBackgroundBlur:materialView]; 87 | [colorizer colorBorder:self.velvetView]; 88 | [colorizer colorShadow:materialView]; 89 | [colorizer colorLine:self.velvetView inFrame:materialView.frame]; 90 | [colorizer colorTitle:title]; 91 | [colorizer colorMessage:message]; 92 | [colorizer colorDate:dateLabel]; 93 | [colorizer setAppearance:self.view]; 94 | } 95 | %end 96 | 97 | %hook NCNotificationSummaryPlatterView 98 | %property (nonatomic,retain) UIView *velvetView; 99 | 100 | -(void)didMoveToWindow { 101 | 102 | if (!self.velvetView) { 103 | UIView *velvetView = [UIView new]; 104 | [velvetView.layer insertSublayer:[CALayer layer] atIndex:0]; 105 | [velvetView.layer insertSublayer:[CALayer layer] atIndex:0]; 106 | [self insertSubview:velvetView atIndex:1]; 107 | 108 | self.velvetView = velvetView; 109 | self.velvetView.clipsToBounds = YES; 110 | } 111 | 112 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(velvetUpdateStyle) name:@"com.noisyflake.velvet2/updateStyle" object:nil]; 113 | } 114 | -(void)layoutSubviews { 115 | %orig; 116 | 117 | [self velvetUpdateStyle]; 118 | } 119 | 120 | %new 121 | -(void)velvetUpdateStyle { 122 | MTMaterialView *materialView = (MTMaterialView*)self.subviews[0]; 123 | NCNotificationSummaryContentView *contentView = [self valueForKey:@"summaryContentView"]; 124 | UILabel *title = [contentView valueForKey:@"summaryTitleLabel"]; 125 | UILabel *message = [contentView valueForKey:@"summaryLabel"]; 126 | 127 | Velvet2Colorizer *colorizer = [[Velvet2Colorizer alloc] initWithIdentifier:@"com.noisyflake.velvetFocus"]; 128 | 129 | CGFloat defaultCornerRadius = SYSTEM_VERSION_LESS_THAN(@"16.0") ? 19 : 23.5; 130 | CGFloat cornerRadius = [[prefsManager settingForKey:@"cornerRadiusEnabled" withIdentifier:@"com.noisyflake.velvetFocus"] boolValue] ? [[prefsManager settingForKey:@"cornerRadiusCustom" withIdentifier:@"com.noisyflake.velvetFocus"] floatValue] : defaultCornerRadius; 131 | 132 | if (materialView) { 133 | self.velvetView.frame = materialView.frame; 134 | materialView.layer.continuousCorners = cornerRadius < self.frame.size.height / 2; 135 | materialView.layer.cornerRadius = MIN(cornerRadius, self.frame.size.height / 2); 136 | self.velvetView.layer.continuousCorners = cornerRadius < self.velvetView.frame.size.height / 2; 137 | self.velvetView.layer.cornerRadius = MIN(cornerRadius, self.velvetView.frame.size.height / 2); 138 | 139 | [colorizer colorBackground:self.velvetView]; 140 | [colorizer colorBorder:self.velvetView]; 141 | [colorizer colorShadow:materialView]; 142 | [colorizer colorLine:self.velvetView inFrame:materialView.frame]; 143 | [colorizer colorTitle:title]; 144 | [colorizer colorMessage:message]; 145 | [colorizer setAppearance:self]; 146 | } 147 | } 148 | %end 149 | 150 | %hook NCNotificationSeamlessContentView 151 | -(void)didMoveToWindow { 152 | %orig; 153 | 154 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(velvetUpdateStyle) name:@"com.noisyflake.velvet2/updateStyle" object:nil]; 155 | } 156 | -(void)layoutSubviews { 157 | %orig; 158 | 159 | [self velvetUpdateStyle]; 160 | } 161 | 162 | %new 163 | -(void)velvetUpdateStyle { 164 | NCNotificationShortLookViewController *controller = [self _viewControllerForAncestor]; 165 | Velvet2Colorizer *colorizer = [[Velvet2Colorizer alloc] initWithIdentifier:controller.notificationRequest.sectionIdentifier]; 166 | 167 | UILabel *title = [self valueForKey:@"primaryTextLabel"]; 168 | UILabel *message = [self valueForKey:@"secondaryTextElement"]; 169 | UILabel *footer = [self valueForKey:@"footerTextLabel"]; 170 | NCBadgedIconView *badgedIconView = [self valueForKey:@"badgedIconView"]; 171 | 172 | [colorizer toggleAppIconVisibility:badgedIconView withTitle:title message:message footer:footer alwaysUpdate:YES]; 173 | } 174 | %end 175 | 176 | // Fix notifications in a stack having the wrong color because they were re-used 177 | 178 | %hook NCNotificationListView 179 | -(void)recycleVisibleViews{} 180 | -(void)_recycleViewIfNecessary:(id)arg1{} 181 | -(void)_recycleViewIfNecessary:(id)arg1 withDataSource:(id)arg2{} 182 | %end 183 | 184 | %ctor { 185 | prefsManager = [NSClassFromString(@"Velvet2PrefsManager") sharedInstance]; 186 | 187 | if ([[prefsManager objectForKey:@"enabled"] boolValue]) { 188 | %init; 189 | } 190 | } -------------------------------------------------------------------------------- /src/Velvet2Colorizer.m: -------------------------------------------------------------------------------- 1 | #import "../headers/HeadersTweak.h" 2 | 3 | @implementation Velvet2Colorizer 4 | 5 | - (instancetype)initWithIdentifier:(NSString *)identifier { 6 | Velvet2Colorizer *colorizer = [super init]; 7 | 8 | self.identifier = identifier; 9 | self.manager = [Velvet2PrefsManager sharedInstance]; 10 | 11 | return colorizer; 12 | } 13 | 14 | - (UIColor*)iconColor { 15 | if (!_iconColor) { 16 | NSArray *iconColors = [[CCColorCube new] extractColorsFromImage:self.appIcon flags:CCAvoidWhite|CCOnlyBrightColors count:1]; 17 | _iconColor = iconColors.count ? iconColors[0] : nil; 18 | } 19 | 20 | return _iconColor; 21 | } 22 | 23 | - (void)colorBackground:(UIView *)backgroundView { 24 | UIColor *backgroundColor; 25 | 26 | BOOL backgroundEnabled = [[self.manager settingForKey:@"backgroundEnabled" withIdentifier:self.identifier] boolValue]; 27 | 28 | if (backgroundEnabled) { 29 | NSString *backgroundType = [self.manager settingForKey:@"backgroundType" withIdentifier:self.identifier]; 30 | 31 | if ([backgroundType isEqual:@"color"]) { 32 | backgroundColor = [self.manager colorForKey:@"backgroundColor" withIdentifier:self.identifier]; 33 | } else if ([backgroundType isEqual:@"gradient"]) { 34 | NSArray *gradientColors = @[(id)[self.manager colorForKey:@"backgroundGradient1" withIdentifier:self.identifier].CGColor, (id)[self.manager colorForKey:@"backgroundGradient2" withIdentifier:self.identifier].CGColor]; 35 | backgroundColor = [UIColor colorFromGradient:gradientColors withDirection:[self.manager settingForKey:@"backgroundGradientDirection" withIdentifier:self.identifier] inFrame:backgroundView.frame]; 36 | } else if ([backgroundType isEqual:@"icon"]) { 37 | backgroundColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"backgroundIconAlpha" withIdentifier:self.identifier]] : nil; 38 | } 39 | } 40 | 41 | backgroundView.backgroundColor = backgroundColor; 42 | } 43 | 44 | - (void)setBackgroundBlur:(UIView *)materialView { 45 | BOOL hideBackground = [[self.manager settingForKey:@"backgroundBlurHidden" withIdentifier:self.identifier] boolValue]; 46 | materialView.alpha = hideBackground ? 0 : 1; 47 | materialView.hidden = hideBackground ? YES : NO; 48 | } 49 | 50 | - (void)colorBorder:(UIView *)borderView { 51 | UIColor *borderColor; 52 | 53 | BOOL borderEnabled = [[self.manager settingForKey:@"borderEnabled" withIdentifier:self.identifier] boolValue]; 54 | 55 | if (borderEnabled) { 56 | NSString *borderType = [self.manager settingForKey:@"borderType" withIdentifier:self.identifier]; 57 | 58 | if ([borderType isEqual:@"color"]) { 59 | borderColor = [self.manager colorForKey:@"borderColor" withIdentifier:self.identifier]; 60 | } else if ([borderType isEqual:@"gradient"]) { 61 | NSArray *gradientColors = @[(id)[self.manager colorForKey:@"borderGradient1" withIdentifier:self.identifier].CGColor, (id)[self.manager colorForKey:@"borderGradient2" withIdentifier:self.identifier].CGColor]; 62 | borderColor = [UIColor colorFromGradient:gradientColors withDirection:[self.manager settingForKey:@"borderGradientDirection" withIdentifier:self.identifier] inFrame:borderView.frame flipY:YES]; 63 | } else if ([borderType isEqual:@"icon"]) { 64 | borderColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"borderIconAlpha" withIdentifier:self.identifier]] : nil; 65 | } 66 | } 67 | 68 | borderView.layer.borderWidth = borderColor ? [[self.manager settingForKey:@"borderWidth" withIdentifier:self.identifier] floatValue] : 0; 69 | borderView.layer.borderColor = borderColor ? borderColor.CGColor : nil; 70 | } 71 | 72 | - (void)colorShadow:(UIView *)shadowView { 73 | UIColor *shadowColor; 74 | 75 | BOOL shadowEnabled = [[self.manager settingForKey:@"shadowEnabled" withIdentifier:self.identifier] boolValue]; 76 | 77 | if (shadowEnabled) { 78 | NSString *shadowType = [self.manager settingForKey:@"shadowType" withIdentifier:self.identifier]; 79 | 80 | if ([shadowType isEqual:@"color"]) { 81 | shadowColor = [self.manager colorForKey:@"shadowColor" withIdentifier:self.identifier]; 82 | } else if ([shadowType isEqual:@"icon"]) { 83 | shadowColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"shadowIconAlpha" withIdentifier:self.identifier]] : nil; 84 | } 85 | } 86 | 87 | shadowView.layer.shadowRadius = shadowColor ? [[self.manager settingForKey:@"shadowWidth" withIdentifier:self.identifier] floatValue] : 0; 88 | shadowView.layer.shadowOffset = CGSizeZero; 89 | shadowView.layer.shadowColor = shadowColor ? shadowColor.CGColor : nil; 90 | shadowView.layer.shadowOpacity = 1; 91 | } 92 | 93 | - (void)colorLine:(UIView *)lineView inFrame:(CGRect)frame { 94 | NSString *position = [self.manager settingForKey:@"linePosition" withIdentifier:self.identifier]; 95 | CGFloat size = [[self.manager settingForKey:@"lineWidth" withIdentifier:self.identifier] floatValue]; 96 | 97 | CGFloat x = [position isEqual:@"right"] || [position isEqual:@"leftRight"] ? frame.size.width - size : 0; 98 | CGFloat y = [position isEqual:@"bottom"] || [position isEqual:@"topBottom"] ? frame.size.height - size : 0; 99 | CGFloat width = [position isEqual:@"top"] || [position isEqual:@"bottom"] || [position isEqual:@"topBottom"] ? frame.size.width : size; 100 | CGFloat height = [position isEqual:@"left"] || [position isEqual:@"right"] || [position isEqual:@"leftRight"] ? frame.size.height : size; 101 | lineView.layer.sublayers[0].frame = CGRectMake(x, y, width, height); 102 | 103 | if ([position isEqual:@"topBottom"] || [position isEqual:@"leftRight"]) { 104 | lineView.layer.sublayers[1].frame = CGRectMake(0, 0, width, height); 105 | } else { 106 | lineView.layer.sublayers[1].frame = CGRectZero; 107 | } 108 | 109 | UIColor *lineColor; 110 | 111 | BOOL lineEnabled = [[self.manager settingForKey:@"lineEnabled" withIdentifier:self.identifier] boolValue]; 112 | 113 | if (lineEnabled) { 114 | NSString *lineType = [self.manager settingForKey:@"lineType" withIdentifier:self.identifier]; 115 | 116 | if ([lineType isEqual:@"color"]) { 117 | lineColor = [self.manager colorForKey:@"lineColor" withIdentifier:self.identifier]; 118 | } else if ([lineType isEqual:@"gradient"]) { 119 | NSArray *gradientColors = @[(id)[self.manager colorForKey:@"lineGradient1" withIdentifier:self.identifier].CGColor, (id)[self.manager colorForKey:@"lineGradient2" withIdentifier:self.identifier].CGColor]; 120 | lineColor = [UIColor colorFromGradient:gradientColors withDirection:[self.manager settingForKey:@"lineGradientDirection" withIdentifier:self.identifier] inFrame:lineView.layer.sublayers[0].frame flipY:YES]; 121 | } else if ([lineType isEqual:@"icon"]) { 122 | lineColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"lineIconAlpha" withIdentifier:self.identifier]] : nil; 123 | } 124 | } 125 | 126 | lineView.layer.sublayers[0].backgroundColor = lineColor ? lineColor.CGColor : nil; 127 | lineView.layer.sublayers[1].backgroundColor = lineColor ? lineColor.CGColor : nil; 128 | } 129 | 130 | - (void)colorTitle:(UILabel*)title { 131 | UIColor *titleColor; 132 | 133 | BOOL titleEnabled = [[self.manager settingForKey:@"titleEnabled" withIdentifier:self.identifier] boolValue]; 134 | 135 | if (titleEnabled) { 136 | NSString *titleType = [self.manager settingForKey:@"titleType" withIdentifier:self.identifier]; 137 | 138 | if ([titleType isEqual:@"color"]) { 139 | titleColor = [self.manager colorForKey:@"titleColor" withIdentifier:self.identifier]; 140 | } else if ([titleType isEqual:@"gradient"]) { 141 | NSArray *gradientColors = @[(id)[self.manager colorForKey:@"titleGradient1" withIdentifier:self.identifier].CGColor, (id)[self.manager colorForKey:@"titleGradient2" withIdentifier:self.identifier].CGColor]; 142 | CGSize size = [title sizeThatFits:title.frame.size]; 143 | titleColor = [UIColor colorFromGradient:gradientColors withDirection:[self.manager settingForKey:@"titleGradientDirection" withIdentifier:self.identifier] inFrame:CGRectMake(title.frame.origin.x, title.frame.origin.y, size.width, size.height)]; 144 | } else if ([titleType isEqual:@"icon"]) { 145 | titleColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"titleIconAlpha" withIdentifier:self.identifier]] : nil; 146 | } 147 | } 148 | 149 | title.textColor = titleColor ? titleColor : [UIColor.labelColor colorWithAlphaComponent:0.9]; 150 | } 151 | 152 | - (void)colorMessage:(UILabel*)message { 153 | UIColor *messageColor; 154 | 155 | BOOL messageEnabled = [[self.manager settingForKey:@"messageEnabled" withIdentifier:self.identifier] boolValue]; 156 | 157 | if (messageEnabled) { 158 | NSString *messageType = [self.manager settingForKey:@"messageType" withIdentifier:self.identifier]; 159 | 160 | if ([messageType isEqual:@"color"]) { 161 | messageColor = [self.manager colorForKey:@"messageColor" withIdentifier:self.identifier]; 162 | } else if ([messageType isEqual:@"gradient"]) { 163 | NSArray *gradientColors = @[(id)[self.manager colorForKey:@"messageGradient1" withIdentifier:self.identifier].CGColor, (id)[self.manager colorForKey:@"messageGradient2" withIdentifier:self.identifier].CGColor]; 164 | CGSize size = [message sizeThatFits:message.frame.size]; 165 | messageColor = [UIColor colorFromGradient:gradientColors withDirection:[self.manager settingForKey:@"messageGradientDirection" withIdentifier:self.identifier] inFrame:CGRectMake(message.frame.origin.x, message.frame.origin.y, size.width, size.height)]; 166 | } else if ([messageType isEqual:@"icon"]) { 167 | messageColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"messageIconAlpha" withIdentifier:self.identifier]] : nil; 168 | } 169 | } 170 | 171 | message.textColor = messageColor ? messageColor : [UIColor.labelColor colorWithAlphaComponent:0.9]; 172 | } 173 | 174 | - (void)colorDate:(UILabel*)date { 175 | UIColor *dateColor; 176 | 177 | BOOL dateEnabled = [[self.manager settingForKey:@"dateEnabled" withIdentifier:self.identifier] boolValue]; 178 | 179 | if (dateEnabled) { 180 | NSString *dateType = [self.manager settingForKey:@"dateType" withIdentifier:self.identifier]; 181 | 182 | if ([dateType isEqual:@"color"]) { 183 | dateColor = [self.manager colorForKey:@"dateColor" withIdentifier:self.identifier]; 184 | } else if ([dateType isEqual:@"gradient"]) { 185 | NSArray *gradientColors = @[(id)[self.manager colorForKey:@"dateGradient1" withIdentifier:self.identifier].CGColor, (id)[self.manager colorForKey:@"dateGradient2" withIdentifier:self.identifier].CGColor]; 186 | dateColor = [UIColor colorFromGradient:gradientColors withDirection:[self.manager settingForKey:@"dateGradientDirection" withIdentifier:self.identifier] inFrame:date.frame]; 187 | } else if ([dateType isEqual:@"icon"]) { 188 | dateColor = self.iconColor ? [self.iconColor colorWithAlphaComponent:[self.manager alphaValueForKey:@"dateIconAlpha" withIdentifier:self.identifier]] : nil; 189 | } 190 | } 191 | 192 | date.layer.filters = nil; 193 | 194 | // Let's fake the label color here, since the stupid filter won't play nice 195 | date.textColor = dateColor ? dateColor : [UIColor.labelColor colorWithAlphaComponent:0.5]; 196 | } 197 | 198 | - (void)setAppIconCornerRadius:(UIView*)appIcon { 199 | appIcon.clipsToBounds = YES; 200 | appIcon.layer.cornerRadius = [[self.manager settingForKey:@"appIconCornerRadiusCircle" withIdentifier:self.identifier] boolValue] ? appIcon.frame.size.height / 2 : 0; 201 | } 202 | 203 | - (void)toggleAppIconVisibility:(UIView*)appIcon withTitle:(UILabel*)title message:(UILabel*)message footer:(UILabel*)footer alwaysUpdate:(BOOL)alwaysUpdate { 204 | BOOL shouldHide = [[self.manager settingForKey:@"appIconHidden" withIdentifier:self.identifier] boolValue]; 205 | 206 | // AlwaysUpdate is necessary for the real notification, as layoutSubviews gets called constantly and would override our one-time change. 207 | // However, in the Settings preview, this is not necessary and would only move the labels too far to the left 208 | 209 | if (shouldHide && (!appIcon.hidden || alwaysUpdate)) { 210 | appIcon.alpha = 0; // For smooth animations in the preview 211 | appIcon.hidden = YES; 212 | title.frame = CGRectMake(title.frame.origin.x - appIcon.frame.size.width - 8, title.frame.origin.y, title.frame.size.width + appIcon.frame.size.width, title.frame.size.height); 213 | message.frame = CGRectMake(message.frame.origin.x - appIcon.frame.size.width - 8, message.frame.origin.y, message.frame.size.width + appIcon.frame.size.width, message.frame.size.height); 214 | if (footer) footer.frame = CGRectMake(footer.frame.origin.x - appIcon.frame.size.width - 8, footer.frame.origin.y, footer.frame.size.width + appIcon.frame.size.width, footer.frame.size.height); 215 | } else if (!shouldHide && appIcon.hidden) { 216 | appIcon.alpha = 1; 217 | appIcon.hidden = NO; 218 | title.frame = CGRectMake(title.frame.origin.x + appIcon.frame.size.width + 8, title.frame.origin.y, title.frame.size.width - appIcon.frame.size.width, title.frame.size.height); 219 | message.frame = CGRectMake(message.frame.origin.x + appIcon.frame.size.width + 8, message.frame.origin.y, message.frame.size.width - appIcon.frame.size.width, message.frame.size.height); 220 | if (footer) footer.frame = CGRectMake(footer.frame.origin.x + appIcon.frame.size.width + 8, footer.frame.origin.y, footer.frame.size.width - appIcon.frame.size.width, footer.frame.size.height); 221 | } 222 | } 223 | 224 | - (void)setAppearance:(UIView*)view { 225 | NSString *appearance = [self.manager settingForKey:@"appearance" withIdentifier:self.identifier]; 226 | view.overrideUserInterfaceStyle = [appearance isEqual:@"dark"] ? UIUserInterfaceStyleDark : [appearance isEqual:@"light"] ? UIUserInterfaceStyleLight : UIUserInterfaceStyleUnspecified; 227 | } 228 | 229 | @end -------------------------------------------------------------------------------- /src/ColorDetection.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Ole Krause-Sparmann 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without restriction, 5 | // including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 6 | // and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 7 | // subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or substantial 10 | // portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 13 | // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 15 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | #import "../headers/Velvet2/ColorDetection.h" 19 | 20 | @implementation CCLocalMaximum 21 | @end 22 | 23 | // The cell resolution in each color dimension 24 | #define COLOR_CUBE_RESOLUTION 30 25 | 26 | // Threshold used to filter bright colors 27 | #define BRIGHT_COLOR_THRESHOLD 0.6 28 | 29 | // Threshold used to filter dark colors 30 | #define DARK_COLOR_THRESHOLD 0.4 31 | 32 | // Threshold (distance in color space) for distinct colors 33 | #define DISTINCT_COLOR_THRESHOLD 0.2 34 | 35 | // Helper macro to compute linear index for cells 36 | #define CELL_INDEX(r,g,b) (r+g*COLOR_CUBE_RESOLUTION+b*COLOR_CUBE_RESOLUTION*COLOR_CUBE_RESOLUTION) 37 | 38 | // Helper macro to get total count of cells 39 | #define CELL_COUNT COLOR_CUBE_RESOLUTION*COLOR_CUBE_RESOLUTION*COLOR_CUBE_RESOLUTION 40 | 41 | // Indices for neighbour cells in three dimensional grid 42 | int neighbourIndices[27][3] = { 43 | { 0, 0, 0}, 44 | { 0, 0, 1}, 45 | { 0, 0,-1}, 46 | 47 | { 0, 1, 0}, 48 | { 0, 1, 1}, 49 | { 0, 1,-1}, 50 | 51 | { 0,-1, 0}, 52 | { 0,-1, 1}, 53 | { 0,-1,-1}, 54 | 55 | { 1, 0, 0}, 56 | { 1, 0, 1}, 57 | { 1, 0,-1}, 58 | 59 | { 1, 1, 0}, 60 | { 1, 1, 1}, 61 | { 1, 1,-1}, 62 | 63 | { 1,-1, 0}, 64 | { 1,-1, 1}, 65 | { 1,-1,-1}, 66 | 67 | {-1, 0, 0}, 68 | {-1, 0, 1}, 69 | {-1, 0,-1}, 70 | 71 | {-1, 1, 0}, 72 | {-1, 1, 1}, 73 | {-1, 1,-1}, 74 | 75 | {-1,-1, 0}, 76 | {-1,-1, 1}, 77 | {-1,-1,-1} 78 | }; 79 | 80 | @interface CCColorCube () { 81 | CCCubeCell cells[COLOR_CUBE_RESOLUTION*COLOR_CUBE_RESOLUTION*COLOR_CUBE_RESOLUTION]; 82 | } 83 | 84 | // Returns array of raw pixel data (needs to be freed) 85 | - (unsigned char *)rawPixelDataFromImage:(UIImage *)image pixelCount:(unsigned int*)pixelCount; 86 | 87 | // Resets all cells 88 | - (void)clearCells; 89 | 90 | // Returns array of CCLocalMaximum objects 91 | - (NSArray *)findLocalMaximaInImage:(UIImage *)image flags:(NSUInteger)flags; 92 | 93 | // Returns array of CCLocalMaximum objects 94 | - (NSArray *)findAndSortMaximaInImage:(UIImage *)image flags:(NSUInteger)flags; 95 | 96 | // Returns array of CCLocalMaximum objects 97 | - (NSArray *)extractAndFilterMaximaFromImage:(UIImage *)image flags:(NSUInteger)flags; 98 | 99 | // Returns array of UIColor objects 100 | - (NSArray *)colorsFromMaxima:(NSArray *)maxima; 101 | 102 | // Returns new array with only distinct maxima 103 | - (NSArray *)filterDistinctMaxima:(NSArray *)maxima threshold:(CGFloat)threshold; 104 | 105 | // Removes maxima too close to specified color 106 | - (NSArray *)filterMaxima:(NSArray *)maxima tooCloseToColor:(UIColor *)color; 107 | 108 | // Tries to get count distinct maxima 109 | - (NSArray *)performAdaptiveDistinctFilteringForMaxima:(NSArray *)maxima count:(NSUInteger)count; 110 | 111 | // Orders maxima by brightness 112 | - (NSArray *)orderByBrightness:(NSArray *)maxima; 113 | 114 | // Orders maxima by darkness 115 | - (NSArray *)orderByDarkness:(NSArray *)maxima; 116 | 117 | @end 118 | 119 | @implementation CCColorCube 120 | 121 | #pragma mark - Object lifecycle 122 | 123 | - (id)init 124 | { 125 | self = [super init]; 126 | if (self) { 127 | 128 | } 129 | 130 | return self; 131 | } 132 | 133 | - (void)dealloc 134 | { 135 | } 136 | 137 | #pragma mark - Local maxima search 138 | 139 | - (NSArray *)findLocalMaximaInImage:(UIImage *)image flags:(NSUInteger)flags 140 | { 141 | // Reset all cells 142 | [self clearCells]; 143 | 144 | // Get raw pixel data from image 145 | unsigned int pixelCount; 146 | unsigned char *rawData = [self rawPixelDataFromImage:image pixelCount:&pixelCount]; 147 | if (!rawData) return nil; 148 | 149 | // Helper variables 150 | double red, green, blue; 151 | int redIndex, greenIndex, blueIndex, cellIndex, localHitCount; 152 | BOOL isLocalMaximum; 153 | 154 | // Project each pixel into one of the cells in the three dimensional grid 155 | for (int k=0; k= DARK_COLOR_THRESHOLD || green >= DARK_COLOR_THRESHOLD || blue >= DARK_COLOR_THRESHOLD) continue; 167 | } 168 | 169 | // Map color components to cell indices in each color dimension 170 | redIndex = (int)(red*(COLOR_CUBE_RESOLUTION-1.0)); 171 | greenIndex = (int)(green*(COLOR_CUBE_RESOLUTION-1.0)); 172 | blueIndex = (int)(blue*(COLOR_CUBE_RESOLUTION-1.0)); 173 | 174 | // Compute linear cell index 175 | cellIndex = CELL_INDEX(redIndex, greenIndex, blueIndex); 176 | 177 | // Increase hit count of cell 178 | cells[cellIndex].hitCount++; 179 | 180 | // Add pixel colors to cell color accumulators 181 | cells[cellIndex].redAcc += red; 182 | cells[cellIndex].greenAcc += green; 183 | cells[cellIndex].blueAcc += blue; 184 | } 185 | 186 | // Deallocate raw pixel data memory 187 | free(rawData); 188 | 189 | // We collect local maxima in here 190 | NSMutableArray *localMaxima = [NSMutableArray array]; 191 | 192 | // Find local maxima in the grid 193 | for (int r=0; r= 0 && greenIndex >= 0 && blueIndex >= 0) { 213 | if (redIndex < COLOR_CUBE_RESOLUTION && greenIndex < COLOR_CUBE_RESOLUTION && blueIndex < COLOR_CUBE_RESOLUTION) { 214 | if (cells[CELL_INDEX(redIndex, greenIndex, blueIndex)].hitCount > localHitCount) { 215 | // Neighbour hit count is higher, so this is NOT a local maximum. 216 | isLocalMaximum = NO; 217 | // Break inner loop 218 | break; 219 | } 220 | } 221 | } 222 | } 223 | 224 | // If this is not a local maximum, continue with loop. 225 | if (!isLocalMaximum) continue; 226 | 227 | // Otherwise add this cell as local maximum 228 | CCLocalMaximum *maximum = [[CCLocalMaximum alloc] init]; 229 | maximum.cellIndex = CELL_INDEX(r, g, b); 230 | maximum.hitCount = cells[maximum.cellIndex].hitCount; 231 | maximum.red = cells[maximum.cellIndex].redAcc / (double)cells[maximum.cellIndex].hitCount; 232 | maximum.green = cells[maximum.cellIndex].greenAcc / (double)cells[maximum.cellIndex].hitCount; 233 | maximum.blue = cells[maximum.cellIndex].blueAcc / (double)cells[maximum.cellIndex].hitCount; 234 | maximum.brightness = fmax(fmax(maximum.red, maximum.green), maximum.blue); 235 | [localMaxima addObject:maximum]; 236 | } 237 | } 238 | } 239 | 240 | // Finally sort the array of local maxima by hit count 241 | NSArray *sortedMaxima = [localMaxima sortedArrayUsingComparator:^NSComparisonResult(CCLocalMaximum *m1, CCLocalMaximum *m2){ 242 | if (m1.hitCount == m2.hitCount) return NSOrderedSame; 243 | return m1.hitCount > m2.hitCount ? NSOrderedAscending : NSOrderedDescending; 244 | }]; 245 | 246 | return sortedMaxima; 247 | } 248 | 249 | - (NSArray *)findAndSortMaximaInImage:(UIImage *)image flags:(NSUInteger)flags 250 | { 251 | // First get local maxima of image 252 | NSArray *sortedMaxima = [self findLocalMaximaInImage:image flags:flags]; 253 | 254 | // Filter the maxima if we want only distinct colors 255 | if (flags & CCOnlyDistinctColors) { 256 | sortedMaxima = [self filterDistinctMaxima:sortedMaxima threshold:DISTINCT_COLOR_THRESHOLD]; 257 | } 258 | 259 | // If we should order the result array by brightness, do it 260 | if (flags & CCOrderByBrightness) { 261 | sortedMaxima = [self orderByBrightness:sortedMaxima]; 262 | } 263 | else if (flags & CCOrderByDarkness) { 264 | sortedMaxima = [self orderByDarkness:sortedMaxima]; 265 | } 266 | 267 | return sortedMaxima; 268 | } 269 | 270 | #pragma mark - Filtering and sorting 271 | 272 | - (NSArray *)filterDistinctMaxima:(NSArray *)maxima threshold:(CGFloat)threshold 273 | { 274 | NSMutableArray *filteredMaxima = [NSMutableArray array]; 275 | 276 | // Check for each maximum 277 | for (int k=0; k= 0.5) { 335 | [filteredMaxima addObject:max1]; 336 | } 337 | } 338 | 339 | return [NSArray arrayWithArray:filteredMaxima]; 340 | } 341 | 342 | - (NSArray *)orderByBrightness:(NSArray *)maxima 343 | { 344 | return [maxima sortedArrayUsingComparator:^NSComparisonResult(CCLocalMaximum *m1, CCLocalMaximum *m2){ 345 | return m1.brightness > m2.brightness ? NSOrderedAscending : NSOrderedDescending; 346 | }]; 347 | } 348 | 349 | - (NSArray *)orderByDarkness:(NSArray *)maxima 350 | { 351 | return [maxima sortedArrayUsingComparator:^NSComparisonResult(CCLocalMaximum *m1, CCLocalMaximum *m2){ 352 | return m1.brightness < m2.brightness ? NSOrderedAscending : NSOrderedDescending; 353 | }]; 354 | } 355 | 356 | - (NSArray *)performAdaptiveDistinctFilteringForMaxima:(NSArray *)maxima count:(NSUInteger)count 357 | { 358 | // If the count of maxima is higher than the requested count, perform distinct thresholding 359 | if (maxima.count > count) { 360 | 361 | NSArray *tempDistinctMaxima = maxima; 362 | double distinctThreshold = 0.1; 363 | 364 | // Decrease the threshold ten times. If this does not result in the wanted count 365 | for (int k=0; k<10; k++) { 366 | // Get array with current distinct threshold 367 | tempDistinctMaxima = [self filterDistinctMaxima:maxima threshold:distinctThreshold]; 368 | 369 | // If this array has less than count, break and take the current sortedMaxima 370 | if (tempDistinctMaxima.count <= count) { 371 | break; 372 | } 373 | 374 | // Keep this result (length is > count) 375 | maxima = tempDistinctMaxima; 376 | 377 | // Increase threshold by 0.05 378 | distinctThreshold += 0.05; 379 | } 380 | 381 | // Only take first count maxima 382 | maxima = [maxima subarrayWithRange:NSMakeRange(0, count)]; 383 | } 384 | 385 | return maxima; 386 | } 387 | 388 | #pragma mark - Maximum to color conversion 389 | 390 | - (NSArray *)colorsFromMaxima:(NSArray *)maxima 391 | { 392 | // Build the resulting color array 393 | NSMutableArray *colorArray = [NSMutableArray array]; 394 | 395 | // For each local maximum generate UIColor and add it to the result array 396 | for (CCLocalMaximum *maximum in maxima) { 397 | [colorArray addObject:[UIColor colorWithRed:maximum.red green:maximum.green blue:maximum.blue alpha:1.0]]; 398 | } 399 | 400 | return [NSArray arrayWithArray:colorArray]; 401 | } 402 | 403 | #pragma mark - Default maxima extraction and filtering 404 | 405 | - (NSArray *)extractAndFilterMaximaFromImage:(UIImage *)image flags:(NSUInteger)flags 406 | { 407 | // Get maxima 408 | NSArray *sortedMaxima = [self findAndSortMaximaInImage:image flags:flags]; 409 | 410 | // Filter out colors too close to black 411 | if (flags & CCAvoidBlack) { 412 | sortedMaxima = [self filterMaxima:sortedMaxima tooCloseToColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:1]]; 413 | } 414 | 415 | // Filter out colors too close to white 416 | if (flags & CCAvoidWhite) { 417 | sortedMaxima = [self filterMaxima:sortedMaxima tooCloseToColor:[UIColor colorWithRed:1 green:1 blue:1 alpha:1]]; 418 | } 419 | 420 | // Return maxima array 421 | return sortedMaxima; 422 | } 423 | #pragma mark - Public methods 424 | 425 | - (NSArray *)extractColorsFromImage:(UIImage *)image flags:(CCFlags)flags 426 | { 427 | // Get maxima 428 | NSArray *sortedMaxima = [self extractAndFilterMaximaFromImage:image flags:flags]; 429 | 430 | // Return color array 431 | return [self colorsFromMaxima:sortedMaxima]; 432 | } 433 | 434 | - (NSArray *)extractColorsFromImage:(UIImage *)image flags:(CCFlags)flags avoidColor:(UIColor*)avoidColor 435 | { 436 | // Get maxima 437 | NSArray *sortedMaxima = [self extractAndFilterMaximaFromImage:image flags:flags]; 438 | 439 | // Filter out colors that are too close to the specified color 440 | sortedMaxima = [self filterMaxima:sortedMaxima tooCloseToColor:avoidColor]; 441 | 442 | // Return color array 443 | return [self colorsFromMaxima:sortedMaxima]; 444 | } 445 | 446 | - (NSArray *)extractBrightColorsFromImage:(UIImage *)image avoidColor:(UIColor *)avoidColor count:(NSUInteger)count 447 | { 448 | // Get maxima (bright only) 449 | NSArray *sortedMaxima = [self findAndSortMaximaInImage:image flags:CCOnlyBrightColors]; 450 | 451 | if (avoidColor) { 452 | // Filter out colors that are too close to the specified color 453 | sortedMaxima = [self filterMaxima:sortedMaxima tooCloseToColor:avoidColor]; 454 | } 455 | 456 | // Do clever distinct color filtering 457 | sortedMaxima = [self performAdaptiveDistinctFilteringForMaxima:sortedMaxima count:count]; 458 | 459 | // Return color array 460 | return [self colorsFromMaxima:sortedMaxima]; 461 | } 462 | 463 | - (NSArray *)extractDarkColorsFromImage:(UIImage *)image avoidColor:(UIColor*)avoidColor count:(NSUInteger)count 464 | { 465 | // Get maxima (dark only) 466 | NSArray *sortedMaxima = [self findAndSortMaximaInImage:image flags:CCOnlyDarkColors]; 467 | 468 | if (avoidColor) { 469 | // Filter out colors that are too close to the specified color 470 | sortedMaxima = [self filterMaxima:sortedMaxima tooCloseToColor:avoidColor]; 471 | } 472 | 473 | // Do clever distinct color filtering 474 | sortedMaxima = [self performAdaptiveDistinctFilteringForMaxima:sortedMaxima count:count]; 475 | 476 | // Return color array 477 | return [self colorsFromMaxima:sortedMaxima]; 478 | } 479 | 480 | - (NSArray *)extractColorsFromImage:(UIImage *)image flags:(CCFlags)flags count:(NSUInteger)count 481 | { 482 | // Get maxima 483 | NSArray *sortedMaxima = [self extractAndFilterMaximaFromImage:image flags:flags]; 484 | 485 | // Do clever distinct color filtering 486 | sortedMaxima = [self performAdaptiveDistinctFilteringForMaxima:sortedMaxima count:count]; 487 | 488 | // Return color array 489 | return [self colorsFromMaxima:sortedMaxima]; 490 | } 491 | 492 | #pragma mark - Resetting cells 493 | 494 | - (void)clearCells 495 | { 496 | for (int k=0; k