mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
Merge branch 'dejal'
* dejal: #1162 (widget) #1162 (widget) #1162 (widget) #1162 (widget) #1162 (widget) #1268 (appearance changes)
This commit is contained in:
commit
25a623c5d1
13 changed files with 345 additions and 97 deletions
|
@ -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];
|
||||
}
|
||||
}];
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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"];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ extern NSString * const ThemeStyleDark;
|
|||
- (void)updatePreferencesTheme;
|
||||
- (BOOL)autoChangeTheme;
|
||||
- (UIGestureRecognizer *)addThemeGestureRecognizerToView:(UIView *)view;
|
||||
- (void)updateForSystemAppearance;
|
||||
- (void)systemAppearanceDidChange:(BOOL)isDark;
|
||||
|
||||
@end
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
48
clients/ios/Widget Extension/WidgetBarView.swift
Normal file
48
clients/ios/Widget Extension/WidgetBarView.swift
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
98
clients/ios/Widget Extension/WidgetFeed.swift
Normal file
98
clients/ios/Widget Extension/WidgetFeed.swift
Normal 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))"
|
||||
}
|
||||
}
|
|
@ -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!
|
||||
|
|
|
@ -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="<date>" 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"><title>
|
||||
(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"><title>
|
||||
</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="<author>" 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="<feed>" 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="<feed>" 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"><content>
|
||||
(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"><content>
|
||||
</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"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue