mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-05 16:58:59 +00:00

- The Dark theme now uses pure black for the story background, and some other darker colors. - The Medium theme now also uses darker colors.
448 lines
15 KiB
Objective-C
448 lines
15 KiB
Objective-C
//
|
|
// ThemeManager.m
|
|
// NewsBlur
|
|
//
|
|
// Created by David Sinclair on 2015-12-06.
|
|
// Copyright © 2015 NewsBlur. All rights reserved.
|
|
//
|
|
|
|
#import "ThemeManager.h"
|
|
#import "NewsBlurAppDelegate.h"
|
|
#import "NBContainerViewController.h"
|
|
#import "NewsBlurViewController.h"
|
|
#import "DashboardViewController.h"
|
|
#import "FeedDetailViewController.h"
|
|
#import "StoryDetailViewController.h"
|
|
#import "StoryPageControl.h"
|
|
#import "OriginalStoryViewController.h"
|
|
#import <AudioToolbox/AudioToolbox.h>
|
|
|
|
NSString * const ThemeStyleLight = @"light";
|
|
NSString * const ThemeStyleSepia = @"sepia";
|
|
NSString * const ThemeStyleMedium = @"medium";
|
|
NSString * const ThemeStyleDark = @"dark";
|
|
|
|
@interface UINavigationController (Theme)
|
|
|
|
@end
|
|
|
|
@implementation UINavigationController (Theme)
|
|
|
|
- (UIStatusBarStyle)preferredStatusBarStyle {
|
|
if ([ThemeManager themeManager].isDarkTheme) {
|
|
return UIStatusBarStyleLightContent;
|
|
} else if (@available(iOS 13.0, *)) {
|
|
return UIStatusBarStyleDarkContent;
|
|
} else {
|
|
return UIStatusBarStyleDefault;
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@interface ThemeManager ()
|
|
|
|
@property (nonatomic, readonly) NewsBlurAppDelegate *appDelegate;
|
|
@property (nonatomic) BOOL justToggledViaGesture;
|
|
|
|
@end
|
|
|
|
@implementation ThemeManager
|
|
|
|
+ (instancetype)themeManager {
|
|
static id themeManager = nil;
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
themeManager = [self new];
|
|
});
|
|
|
|
return themeManager;
|
|
}
|
|
|
|
- (NSString *)theme {
|
|
NSString *theme = [[NSUserDefaults standardUserDefaults] objectForKey:@"theme_style"];
|
|
|
|
if (![self isValidTheme:theme]) {
|
|
theme = ThemeStyleLight;
|
|
}
|
|
|
|
return theme;
|
|
}
|
|
|
|
- (void)setTheme:(NSString *)theme {
|
|
// Automatically turn off following the system appearance when manually changing the theme.
|
|
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"theme_follow_system"];
|
|
|
|
[self reallySetTheme:theme];
|
|
|
|
NSLog(@"Manually changed to theme: %@", self.themeDisplayName); // log
|
|
}
|
|
|
|
- (void)reallySetTheme:(NSString *)theme {
|
|
if ([self isValidTheme:theme]) {
|
|
[[NSUserDefaults standardUserDefaults] setObject:theme forKey:@"theme_style"];
|
|
[self updateTheme];
|
|
}
|
|
}
|
|
|
|
- (NSString *)themeDisplayName {
|
|
NSString *theme = self.theme;
|
|
|
|
if ([theme isEqualToString:ThemeStyleDark]) {
|
|
return @"Dark";
|
|
} else if ([theme isEqualToString:ThemeStyleSepia]) {
|
|
return @"Sepia";
|
|
} else if ([theme isEqualToString:ThemeStyleMedium]) {
|
|
return @"Medium";
|
|
} else {
|
|
return @"Light";
|
|
}
|
|
}
|
|
|
|
- (NSString *)themeCSSSuffix {
|
|
NSString *theme = self.theme;
|
|
|
|
if ([theme isEqualToString:ThemeStyleDark]) {
|
|
return @"Dark";
|
|
} else if ([theme isEqualToString:ThemeStyleSepia]) {
|
|
return @"Sepia";
|
|
} else if ([theme isEqualToString:ThemeStyleMedium]) {
|
|
return @"Medium";
|
|
} else {
|
|
return @"Light";
|
|
}
|
|
}
|
|
|
|
- (NSString *)similarTheme {
|
|
NSString *theme = self.theme;
|
|
|
|
if ([theme isEqualToString:ThemeStyleDark]) {
|
|
return ThemeStyleMedium;
|
|
} else if ([theme isEqualToString:ThemeStyleMedium]) {
|
|
return ThemeStyleDark;
|
|
} else if ([theme isEqualToString:ThemeStyleSepia]) {
|
|
return ThemeStyleLight;
|
|
} else {
|
|
return ThemeStyleSepia;
|
|
}
|
|
}
|
|
|
|
- (BOOL)isDarkTheme {
|
|
NSString *theme = self.theme;
|
|
|
|
return [theme isEqualToString:ThemeStyleDark] || [theme isEqualToString:ThemeStyleMedium];
|
|
}
|
|
|
|
- (BOOL)isValidTheme:(NSString *)theme {
|
|
return [theme isEqualToString:ThemeStyleLight] || [theme isEqualToString:ThemeStyleSepia] || [theme isEqualToString:ThemeStyleMedium] || [theme isEqualToString:ThemeStyleDark];
|
|
}
|
|
|
|
- (NewsBlurAppDelegate *)appDelegate {
|
|
return (NewsBlurAppDelegate *)[UIApplication sharedApplication].delegate;
|
|
}
|
|
|
|
- (UIColor *)fixedColorFromRGB:(NSInteger)rgbValue {
|
|
CGFloat red = ((rgbValue & 0xFF0000) >> 16) / 255.0;
|
|
CGFloat green = ((rgbValue & 0xFF00) >> 8) / 255.0;
|
|
CGFloat blue = ((rgbValue & 0xFF)) / 255.0;
|
|
|
|
return [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
|
|
}
|
|
|
|
- (UIColor *)colorFromLightRGB:(NSInteger)lightRGBValue darkRGB:(NSUInteger)darkRGBValue {
|
|
NSInteger rgbValue = lightRGBValue;
|
|
|
|
if (self.isDarkTheme) {
|
|
rgbValue = darkRGBValue;
|
|
}
|
|
|
|
return [self fixedColorFromRGB:rgbValue];
|
|
}
|
|
|
|
- (UIColor *)colorFromLightRGB:(NSInteger)lightRGBValue sepiaRGB:(NSUInteger)sepiaRGBValue mediumRGB:(NSUInteger)mediumRGBValue darkRGB:(NSUInteger)darkRGBValue {
|
|
NSInteger rgbValue = lightRGBValue;
|
|
|
|
if ([self.theme isEqualToString:ThemeStyleSepia]) {
|
|
rgbValue = sepiaRGBValue;
|
|
} else if ([self.theme isEqualToString:ThemeStyleMedium]) {
|
|
rgbValue = mediumRGBValue;
|
|
} else if ([self.theme isEqualToString:ThemeStyleDark]) {
|
|
rgbValue = darkRGBValue;
|
|
}
|
|
|
|
return [self fixedColorFromRGB:rgbValue];
|
|
}
|
|
|
|
- (UIColor *)themedColorFromRGB:(NSInteger)rgbValue {
|
|
NSString *theme = self.theme;
|
|
CGFloat red = ((rgbValue & 0xFF0000) >> 16) / 255.0;
|
|
CGFloat green = ((rgbValue & 0xFF00) >> 8) / 255.0;
|
|
CGFloat blue = ((rgbValue & 0xFF)) / 255.0;
|
|
|
|
// Debug method to log all of the unique colors; leave commented out
|
|
// [self debugColor:rgbValue];
|
|
|
|
if ([theme isEqualToString:ThemeStyleDark]) {
|
|
return [UIColor colorWithRed:1.0 - red green:1.0 - green blue:1.0 - blue alpha:1.0];
|
|
} else if ([theme isEqualToString:ThemeStyleMedium]) {
|
|
if (rgbValue == 0x8F918B) {
|
|
return [UIColor colorWithWhite:0.7 alpha:1.0];
|
|
} else if (red < 0.5 && green < 0.5 && blue < 0.5) {
|
|
return [UIColor colorWithRed:1.2 - red green:1.2 - green blue:1.2 - blue alpha:1.0];
|
|
} else {
|
|
return [UIColor colorWithRed:red - 0.7 green:green - 0.7 blue:blue - 0.7 alpha:1.0];
|
|
}
|
|
} else if ([theme isEqualToString:ThemeStyleSepia]) {
|
|
CGFloat outputRed = (red * 0.393) + (green * 0.769) + (blue * 0.189);
|
|
CGFloat outputGreen = (red * 0.349) + (green * 0.686) + (blue * 0.168);
|
|
CGFloat outputBlue = (red * 0.272) + (green * 0.534) + (blue * 0.131);
|
|
|
|
return [UIColor colorWithRed:outputRed green:outputGreen blue:outputBlue alpha:1.0];
|
|
} else {
|
|
return [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
|
|
}
|
|
}
|
|
|
|
- (UIImage *)themedImage:(UIImage *)image {
|
|
if ([self.theme isEqualToString:ThemeStyleDark]) {
|
|
CIImage *coreImage = [CIImage imageWithCGImage:image.CGImage];
|
|
CIFilter *filter = [CIFilter filterWithName:@"CIColorInvert"];
|
|
[filter setValue:coreImage forKey:kCIInputImageKey];
|
|
CIImage *result = [filter valueForKey:kCIOutputImageKey];
|
|
|
|
return [UIImage imageWithCIImage:result scale:image.scale orientation:image.imageOrientation];
|
|
} else if ([self.theme isEqualToString:ThemeStyleSepia]) {
|
|
CIImage *coreImage = [CIImage imageWithCGImage:image.CGImage];
|
|
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues:kCIInputImageKey, coreImage, @"inputIntensity", @0.8, nil];
|
|
CIImage *result = [filter outputImage];
|
|
|
|
return [UIImage imageWithCIImage:result scale:image.scale orientation:image.imageOrientation];
|
|
} else {
|
|
return image;
|
|
}
|
|
}
|
|
|
|
- (void)updateTextAttributesForSegmentedControl:(UISegmentedControl *)segmentedControl forState:(UIControlState)state foregroundColor:(UIColor *)foregroundColor {
|
|
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
|
|
NSDictionary *oldAttributes = [segmentedControl titleTextAttributesForState:state];
|
|
|
|
if (oldAttributes != nil) {
|
|
[attributes addEntriesFromDictionary:oldAttributes];
|
|
}
|
|
|
|
attributes[NSForegroundColorAttributeName] = foregroundColor;
|
|
|
|
[segmentedControl setTitleTextAttributes:attributes forState:state];
|
|
}
|
|
|
|
- (void)updateSegmentedControl:(UISegmentedControl *)segmentedControl {
|
|
segmentedControl.tintColor = UIColorFromRGB(0x8F918B);
|
|
|
|
if (@available(iOS 13.0, *)) {
|
|
segmentedControl.backgroundColor = UIColorFromLightDarkRGB(0xe7e6e7, 0x303030);
|
|
segmentedControl.selectedSegmentTintColor = UIColorFromLightDarkRGB(0xffffff, 0x6f6f75);
|
|
|
|
[self updateTextAttributesForSegmentedControl:segmentedControl forState:UIControlStateNormal foregroundColor:UIColorFromLightDarkRGB(0x909090, 0xaaaaaa)];
|
|
[self updateTextAttributesForSegmentedControl:segmentedControl forState:UIControlStateSelected foregroundColor:UIColorFromLightDarkRGB(0x0, 0xffffff)];
|
|
}
|
|
}
|
|
|
|
- (void)updateThemeSegmentedControl:(UISegmentedControl *)segmentedControl {
|
|
segmentedControl.tintColor = [UIColor clearColor];
|
|
|
|
if (@available(iOS 13.0, *)) {
|
|
segmentedControl.backgroundColor = [UIColor clearColor];
|
|
segmentedControl.selectedSegmentTintColor = [UIColor clearColor];
|
|
}
|
|
}
|
|
|
|
- (void)debugColor:(NSInteger)rgbValue {
|
|
static NSMutableSet *colors = nil;
|
|
|
|
if (!colors) {
|
|
colors = [NSMutableSet set];
|
|
}
|
|
|
|
[colors addObject:[NSString stringWithFormat:@"0x%06lX", (long)rgbValue]];
|
|
|
|
NSLog(@"all unique colors: %@", [[colors allObjects] sortedArrayUsingSelector:@selector(compare:)]); // log
|
|
}
|
|
|
|
- (void)prepareForWindow:(UIWindow *)window {
|
|
[self autoChangeTheme];
|
|
[self setupTheme];
|
|
[self addThemeGestureRecognizerToView:window];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenBrightnessChangedNotification:) name:UIScreenBrightnessDidChangeNotification object:nil];
|
|
}
|
|
|
|
- (void)setupTheme {
|
|
[UINavigationBar appearance].tintColor = UIColorFromLightSepiaMediumDarkRGB(0x0, 0x0, 0x9a8f73, 0x9a8f73);
|
|
[UINavigationBar appearance].barTintColor = UIColorFromLightSepiaMediumDarkRGB(0xE3E6E0, 0xFFFFC5, 0x333333, 0x222222);
|
|
[UINavigationBar appearance].backgroundColor = UIColorFromLightSepiaMediumDarkRGB(0xE3E6E0, 0xFFFFC5, 0x333333, 0x222222);
|
|
[UINavigationBar appearance].titleTextAttributes = @{NSForegroundColorAttributeName : UIColorFromLightSepiaMediumDarkRGB(0x8F918B, 0x8F918B, 0x8F918B, 0x8F918B)};
|
|
[UIToolbar appearance].barTintColor = UIColorFromLightSepiaMediumDarkRGB(0xE3E6E0, 0xFFFFC5, 0x4A4A4A, 0x222222);
|
|
[UISegmentedControl appearance].tintColor = UIColorFromLightSepiaMediumDarkRGB(0x8F918B, 0x8F918B, 0x8F918B, 0x8F918B);
|
|
|
|
UIBarStyle style = self.isDarkTheme ? UIBarStyleBlack : UIBarStyleDefault;
|
|
|
|
[UINavigationBar appearance].barStyle = style;
|
|
[UINavigationBar appearance].translucent = YES;
|
|
self.appDelegate.navigationController.navigationBar.barStyle = style;
|
|
|
|
[self.appDelegate.navigationController setNeedsStatusBarAppearanceUpdate];
|
|
}
|
|
|
|
- (void)updateTheme {
|
|
// Keep the dark & light themes in sync, so toggling uses the most recent themes for each
|
|
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
|
|
NSString *theme = self.theme;
|
|
NewsBlurAppDelegate *appDelegate = self.appDelegate;
|
|
|
|
if (self.isDarkTheme) {
|
|
[prefs setObject:theme forKey:@"theme_dark"];
|
|
} else {
|
|
[prefs setObject:theme forKey:@"theme_light"];
|
|
}
|
|
|
|
[self setupTheme];
|
|
|
|
[appDelegate.masterContainerViewController updateTheme];
|
|
[appDelegate.feedsViewController updateTheme];
|
|
[appDelegate.dashboardViewController updateTheme];
|
|
[appDelegate.feedDetailViewController updateTheme];
|
|
[appDelegate.storyPageControl updateTheme];
|
|
[appDelegate.originalStoryViewController updateTheme];
|
|
|
|
[self updatePreferencesTheme];
|
|
}
|
|
|
|
- (void)updatePreferencesTheme {
|
|
NewsBlurAppDelegate *appDelegate = self.appDelegate;
|
|
UIBarButtonItem *item = [appDelegate.preferencesViewController.navigationController.navigationBar.items.firstObject rightBarButtonItem];
|
|
|
|
item.tintColor = UIColorFromRGB(0x333333);
|
|
appDelegate.preferencesViewController.navigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName : UIColorFromRGB(NEWSBLUR_BLACK_COLOR)};
|
|
appDelegate.preferencesViewController.navigationController.navigationBar.tintColor = UIColorFromRGB(NEWSBLUR_BLACK_COLOR);
|
|
appDelegate.preferencesViewController.navigationController.navigationBar.barTintColor = UIColorFromRGB(0xE3E6E0);
|
|
}
|
|
|
|
- (BOOL)autoChangeTheme {
|
|
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
|
|
|
|
if (![prefs boolForKey:@"theme_auto_toggle"]) {
|
|
return NO;
|
|
}
|
|
|
|
CGFloat screenBrightness = [UIScreen mainScreen].brightness;
|
|
CGFloat themeBrightness = [prefs floatForKey:@"theme_auto_brightness"];
|
|
BOOL wantDark = (screenBrightness < themeBrightness);
|
|
BOOL isDark = self.isDarkTheme;
|
|
|
|
if (wantDark != isDark) {
|
|
NSString *theme = nil;
|
|
|
|
if (wantDark) {
|
|
theme = [prefs objectForKey:@"theme_dark"];
|
|
} else {
|
|
theme = [prefs objectForKey:@"theme_light"];
|
|
}
|
|
|
|
NSLog(@"Automatically changing to theme: %@", self.themeDisplayName); // log
|
|
|
|
self.theme = theme;
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)screenBrightnessChangedNotification:(NSNotification *)note {
|
|
if ([self autoChangeTheme]) {
|
|
[self updateTheme];
|
|
}
|
|
}
|
|
|
|
- (UIGestureRecognizer *)addThemeGestureRecognizerToView:(UIView *)view {
|
|
UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleThemeGesture:)];
|
|
|
|
recognizer.minimumNumberOfTouches = 2;
|
|
recognizer.maximumNumberOfTouches = 2;
|
|
|
|
[view addGestureRecognizer:recognizer];
|
|
|
|
return recognizer;
|
|
}
|
|
|
|
- (void)handleThemeGesture:(UIPanGestureRecognizer *)recognizer {
|
|
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
|
|
|
|
if (recognizer.state != UIGestureRecognizerStateChanged || [prefs boolForKey:@"theme_auto_toggle"] || ![prefs boolForKey:@"theme_gesture"]) {
|
|
self.justToggledViaGesture = NO;
|
|
return;
|
|
}
|
|
|
|
CGPoint translation = [recognizer translationInView:recognizer.view];
|
|
|
|
if (self.justToggledViaGesture || fabs(translation.x) > 50.0 || fabs(translation.y) < 50.0) {
|
|
return;
|
|
}
|
|
|
|
BOOL isUpward = translation.y > 0.0;
|
|
NSString *isTheme = self.theme;
|
|
NSString *wantTheme = nil;
|
|
|
|
if (isUpward) {
|
|
wantTheme = [prefs objectForKey:@"theme_dark"];
|
|
} else {
|
|
wantTheme = [prefs objectForKey:@"theme_light"];
|
|
}
|
|
|
|
if ([isTheme isEqualToString:wantTheme]) {
|
|
wantTheme = [self similarTheme];
|
|
}
|
|
|
|
self.theme = wantTheme;
|
|
self.justToggledViaGesture = YES;
|
|
|
|
NSLog(@"Swiped to theme: %@", self.themeDisplayName); // log
|
|
|
|
[self updateTheme];
|
|
|
|
// Play a click sound, like a light switch; might want to use a custom sound instead?
|
|
AudioServicesPlaySystemSound(1105);
|
|
}
|
|
|
|
- (void)updateForSystemAppearance {
|
|
if (@available(iOS 12.0, *)) {
|
|
BOOL isDark = self.appDelegate.window.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
|
|
|
|
[self systemAppearanceDidChange:isDark];
|
|
}
|
|
}
|
|
|
|
- (void)systemAppearanceDidChange:(BOOL)isDark {
|
|
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
|
|
NSString *wantTheme = nil;
|
|
|
|
if (![prefs boolForKey:@"theme_follow_system"]) {
|
|
return;
|
|
}
|
|
|
|
if (isDark) {
|
|
wantTheme = [prefs objectForKey:@"theme_dark"];
|
|
} else {
|
|
wantTheme = [prefs objectForKey:@"theme_light"];
|
|
}
|
|
|
|
if (self.theme != wantTheme) {
|
|
[self reallySetTheme:wantTheme];
|
|
|
|
NSLog(@"System changed to theme: %@", self.themeDisplayName); // log
|
|
}
|
|
}
|
|
|
|
@end
|
|
|