Merge branch 'dejal'

* dejal:
  #1162 (widget)
  #1162 (widget)
  #1162 (widget)
  #1162 (widget)
  #1162 (widget)
  #1268 (appearance changes)
This commit is contained in:
Samuel Clay 2019-12-24 10:05:22 -05:00
commit 25a623c5d1
13 changed files with 345 additions and 97 deletions

View file

@ -32,7 +32,7 @@ static const CGFloat kFolderTitleHeight = 36.0;
@property (nonatomic) BOOL flat;
@property (nonatomic, readonly) NewsBlurAppDelegate *appDelegate;
@property (nonatomic, strong) NSUserDefaults *groupDefaults;
@property (nonatomic, readonly) NSDictionary *widgetFeeds;
@property (nonatomic, readonly) NSArray *widgetFeeds;
@end
@ -374,36 +374,47 @@ static const CGFloat kFolderTitleHeight = 36.0;
[self updateTitle];
}
- (NSDictionary *)widgetFeeds {
NSMutableDictionary *feeds = [self.groupDefaults objectForKey:@"widget:feeds"];
- (NSArray *)widgetFeeds {
NSMutableArray *feeds = [self.groupDefaults objectForKey:@"widget:feeds_array"];
if (feeds == nil) {
feeds = [NSMutableDictionary dictionary];
feeds = [NSMutableArray array];
[self enumerateAllRowsUsingBlock:^(NSIndexPath *indexPath, FeedChooserItem *item) {
[feeds setObject:item.title forKey:item.identifierString];
[feeds addObject:[self widgetFeedForItem:item]];
}];
[self.groupDefaults setObject:feeds forKey:@"widget:feeds"];
[self.groupDefaults setObject:feeds forKey:@"widget:feeds_array"];
}
return feeds;
}
- (BOOL)widgetIncludesFeed:(NSString *)feedId {
return [self.widgetFeeds objectForKey:feedId] != nil;
- (NSDictionary *)widgetFeedForItem:(FeedChooserItem *)item {
return @{@"id" : item.identifierString, @"feed_title" : item.title, @"favicon_fade" : item.info[@"favicon_fade"], @"favicon_color" : item.info[@"favicon_color"]};
}
- (NSInteger)widgetIndexOfFeed:(NSString *)feedId {
return [self.widgetFeeds indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return [obj[@"id"] isEqualToString:feedId];
}];
}
- (void)setWidgetIncludes:(BOOL)include item:(FeedChooserItem *)item {
NSMutableDictionary *feeds = [self.widgetFeeds mutableCopy];
NSMutableArray *feeds = [self.widgetFeeds mutableCopy];
NSInteger feedIndex = [self widgetIndexOfFeed:item.identifierString];
if (include) {
[feeds setObject:item.title forKey:item.identifierString];
if (feedIndex == NSNotFound) {
[feeds addObject:[self widgetFeedForItem:item]];
}
} else {
[feeds removeObjectForKey:item.identifierString];
if (feedIndex != NSNotFound) {
[feeds removeObjectAtIndex:feedIndex];
}
}
[self.groupDefaults setObject:feeds forKey:@"widget:feeds"];
[self.groupDefaults setObject:feeds forKey:@"widget:feeds_array"];
}
- (void)setWidgetIncludes:(BOOL)include itemForIndexPath:(NSIndexPath *)indexPath {
@ -618,11 +629,9 @@ static const CGFloat kFolderTitleHeight = 36.0;
- (void)updateSelectedWidgets {
NSMutableArray *identifiers = [NSMutableArray array];
NSDictionary *feeds = self.widgetFeeds;
NSArray *feedIds = feeds.allKeys;
[self enumerateAllRowsUsingBlock:^(NSIndexPath *indexPath, FeedChooserItem *item) {
if ([feedIds containsObject:item.identifierString]) {
if ([self widgetIndexOfFeed:item.identifierString] != NSNotFound) {
[identifiers addObject:item.identifier];
}
}];

View file

@ -572,7 +572,15 @@
}
[self popToRoot];
[self loadFeed:feedId withStory:storyHash animated:NO];
self.inFindingStoryMode = YES;
[storiesCollection reset];
storiesCollection.isRiverView = YES;
self.tryFeedStoryId = storyHash;
storiesCollection.activeFolder = @"everything";
[self loadRiverFeedDetailView:self.feedDetailViewController withFolder:storiesCollection.activeFolder];
return YES;
}
@ -819,6 +827,7 @@
BOOL theme_follow_system = [[NSUserDefaults standardUserDefaults] boolForKey:@"theme_follow_system"];
if (theme_follow_system) {
[hiddenSet addObjectsFromArray:@[@"theme_auto_toggle", @"theme_auto_brightness", @"theme_style", @"theme_gesture"]];
[[ThemeManager themeManager] updateForSystemAppearance];
}
}
BOOL theme_auto_toggle = [[NSUserDefaults standardUserDefaults] boolForKey:@"theme_auto_toggle"];

View file

@ -1162,21 +1162,25 @@ static NSArray<NSString *> *NewsBlurTopSectionNames;
}
- (void)validateWidgetFeedsForGroupDefaults:(NSUserDefaults *)groupDefaults usingResults:(NSDictionary *)results {
NSMutableDictionary *feeds = [groupDefaults objectForKey:@"widget:feeds"];
NSMutableArray *feeds = [groupDefaults objectForKey:@"widget:feeds_array"];
if (feeds == nil) {
feeds = [NSMutableDictionary dictionary];
feeds = [NSMutableArray array];
NSDictionary *resultsFeeds = results[@"feeds"];
[resultsFeeds enumerateKeysAndObjectsUsingBlock:^(id key, NSDictionary *obj, BOOL *stop) {
NSString *identifier = [NSString stringWithFormat:@"%@", key];
NSString *title = obj[@"feed_title"];
NSString *fade = obj[@"favicon_fade"];
NSString *color = obj[@"favicon_color"];
feeds[identifier] = title;
NSDictionary *feed = @{@"id" : identifier, @"feed_title" : title, @"favicon_fade": fade, @"favicon_color" : color};
[feeds addObject:feed];
}];
[groupDefaults setObject:feeds forKey:@"widget:feeds"];
[groupDefaults setObject:feeds forKey:@"widget:feeds_array"];
}
}

View file

@ -50,6 +50,7 @@ extern NSString * const ThemeStyleDark;
- (void)updatePreferencesTheme;
- (BOOL)autoChangeTheme;
- (UIGestureRecognizer *)addThemeGestureRecognizerToView:(UIView *)view;
- (void)updateForSystemAppearance;
- (void)systemAppearanceDidChange:(BOOL)isDark;
@end

View file

@ -53,6 +53,15 @@ NSString * const ThemeStyleDark = @"dark";
}
- (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];
@ -385,6 +394,14 @@ NSString * const ThemeStyleDark = @"dark";
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;
@ -400,11 +417,9 @@ NSString * const ThemeStyleDark = @"dark";
}
if (self.theme != wantTheme) {
self.theme = wantTheme;
[self reallySetTheme:wantTheme];
NSLog(@"System changed to theme: %@", self.themeDisplayName); // log
[self updateTheme];
}
}

View file

@ -50,9 +50,11 @@
176129611C630AEB00702FE4 /* mute_feed_off@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1761295D1C630AEB00702FE4 /* mute_feed_off@2x.png */; };
176129621C630AEB00702FE4 /* mute_feed_on.png in Resources */ = {isa = PBXBuildFile; fileRef = 1761295E1C630AEB00702FE4 /* mute_feed_on.png */; };
176129631C630AEB00702FE4 /* mute_feed_on@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1761295F1C630AEB00702FE4 /* mute_feed_on@2x.png */; };
1763E2A123B1BCC900BA080C /* WidgetFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1763E2A023B1BCC900BA080C /* WidgetFeed.swift */; };
1763E2A323B1CEB600BA080C /* WidgetBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1763E2A223B1CEB600BA080C /* WidgetBarView.swift */; };
177551D5238E228A00E27818 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 177551D4238E228A00E27818 /* NotificationCenter.framework */; };
177551DB238E228A00E27818 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 177551D9238E228A00E27818 /* MainInterface.storyboard */; };
177551DF238E228A00E27818 /* NewsBlur.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 177551D3238E228A00E27818 /* NewsBlur.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
177551DF238E228A00E27818 /* NewsBlur Latest.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 177551D3238E228A00E27818 /* NewsBlur Latest.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
17813FB723AC6E450057FB16 /* WidgetErrorTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 17CE3F0523AC529B003152EF /* WidgetErrorTableViewCell.xib */; };
17876B9E1C9911D40055DD15 /* g_icn_folder_rss_sm.png in Resources */ = {isa = PBXBuildFile; fileRef = 17876B9A1C9911D40055DD15 /* g_icn_folder_rss_sm.png */; };
17876B9F1C9911D40055DD15 /* g_icn_folder_rss_sm@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17876B9B1C9911D40055DD15 /* g_icn_folder_rss_sm@2x.png */; };
@ -651,7 +653,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
177551DF238E228A00E27818 /* NewsBlur.appex in Embed App Extensions */,
177551DF238E228A00E27818 /* NewsBlur Latest.appex in Embed App Extensions */,
1749391B1C251BFE003D98AA /* Share Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
@ -713,7 +715,9 @@
1761295D1C630AEB00702FE4 /* mute_feed_off@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mute_feed_off@2x.png"; sourceTree = "<group>"; };
1761295E1C630AEB00702FE4 /* mute_feed_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mute_feed_on.png; sourceTree = "<group>"; };
1761295F1C630AEB00702FE4 /* mute_feed_on@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mute_feed_on@2x.png"; sourceTree = "<group>"; };
177551D3238E228A00E27818 /* NewsBlur.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NewsBlur.appex; sourceTree = BUILT_PRODUCTS_DIR; };
1763E2A023B1BCC900BA080C /* WidgetFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetFeed.swift; sourceTree = "<group>"; };
1763E2A223B1CEB600BA080C /* WidgetBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBarView.swift; sourceTree = "<group>"; };
177551D3238E228A00E27818 /* NewsBlur Latest.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NewsBlur Latest.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
177551D4238E228A00E27818 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; };
177551DA238E228A00E27818 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
177551DC238E228A00E27818 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -1622,6 +1626,8 @@
17E86ED3238E444B00863EC8 /* WidgetTableViewCell.xib */,
17CE3F0623AC529E003152EF /* WidgetErrorTableViewCell.swift */,
17CE3F0523AC529B003152EF /* WidgetErrorTableViewCell.xib */,
1763E2A223B1CEB600BA080C /* WidgetBarView.swift */,
1763E2A023B1BCC900BA080C /* WidgetFeed.swift */,
17042DB82391D68A001BCD32 /* WidgetStory.swift */,
17042DBA23922A4D001BCD32 /* WidgetLoader.swift */,
177551D9238E228A00E27818 /* MainInterface.storyboard */,
@ -1638,7 +1644,7 @@
1D6058910D05DD3D006BFB54 /* NewsBlur.app */,
174939101C251BFE003D98AA /* Share Extension.appex */,
FF8A94971DE3BB77000A4C31 /* Story Notification Service Extension.appex */,
177551D3238E228A00E27818 /* NewsBlur.appex */,
177551D3238E228A00E27818 /* NewsBlur Latest.appex */,
);
name = Products;
sourceTree = "<group>";
@ -2766,7 +2772,7 @@
);
name = "Widget Extension";
productName = widget;
productReference = 177551D3238E228A00E27818 /* NewsBlur.appex */;
productReference = 177551D3238E228A00E27818 /* NewsBlur Latest.appex */;
productType = "com.apple.product-type.app-extension";
};
1D6058900D05DD3D006BFB54 /* NewsBlur */ = {
@ -3357,9 +3363,11 @@
buildActionMask = 2147483647;
files = (
17CE3F0723AC529E003152EF /* WidgetErrorTableViewCell.swift in Sources */,
1763E2A123B1BCC900BA080C /* WidgetFeed.swift in Sources */,
17E86ED6238E444B00863EC8 /* WidgetTableViewCell.swift in Sources */,
17F363F2238E417300D5379D /* WidgetExtensionViewController.swift in Sources */,
17042DB92391D68A001BCD32 /* WidgetStory.swift in Sources */,
1763E2A323B1CEB600BA080C /* WidgetBarView.swift in Sources */,
17042DBB23922A4D001BCD32 /* WidgetLoader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -3729,7 +3737,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget;
PRODUCT_NAME = NewsBlur;
PRODUCT_NAME = "NewsBlur Latest";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -3770,7 +3778,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget;
PRODUCT_NAME = NewsBlur;
PRODUCT_NAME = "NewsBlur Latest";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View file

@ -16,7 +16,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "177551D2238E228A00E27818"
BuildableName = "NewsBlur.appex"
BuildableName = "NewsBlur Latest.appex"
BlueprintName = "Widget Extension"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>
@ -60,7 +60,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "177551D2238E228A00E27818"
BuildableName = "NewsBlur.appex"
BuildableName = "NewsBlur Latest.appex"
BlueprintName = "Widget Extension"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>

View file

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<string>NewsBlur</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

View file

@ -0,0 +1,48 @@
//
// WidgetBarView.swift
// Widget Extension
//
// Created by David Sinclair on 2019-12-23.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
/// Color bars at the left of the feed cell.
class BarView: UIView {
/// The left bar color.
var leftColor: UIColor?
/// The right bar color.
var rightColor: UIColor?
override func draw(_ rect: CGRect) {
guard let leftColor = leftColor, let rightColor = rightColor, let context = UIGraphicsGetCurrentContext() else {
return
}
let height = bounds.height
context.setStrokeColor(leftColor.cgColor)
context.setLineWidth(4)
context.beginPath()
context.move(to: CGPoint(x: 2, y: 0))
context.addLine(to: CGPoint(x: 2, y: height))
context.strokePath()
context.setStrokeColor(rightColor.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 6, y: 0))
context.addLine(to: CGPoint(x: 6, y: height))
context.strokePath()
let isDark = traitCollection.userInterfaceStyle == .dark
context.setStrokeColor(isDark ? UIColor.black.cgColor : UIColor.white.cgColor)
context.setLineWidth(1)
context.beginPath()
context.move(to: CGPoint(x: 0, y: 0.5))
context.addLine(to: CGPoint(x: bounds.width, y: 0.5))
context.strokePath()
}
}

View file

@ -23,11 +23,8 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
/// The secret token for authentication.
var token: String?
/// Dictionary of feed IDs and names.
typealias FeedsDictionary = [String : String]
/// A dictionary of feed IDs and names to fetch.
var feeds = FeedsDictionary()
/// An array of feeds to load.
var feeds = [Feed]()
/// Loaded stories.
var stories = [Story]()
@ -35,11 +32,22 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
/// An error to display instead of the stories, or `nil` if the stories should be displayed.
var error: WidgetError?
/// Paragraph style for title and content labels.
lazy var paragraphStyle: NSParagraphStyle = {
let paragraph = NSMutableParagraphStyle()
paragraph.lineBreakMode = .byTruncatingTail
paragraph.alignment = .left
paragraph.lineHeightMultiple = 0.95
return paragraph
}()
struct Constant {
static let group = "group.com.newsblur.NewsBlur-Group"
static let token = "share:token"
static let host = "share:host"
static let feeds = "widget:feeds"
static let feeds = "widget:feeds_array"
static let widgetFolder = "Widget"
static let storiesFilename = "Stories.json"
static let imageExtension = "png"
@ -105,7 +113,8 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
return
}
let combinedFeeds = feeds.keys.joined(separator: "&f=")
let feedIds = feeds.map { $0.id }
let combinedFeeds = feedIds.joined(separator: "&f=")
guard let url = hostURL(with: "/reader/river_stories/?include_hidden=false&page=1&infrequent=false&order=newest&read_filter=unread&limit=\(Constant.limit)&f=\(combinedFeeds)") else {
completionHandler(.failed)
@ -136,29 +145,7 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let updatedVisibleCellCount = numberOfTableRowsToDisplay
let currentVisibleCellCount = self.tableView.visibleCells.count
let cellCountDifference = updatedVisibleCellCount - currentVisibleCellCount
// If the number of visible cells has changed, animate them in/out along with the resize animation.
if cellCountDifference != 0 {
coordinator.animate(alongsideTransition: { [unowned self] (UIViewControllerTransitionCoordinatorContext) in
self.tableView.performBatchUpdates({ [unowned self] in
// Build an array of IndexPath objects representing the rows to be inserted or deleted.
let range = (1...abs(cellCountDifference))
let indexPaths = range.map({ (index) -> IndexPath in
return IndexPath(row: index, section: 0)
})
// Animate the insertion or deletion of the rows.
if cellCountDifference > 0 {
self.tableView.insertRows(at: indexPaths, with: .fade)
} else {
self.tableView.deleteRows(at: indexPaths, with: .fade)
}
}, completion: nil)
}, completion: nil)
}
tableView.reloadData()
}
// MARK: - Table view data source
@ -179,7 +166,7 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
case .notLoggedIn:
cell.errorLabel.text = "Please log in to NewsBlur"
case .loading:
cell.errorLabel.text = "On its way..."
cell.errorLabel.text = "Tap to set up in NewsBlur"
case .noFeeds:
cell.errorLabel.text = "Please choose sites to show"
case .noStories:
@ -195,6 +182,19 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
}
let story = stories[indexPath.row]
let feed = feeds.first(where: { $0.id == story.feed })
let baseDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .caption1)
let sizedDescriptor = baseDescriptor.withSize(13)
let boldDescriptor = sizedDescriptor.withSymbolicTraits(.traitBold) ?? sizedDescriptor
let titleFont = UIFont(descriptor: boldDescriptor, size: sizedDescriptor.pointSize)
let titleColor = UIColor.label
let contentFont = UIFont(descriptor: sizedDescriptor, size: 0)
let contentColor = UIColor.secondaryLabel
cell.barView.leftColor = feed?.leftColor
cell.barView.rightColor = feed?.rightColor
cell.barView.setNeedsDisplay()
cell.feedImageView.image = nil
@ -205,16 +205,19 @@ class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
}
}
if let name = feeds[story.feed] {
cell.feedLabel.text = name
if let title = feed?.title {
cell.feedLabel.text = title
} else {
cell.feedLabel.text = ""
}
cell.titleLabel.text = cleaned(story.title)
cell.contentLabel.text = cleaned(story.content)
cell.feedLabel.textColor = UIColor.secondaryLabel
cell.titleLabel.attributedText = attributed(story.title, with: titleFont, color: titleColor)
cell.contentLabel.attributedText = attributed(story.content, with: contentFont, color: contentColor)
cell.authorLabel.text = cleaned(story.author).uppercased()
cell.authorLabel.textColor = UIColor.tertiaryLabel
cell.dateLabel.text = story.date
cell.dateLabel.textColor = UIColor.secondaryLabel
cell.thumbnailImageView.image = nil
storyImage(for: story.id, imageURL: story.imageURL) { (image, id) in
@ -271,13 +274,18 @@ private extension WidgetExtensionViewController {
func cleaned(_ string: String) -> String {
let clean = string.prefix(1000).replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
.replacingOccurrences(of: "&[^;]+;", with: " ", options: .regularExpression, range: nil)
.replacingOccurrences(of: "<[^>]+>|&[^;]+;", with: " ", options: .regularExpression, range: nil)
.trimmingCharacters(in: .whitespaces)
return clean.isEmpty ? " " : clean
}
func attributed(_ string: String, with font: UIFont, color: UIColor) -> NSAttributedString {
let attributes: [NSAttributedString.Key : Any] = [.font : font, .foregroundColor: color, .paragraphStyle: paragraphStyle]
return NSAttributedString(string: cleaned(string), attributes: attributes)
}
func hostURL(with path: String) -> URL? {
guard let host = host else {
return nil
@ -330,13 +338,16 @@ private extension WidgetExtensionViewController {
error = .noStories
}
// Keep a local copy, since the property will be cleared before the async closure is called.
let localCompletion = widgetCompletion
DispatchQueue.main.async {
self.extensionContext?.widgetLargestAvailableDisplayMode = self.error == nil ? .expanded : .compact
self.tableView.reloadData()
self.tableView.setNeedsDisplay()
self.widgetCompletion?(.newData)
localCompletion?(.newData)
}
}
@ -361,6 +372,7 @@ private extension WidgetExtensionViewController {
}
func loadCachedStories() {
feeds = []
stories = []
guard let defaults = UserDefaults.init(suiteName: Constant.group) else {
@ -370,8 +382,8 @@ private extension WidgetExtensionViewController {
host = defaults.string(forKey: Constant.host)
token = defaults.string(forKey: Constant.token)
if let dict = defaults.dictionary(forKey: Constant.feeds) as? FeedsDictionary {
feeds = dict
if let array = defaults.array(forKey: Constant.feeds) as? [Feed.Dictionary] {
feeds = array.map { Feed(from: $0) }
}
guard let url = storiesURL else {
@ -530,15 +542,25 @@ private extension WidgetExtensionViewController {
}
func scale(image: UIImage) -> UIImage {
guard image.size.width > Constant.storyImageSize || image.size.height > Constant.storyImageSize else {
let oldSize = image.size
guard oldSize.width > Constant.storyImageSize || oldSize.height > Constant.storyImageSize else {
return image
}
let size = CGSize(width: Constant.storyImageSize, height: Constant.storyImageSize)
let scale: CGFloat
UIGraphicsBeginImageContextWithOptions(size, true, 1)
if oldSize.width < oldSize.height {
scale = Constant.storyImageSize / oldSize.width
} else {
scale = Constant.storyImageSize / oldSize.height
}
image.draw(in: CGRect(origin: .zero, size: size))
let newSize = CGSize(width: oldSize.width * scale, height: oldSize.height * scale)
UIGraphicsBeginImageContextWithOptions(newSize, true, 1)
image.draw(in: CGRect(origin: .zero, size: newSize))
defer {
UIGraphicsEndImageContext()

View file

@ -0,0 +1,98 @@
//
// WidgetFeed.swift
// Widget Extension
//
// Created by David Sinclair on 2019-12-23.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
/// A feed to display in the widget.
struct Feed: Identifiable {
/// The feed ID.
let id: String
/// The name of the feed.
let title: String
/// The left bar color.
let leftColor: UIColor
/// The right bar color.
let rightColor: UIColor
/// Keys for the dictionary representation.
struct DictionaryKeys {
static let id = "id"
static let title = "feed_title"
static let leftColor = "favicon_color"
static let rightColor = "favicon_fade"
}
/// A dictionary representation of the feed.
typealias Dictionary = [String : Any]
/// Initializer from a dictionary.
///
/// - Parameter dictionary: Dictionary representation.
init(from dictionary: Dictionary) {
id = dictionary[DictionaryKeys.id] as? String ?? ""
title = dictionary[DictionaryKeys.title] as? String ?? ""
if let fadeHex = dictionary[DictionaryKeys.leftColor] as? String {
leftColor = Self.from(hexString: fadeHex)
} else {
leftColor = Self.from(hexString: "707070")
}
if let otherHex = dictionary[DictionaryKeys.rightColor] as? String {
rightColor = Self.from(hexString: otherHex)
} else {
rightColor = Self.from(hexString: "505050")
}
}
/// Given a hex string, returns the corresponding color.
///
/// - Parameter hexString: The hex string.
/// - Returns: The color equivalent.
static func from(hexString: String) -> UIColor {
var red: Double = 0
var green: Double = 0
var blue: Double = 0
var alpha: Double = 1
let length = hexString.count
let scanner = Scanner(string: hexString)
var hex: UInt64 = 0
scanner.scanHexInt64(&hex)
if length == 8 {
red = Double((hex & 0xFF000000) >> 24) / 255
green = Double((hex & 0x00FF0000) >> 16) / 255
blue = Double((hex & 0x0000FF00) >> 8) / 255
alpha = Double( hex & 0x000000FF) / 255
} else if length == 6 {
red = Double((hex & 0xFF0000) >> 16) / 255
green = Double((hex & 0x00FF00) >> 8) / 255
blue = Double( hex & 0x0000FF) / 255
}
print("Reading color from '\(hexString)': red: \(red), green: \(green), blue: \(blue), alpha: \(alpha)")
return UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha))
}
}
extension Feed: Equatable {
static func ==(lhs: Feed, rhs: Feed) -> Bool {
return lhs.id == rhs.id
}
}
extension Feed: CustomStringConvertible {
var description: String {
return "Feed \(title) (\(id))"
}
}

View file

@ -12,6 +12,7 @@ class WidgetTableViewCell: UITableViewCell {
/// The reuse identifier for this table view cell.
static let reuseIdentifier = "WidgetTableViewCell"
@IBOutlet var barView: BarView!
@IBOutlet var feedImageView: UIImageView!
@IBOutlet var feedLabel: UILabel!
@IBOutlet var titleLabel: UILabel!

View file

@ -16,21 +16,39 @@
<rect key="frame" x="0.0" y="0.0" width="320" height="110"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="h4R-ku-5LC" customClass="BarView" customModule="NewsBlur_Latest" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="8" height="110"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="width" constant="8" id="ozb-aL-EwP"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="&lt;date&gt;" textAlignment="natural" lineBreakMode="headTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="02U-BY-b7a">
<rect key="frame" x="265.5" y="91" width="34.5" height="12"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
<color key="textColor" systemColor="tertiaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" horizontalCompressionResistancePriority="745" verticalCompressionResistancePriority="745" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="232" translatesAutoresizingMaskIntoConstraints="NO" id="I7A-fd-pwi">
<rect key="frame" x="20" y="27" width="208" height="33.5"/>
<string key="text">&lt;title&gt;
(2 lines)</string>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
<nil key="textColor"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" horizontalCompressionResistancePriority="745" verticalCompressionResistancePriority="745" usesAttributedText="YES" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="232" translatesAutoresizingMaskIntoConstraints="NO" id="I7A-fd-pwi">
<rect key="frame" x="20" y="29" width="208" height="33"/>
<attributedString key="attributedText">
<fragment>
<string key="content">&lt;title&gt;
</string>
<attributes>
<font key="NSFont" metaFont="menu" size="14"/>
</attributes>
</fragment>
<fragment content="(2 lines)">
<attributes>
<font key="NSFont" metaFont="menu" size="14"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="truncatingTail" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QWb-zc-x4f">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QWb-zc-x4f">
<rect key="frame" x="236" y="23" width="64" height="64"/>
<constraints>
<constraint firstAttribute="width" constant="64" id="GrS-OY-t9T"/>
@ -38,14 +56,14 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" text="&lt;author&gt;" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Wai-eu-jJp">
<rect key="frame" x="20" y="91" width="44.5" height="12"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
<color key="textColor" systemColor="tertiaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
<rect key="frame" x="20" y="91" width="48.5" height="12"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="10"/>
<color key="textColor" systemColor="quaternaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.17999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="&lt;feed&gt;" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pv8-bs-3wb">
<rect key="frame" x="44" y="7" width="256" height="12"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="&lt;feed&gt;" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="256" translatesAutoresizingMaskIntoConstraints="NO" id="pv8-bs-3wb">
<rect key="frame" x="44" y="5" width="256" height="16"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="13"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
@ -56,19 +74,31 @@
<constraint firstAttribute="width" constant="16" id="qzQ-Ql-xKX"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="232" translatesAutoresizingMaskIntoConstraints="NO" id="YTq-d7-IZe">
<rect key="frame" x="20" y="60.5" width="208" height="30.5"/>
<string key="text">&lt;content&gt;
(2 lines)</string>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="12"/>
<nil key="textColor"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" usesAttributedText="YES" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="232" translatesAutoresizingMaskIntoConstraints="NO" id="YTq-d7-IZe">
<rect key="frame" x="20" y="62" width="208" height="29"/>
<attributedString key="attributedText">
<fragment>
<string key="content">&lt;content&gt;
</string>
<attributes>
<font key="NSFont" metaFont="cellTitle"/>
</attributes>
</fragment>
<fragment content="(2 lines)">
<attributes>
<font key="NSFont" metaFont="cellTitle"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="truncatingTail" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="QWb-zc-x4f" secondAttribute="trailing" constant="20" symbolic="YES" id="0AJ-OQ-qEF"/>
<constraint firstItem="I7A-fd-pwi" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="20" symbolic="YES" id="1Oj-8N-Kbm"/>
<constraint firstAttribute="trailing" relation="lessThanOrEqual" secondItem="pv8-bs-3wb" secondAttribute="trailing" constant="20" symbolic="YES" id="510-Ep-Tcu"/>
<constraint firstAttribute="bottom" secondItem="h4R-ku-5LC" secondAttribute="bottom" id="4ER-Ya-oZs"/>
<constraint firstAttribute="trailing" secondItem="pv8-bs-3wb" secondAttribute="trailing" constant="20" symbolic="YES" id="510-Ep-Tcu"/>
<constraint firstItem="YTq-d7-IZe" firstAttribute="top" secondItem="I7A-fd-pwi" secondAttribute="bottom" id="74T-Ba-a26"/>
<constraint firstItem="el3-VK-r6t" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="5" id="BHE-Ve-mwT"/>
<constraint firstItem="QWb-zc-x4f" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="Cbg-Kp-Sw2"/>
@ -79,10 +109,12 @@
<constraint firstItem="pv8-bs-3wb" firstAttribute="leading" secondItem="el3-VK-r6t" secondAttribute="trailing" constant="8" symbolic="YES" id="SD9-74-HqO"/>
<constraint firstAttribute="trailing" secondItem="02U-BY-b7a" secondAttribute="trailing" constant="20" symbolic="YES" id="VzJ-zb-5QT"/>
<constraint firstItem="QWb-zc-x4f" firstAttribute="leading" secondItem="I7A-fd-pwi" secondAttribute="trailing" constant="8" symbolic="YES" id="Vzq-9x-tFf"/>
<constraint firstItem="h4R-ku-5LC" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="Wyg-vr-ICY"/>
<constraint firstItem="02U-BY-b7a" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Wai-eu-jJp" secondAttribute="trailing" constant="8" symbolic="YES" id="Yjh-mv-hS2"/>
<constraint firstItem="Wai-eu-jJp" firstAttribute="top" secondItem="YTq-d7-IZe" secondAttribute="bottom" id="ZFP-EZ-zMe"/>
<constraint firstItem="QWb-zc-x4f" firstAttribute="leading" secondItem="YTq-d7-IZe" secondAttribute="trailing" constant="8" symbolic="YES" id="a51-cq-Mpe"/>
<constraint firstItem="el3-VK-r6t" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="20" symbolic="YES" id="dYO-Ry-nTC"/>
<constraint firstItem="h4R-ku-5LC" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="ft5-GB-uUo"/>
<constraint firstItem="YTq-d7-IZe" firstAttribute="leading" secondItem="I7A-fd-pwi" secondAttribute="leading" id="iv8-ta-QKu"/>
<constraint firstItem="YTq-d7-IZe" firstAttribute="leading" secondItem="Wai-eu-jJp" secondAttribute="leading" id="knm-S7-Wnd"/>
</constraints>
@ -90,6 +122,7 @@
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="authorLabel" destination="Wai-eu-jJp" id="25m-9w-Q54"/>
<outlet property="barView" destination="h4R-ku-5LC" id="xpw-QX-aXE"/>
<outlet property="contentLabel" destination="YTq-d7-IZe" id="Kbg-hX-11B"/>
<outlet property="dateLabel" destination="02U-BY-b7a" id="aRG-cY-3pe"/>
<outlet property="feedImageView" destination="el3-VK-r6t" id="6PP-TY-Z1u"/>