Done!  See my comment in the issue for details.
This commit is contained in:
David Sinclair 2019-12-20 19:09:20 -08:00
parent e648f1e135
commit b314b6f289
24 changed files with 1556 additions and 65 deletions

View file

@ -12,7 +12,8 @@
typedef NS_ENUM(NSUInteger, FeedChooserOperation)
{
FeedChooserOperationMuteSites = 0,
FeedChooserOperationOrganizeSites = 1
FeedChooserOperationOrganizeSites = 1,
FeedChooserOperationWidgetSites = 2
};

View file

@ -31,6 +31,8 @@ static const CGFloat kFolderTitleHeight = 36.0;
@property (nonatomic) BOOL ascending;
@property (nonatomic) BOOL flat;
@property (nonatomic, readonly) NewsBlurAppDelegate *appDelegate;
@property (nonatomic, strong) NSUserDefaults *groupDefaults;
@property (nonatomic, readonly) NSDictionary *widgetFeeds;
@end
@ -45,10 +47,21 @@ static const CGFloat kFolderTitleHeight = 36.0;
appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
if (self.operation == FeedChooserOperationWidgetSites) {
self.groupDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.newsblur.NewsBlur-Group"];
}
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(done)];
self.optionsItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"nav_icn_settings.png"] style:UIBarButtonItemStylePlain target:self action:@selector(showOptionsMenu)];
if (self.operation == FeedChooserOperationOrganizeSites) {
if (self.operation == FeedChooserOperationMuteSites) {
UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel)];
self.navigationItem.leftBarButtonItem = cancelItem;
self.navigationItem.rightBarButtonItems = @[doneItem, self.optionsItem];
self.tableView.editing = NO;
} else if (self.operation == FeedChooserOperationOrganizeSites) {
self.moveItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"menu_icn_move.png"] style:UIBarButtonItemStylePlain target:self action:@selector(showMoveMenu)];
self.deleteItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"menu_icn_delete.png"] style:UIBarButtonItemStylePlain target:self action:@selector(deleteFeeds)];
@ -57,12 +70,10 @@ static const CGFloat kFolderTitleHeight = 36.0;
self.tableView.editing = YES;
} else {
UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel)];
self.navigationItem.leftBarButtonItem = cancelItem;
self.navigationItem.leftBarButtonItems = nil;
self.navigationItem.rightBarButtonItems = @[doneItem, self.optionsItem];
self.tableView.editing = NO;
self.tableView.editing = YES;
}
self.tableView.backgroundColor = UIColorFromRGB(0xECEEEA);
@ -76,9 +87,13 @@ static const CGFloat kFolderTitleHeight = 36.0;
if (self.operation == FeedChooserOperationMuteSites) {
[self performGetInactiveFeeds];
} else if (self.operation == FeedChooserOperationOrganizeSites) {
[self updateDictFolders];
[self rebuildItemsAnimated:NO];
} else {
[self updateDictFolders];
[self rebuildItemsAnimated:NO];
[self updateSelectedWidgets];
}
}
@ -315,6 +330,10 @@ static const CGFloat kFolderTitleHeight = 36.0;
} else {
[self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section] animated:YES];
}
if (self.operation == FeedChooserOperationWidgetSites) {
[self setWidgetIncludes:select item:item];
}
}];
[self updateControls];
@ -331,8 +350,18 @@ static const CGFloat kFolderTitleHeight = 36.0;
} else {
self.navigationItem.title = [NSString stringWithFormat:@"Mute %@ Sites", @(count)];
}
} else {
} else if (self.operation == FeedChooserOperationOrganizeSites) {
self.navigationItem.title = @"Organize Sites";
} else {
NSUInteger count = self.tableView.indexPathsForSelectedRows.count;
if (count == 0) {
self.navigationItem.title = @"No Widget Sites";
} else if (count == 1) {
self.navigationItem.title = @"1 Widget Site";
} else {
self.navigationItem.title = [NSString stringWithFormat:@"%@ Widget Sites", @(count)];
}
}
}
@ -345,6 +374,42 @@ static const CGFloat kFolderTitleHeight = 36.0;
[self updateTitle];
}
- (NSDictionary *)widgetFeeds {
NSMutableDictionary *feeds = [self.groupDefaults objectForKey:@"widget:feeds"];
if (feeds == nil) {
feeds = [NSMutableDictionary dictionary];
[self enumerateAllRowsUsingBlock:^(NSIndexPath *indexPath, FeedChooserItem *item) {
[feeds setObject:item.title forKey:item.identifierString];
}];
[self.groupDefaults setObject:feeds forKey:@"widget:feeds"];
}
return feeds;
}
- (BOOL)widgetIncludesFeed:(NSString *)feedId {
return [self.widgetFeeds objectForKey:feedId] != nil;
}
- (void)setWidgetIncludes:(BOOL)include item:(FeedChooserItem *)item {
NSMutableDictionary *feeds = [self.widgetFeeds mutableCopy];
if (include) {
[feeds setObject:item.title forKey:item.identifierString];
} else {
[feeds removeObjectForKey:item.identifierString];
}
[self.groupDefaults setObject:feeds forKey:@"widget:feeds"];
}
- (void)setWidgetIncludes:(BOOL)include itemForIndexPath:(NSIndexPath *)indexPath {
[self setWidgetIncludes:include item:[self itemForIndexPath:indexPath]];
}
#pragma mark - Title delegate methods
- (void)didSelectTitleView:(UIButton *)sender {
@ -416,8 +481,7 @@ static const CGFloat kFolderTitleHeight = 36.0;
[self rebuildItemsAnimated:YES];
[self selectItemsWithIdentifiers:identifiers animated:NO];
}];
MenuItemHandler selectAllHandler = ^{
[self enumerateSectionsUsingBlock:^(NSUInteger section, FeedChooserItem *folder) {
[self select:YES section:section];
@ -427,6 +491,7 @@ static const CGFloat kFolderTitleHeight = 36.0;
[self select:NO section:section];
}];
};
if (isMute) {
[viewController addTitle:@"Mute All" iconName:@"mute_feed_off.png" selectionShouldDismiss:YES handler:selectAllHandler];
[viewController addTitle:@"Unmute All" iconName:@"mute_feed_on.png" selectionShouldDismiss:YES handler:selectNoneHandler];
@ -551,6 +616,20 @@ static const CGFloat kFolderTitleHeight = 36.0;
self.dictFolders = folders;
}
- (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]) {
[identifiers addObject:item.identifier];
}
}];
[self selectItemsWithIdentifiers:identifiers animated:NO];
}
- (void)performSaveActiveFeeds {
[MBProgressHUD hideHUDForView:self.view animated:YES];
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
@ -685,10 +764,18 @@ static const CGFloat kFolderTitleHeight = 36.0;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.operation == FeedChooserOperationWidgetSites) {
[self setWidgetIncludes:YES itemForIndexPath:indexPath];
}
[self updateControls];
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.operation == FeedChooserOperationWidgetSites) {
[self setWidgetIncludes:NO itemForIndexPath:indexPath];
}
[self updateControls];
}

View file

@ -45,6 +45,7 @@
initWithObjects:[@"Preferences" uppercaseString],
[@"Mute Sites" uppercaseString],
[@"Organize Sites" uppercaseString],
[@"Widget Sites" uppercaseString],
[@"Notifications" uppercaseString],
[@"Find Friends" uppercaseString],
[appDelegate.isPremium ? @"Premium Account": @"Upgrade to Premium" uppercaseString],
@ -56,6 +57,7 @@
initWithObjects:[@"Preferences" uppercaseString],
[@"Mute Sites" uppercaseString],
[@"Organize Sites" uppercaseString],
[@"Widget Sites" uppercaseString],
[@"Notifications" uppercaseString],
[@"Find Friends" uppercaseString],
[appDelegate.isPremium ? @"Premium Account": @"Upgrade to Premium" uppercaseString],
@ -171,22 +173,26 @@
break;
case 3:
image = [UIImage imageNamed:@"menu_icn_widget.png"];
break;
case 4:
image = [UIImage imageNamed:@"menu_icn_notifications.png"];
break;
case 4:
case 5:
image = [UIImage imageNamed:@"menu_icn_followers.png"];
break;
case 5:
case 6:
image = [UIImage imageNamed:@"g_icn_greensun.png"];
break;
case 6:
case 7:
image = [UIImage imageNamed:@"menu_icn_fetch_subscribers.png"];
break;
case 7:
case 8:
image = [UIImage imageNamed:@"barbutton_sendto.png"];
break;
@ -225,22 +231,26 @@
break;
case 3:
[appDelegate showWidgetSites];
break;
case 4:
[appDelegate openNotificationsWithFeed:nil];
break;
case 4:
case 5:
[appDelegate showFindFriends];
break;
case 5:
case 6:
[appDelegate showPremiumDialog];
break;
case 6:
case 7:
[appDelegate confirmLogout];
break;
case 7:
case 8:
[self showLoginAsDialog];
break;

View file

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13196" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13173"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -22,7 +20,7 @@
<rect key="frame" x="0.0" y="0.0" width="320" height="460"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="12">
<tableView clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="12">
<rect key="frame" x="0.0" y="0.0" width="320" height="460"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -31,7 +29,7 @@
<outlet property="delegate" destination="-1" id="15"/>
</connections>
</tableView>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" id="WaK-km-cVb">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" translatesAutoresizingMaskIntoConstraints="NO" id="WaK-km-cVb">
<rect key="frame" x="43" y="216" width="234" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -44,7 +42,7 @@
<action selector="changeTheme:" destination="-1" eventType="valueChanged" id="lQ3-gY-fUx"/>
</connections>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" misplaced="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" id="tAD-6D-jDd">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" translatesAutoresizingMaskIntoConstraints="NO" id="tAD-6D-jDd">
<rect key="frame" x="56" y="252" width="209" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -62,6 +60,7 @@
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="139" y="154"/>
</view>
</objects>
<resources>

View file

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" colorMatched="YES">
<device id="ipad9_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="ipad9_7" orientation="portrait" layout="fullscreen" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -26,7 +24,7 @@
<rect key="frame" x="0.0" y="0.0" width="240" height="222"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" id="28">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" translatesAutoresizingMaskIntoConstraints="NO" id="28">
<rect key="frame" x="3" y="81" width="234" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -40,7 +38,7 @@
<action selector="changeFontSize:" destination="-1" eventType="valueChanged" id="29"/>
</connections>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" id="4iD-hO-qU2">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" translatesAutoresizingMaskIntoConstraints="NO" id="4iD-hO-qU2">
<rect key="frame" x="3" y="123" width="234" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -54,7 +52,7 @@
<action selector="changeLineSpacing:" destination="-1" eventType="valueChanged" id="3hE-hD-2Aq"/>
</connections>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" id="muZ-4L-Qll">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" translatesAutoresizingMaskIntoConstraints="NO" id="muZ-4L-Qll">
<rect key="frame" x="3" y="96" width="234" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -65,7 +63,7 @@
<action selector="changeScrollOrientation:" destination="-1" eventType="valueChanged" id="x5R-7P-7ha"/>
</connections>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" id="RRB-to-azP">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" translatesAutoresizingMaskIntoConstraints="NO" id="RRB-to-azP">
<rect key="frame" x="3" y="96" width="234" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -76,7 +74,7 @@
<action selector="changeFullscreen:" destination="-1" eventType="valueChanged" id="h3Y-T6-P9J"/>
</connections>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" id="DbL-5f-bAN">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" translatesAutoresizingMaskIntoConstraints="NO" id="DbL-5f-bAN">
<rect key="frame" x="3" y="96" width="234" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -87,7 +85,7 @@
<action selector="changeAutoscroll:" destination="-1" eventType="valueChanged" id="dFh-eV-zfx"/>
</connections>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" id="9Zd-0Z-SQt">
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="bar" selectedSegmentIndex="2" translatesAutoresizingMaskIntoConstraints="NO" id="9Zd-0Z-SQt">
<rect key="frame" x="3" y="97" width="234" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
@ -100,7 +98,7 @@
<action selector="changeTheme:" destination="-1" eventType="valueChanged" id="d7d-9h-DPN"/>
</connections>
</segmentedControl>
<tableView clipsSubviews="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="35">
<tableView clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="35">
<rect key="frame" x="0.0" y="-1" width="240" height="223"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -113,14 +111,15 @@
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="139" y="154"/>
</view>
</objects>
<resources>
<image name="line_spacing_l.png" width="32" height="32"/>
<image name="line_spacing_m.png" width="32" height="32"/>
<image name="line_spacing_s.png" width="32" height="32"/>
<image name="line_spacing_xl.png" width="32" height="32"/>
<image name="line_spacing_xs.png" width="32" height="32"/>
<image name="line_spacing_l.png" width="32.450702667236328" height="32.450702667236328"/>
<image name="line_spacing_m.png" width="32.450702667236328" height="32.450702667236328"/>
<image name="line_spacing_s.png" width="32.450702667236328" height="32.450702667236328"/>
<image name="line_spacing_xl.png" width="32.450702667236328" height="32.450702667236328"/>
<image name="line_spacing_xs.png" width="32.450702667236328" height="32.450702667236328"/>
<image name="theme_color_dark.png" width="32" height="32"/>
<image name="theme_color_light.png" width="32" height="32"/>
<image name="theme_color_medium.png" width="32" height="32"/>

View file

@ -307,11 +307,13 @@ SFSafariViewControllerDelegate> {
- (void)showFindFriends;
- (void)showMuteSites;
- (void)showOrganizeSites;
- (void)showWidgetSites;
- (void)showPremiumDialog;
- (void)showPreferences;
- (void)setHiddenPreferencesAnimated:(BOOL)animated;
- (void)resizePreviewSize;
- (void)resizeFontSize;
- (void)popToRoot;
- (void)showMoveSite;
- (void)openTrainSite;

View file

@ -515,15 +515,7 @@
}];
} else if ([action isEqualToString:@"VIEW_STORY_IDENTIFIER"] ||
[action isEqualToString:@"com.apple.UNNotificationDefaultActionIdentifier"]) {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
[masterContainerViewController dismissViewControllerAnimated:NO completion:nil];
[self.navigationController
popToViewController:[self.navigationController.viewControllers
objectAtIndex:0]
animated:YES];
} else {
[self.navigationController popToRootViewControllerAnimated:NO];
}
[self popToRoot];
[self loadFeed:feedIdStr withStory:storyHash animated:NO];
if (completionHandler) completionHandler();
} else if ([action isEqualToString:@"DISMISS_IDENTIFIER"]) {
@ -555,6 +547,36 @@
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
if (self.activeUsername && [url.scheme isEqualToString:@"newsblurwidget"]) {
NSMutableDictionary *query = [NSMutableDictionary dictionary];
for (NSString *component in [url.query componentsSeparatedByString:@"&"]) {
NSArray *keyAndValue = [component componentsSeparatedByString:@"="];
[query setObject:keyAndValue.lastObject forKey:keyAndValue.firstObject];
}
NSString *feedId = query[@"feedId"];
NSString *storyHash = query[@"storyHash"];
NSString *error = query[@"error"];
if (error.length) {
[self popToRoot];
[self showWidgetSites];
return YES;
}
if (!feedId.length || !storyHash.length) {
return NO;
}
[self popToRoot];
[self loadFeed:feedId withStory:storyHash animated:NO];
return YES;
}
return NO;
}
@ -720,6 +742,17 @@
[feedsViewController resizeFontSize];
}
- (void)popToRoot {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
[masterContainerViewController dismissViewControllerAnimated:NO completion:nil];
[self.navigationController
popToViewController:[self.navigationController.viewControllers objectAtIndex:0]
animated:YES];
} else {
[self.navigationController popToRootViewControllerAnimated:NO];
}
}
- (void)showPremiumDialog {
UINavigationController *navController = self.navigationController;
if (self.premiumNavigationController == nil) {
@ -825,6 +858,10 @@
[self showFeedChooserForOperation:FeedChooserOperationOrganizeSites];
}
- (void)showWidgetSites {
[self showFeedChooserForOperation:FeedChooserOperationWidgetSites];
}
- (void)showFindFriends {
[self hidePopover];

View file

@ -566,9 +566,10 @@ static NSArray<NSString *> *NewsBlurTopSectionNames;
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.newsblur.NewsBlur-Group"];
[defaults setObject:[results objectForKey:@"share_ext_token"] forKey:@"share:token"];
[defaults setObject:DEFAULT_NEWSBLUR_URL forKey:@"share:host"];
[defaults setObject:self.appDelegate.url forKey:@"share:host"];
[self validateWidgetFeedsForGroupDefaults:defaults usingResults:results];
[defaults synchronize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
[appDelegate.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
@ -1160,6 +1161,25 @@ static NSArray<NSString *> *NewsBlurTopSectionNames;
}
}
- (void)validateWidgetFeedsForGroupDefaults:(NSUserDefaults *)groupDefaults usingResults:(NSDictionary *)results {
NSMutableDictionary *feeds = [groupDefaults objectForKey:@"widget:feeds"];
if (feeds == nil) {
feeds = [NSMutableDictionary dictionary];
NSDictionary *resultsFeeds = results[@"feeds"];
[resultsFeeds enumerateKeysAndObjectsUsingBlock:^(id key, NSDictionary *obj, BOOL *stop) {
NSString *identifier = [NSString stringWithFormat:@"%@", key];
NSString *title = obj[@"feed_title"];
feeds[identifier] = title;
}];
[groupDefaults setObject:feeds forKey:@"widget:feeds"];
}
}
#pragma mark -
#pragma mark Table View - Feed List

View file

@ -56,6 +56,16 @@
<string>pocketapp16638</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.newsblur.NewsBlur.widget</string>
<key>CFBundleURLSchemes</key>
<array>
<string>newsblurwidget</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

View file

@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
010EDEFA1B2386B7003B79DE /* OnePasswordExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 010EDEF81B2386B7003B79DE /* OnePasswordExtension.m */; };
010EDEFC1B238722003B79DE /* 1Password.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 010EDEFB1B238722003B79DE /* 1Password.xcassets */; };
17042DB92391D68A001BCD32 /* WidgetStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17042DB82391D68A001BCD32 /* WidgetStory.swift */; };
17042DBB23922A4D001BCD32 /* WidgetLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17042DBA23922A4D001BCD32 /* WidgetLoader.swift */; };
1715D02B2166B3F900227731 /* PremiumManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1715D02A2166B3F900227731 /* PremiumManager.m */; };
17362ADD23639B4E00A0FCCC /* OfflineFetchText.m in Sources */ = {isa = PBXBuildFile; fileRef = 17362ADC23639B4E00A0FCCC /* OfflineFetchText.m */; };
1740C6881C10FD75005EA453 /* theme_color_dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 1740C6841C10FD75005EA453 /* theme_color_dark.png */; };
@ -42,10 +44,16 @@
175065911C5730FB00072BF5 /* barbutton_selection_off@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1750658E1C5730FB00072BF5 /* barbutton_selection_off@3x.png */; };
175696A61C596ABC004C128D /* menu_icn_all.png in Resources */ = {isa = PBXBuildFile; fileRef = 175696A41C596ABC004C128D /* menu_icn_all.png */; };
175696A71C596ABC004C128D /* menu_icn_all@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 175696A51C596ABC004C128D /* menu_icn_all@2x.png */; };
175FAC4C23AB34EB002AC38C /* menu_icn_widget.png in Resources */ = {isa = PBXBuildFile; fileRef = 175FAC4A23AB34EB002AC38C /* menu_icn_widget.png */; };
175FAC4D23AB34EB002AC38C /* menu_icn_widget@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 175FAC4B23AB34EB002AC38C /* menu_icn_widget@2x.png */; };
176129601C630AEB00702FE4 /* mute_feed_off.png in Resources */ = {isa = PBXBuildFile; fileRef = 1761295C1C630AEB00702FE4 /* mute_feed_off.png */; };
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 */; };
177551D5238E228A00E27818 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 177551D4238E228A00E27818 /* NotificationCenter.framework */; };
177551DB238E228A00E27818 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 177551D9238E228A00E27818 /* MainInterface.storyboard */; };
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 */; };
17876BA01C9911D40055DD15 /* g_icn_folder_sm.png in Resources */ = {isa = PBXBuildFile; fileRef = 17876B9C1C9911D40055DD15 /* g_icn_folder_sm.png */; };
@ -94,6 +102,7 @@
17CBD3BF1BF66B6C003FCCAE /* MarkReadMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 17CBD3BE1BF66B6C003FCCAE /* MarkReadMenuViewController.m */; };
17CBD3C21BF6ED2C003FCCAE /* menu_icn_markread.png in Resources */ = {isa = PBXBuildFile; fileRef = 17CBD3C01BF6ED2C003FCCAE /* menu_icn_markread.png */; };
17CBD3C31BF6ED2C003FCCAE /* menu_icn_markread@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17CBD3C11BF6ED2C003FCCAE /* menu_icn_markread@2x.png */; };
17CE3F0723AC529E003152EF /* WidgetErrorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CE3F0623AC529E003152EF /* WidgetErrorTableViewCell.swift */; };
17CF7DD41C5C6AE40067BC5B /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17CF7DD31C5C6AE40067BC5B /* WebKit.framework */; };
17E265DE1C0D17340060655F /* storyDetailViewDark.css in Resources */ = {isa = PBXBuildFile; fileRef = 17E265DD1C0D17340060655F /* storyDetailViewDark.css */; };
17E57D571C0E592600EB3D4B /* storyDetailViewMedium.css in Resources */ = {isa = PBXBuildFile; fileRef = 17E57D551C0E592600EB3D4B /* storyDetailViewMedium.css */; };
@ -105,11 +114,15 @@
17E635A81C5432220075338E /* barbutton_selection@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17E635A21C5432220075338E /* barbutton_selection@2x.png */; };
17E635A91C5432220075338E /* barbutton_selection@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17E635A31C5432220075338E /* barbutton_selection@3x.png */; };
17E635AF1C548C580075338E /* FeedChooserItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 17E635AE1C548C580075338E /* FeedChooserItem.m */; };
17E86ED5238E444B00863EC8 /* WidgetTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 17E86ED3238E444B00863EC8 /* WidgetTableViewCell.xib */; };
17E86ED6238E444B00863EC8 /* WidgetTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E86ED4238E444B00863EC8 /* WidgetTableViewCell.swift */; };
17EB505C1BE4411E0021358B /* choose_font.png in Resources */ = {isa = PBXBuildFile; fileRef = 17EB505A1BE4411E0021358B /* choose_font.png */; };
17EB505D1BE4411E0021358B /* choose_font@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17EB505B1BE4411E0021358B /* choose_font@2x.png */; };
17EB50601BE46A900021358B /* FontListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 17EB505F1BE46A900021358B /* FontListViewController.m */; };
17EB50621BE46BB00021358B /* FontListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 17EB50611BE46BB00021358B /* FontListViewController.xib */; };
17F156711BDABBF60092EBFD /* safari_shadow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17F156701BDABBF60092EBFD /* safari_shadow@2x.png */; };
17F363F2238E417300D5379D /* WidgetExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F363EF238E417300D5379D /* WidgetExtensionViewController.swift */; };
17FB51D723AC81C500F5D5BF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 17FB51D923AC81C500F5D5BF /* InfoPlist.strings */; };
1D3623260D0F684500981E51 /* NewsBlurAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* NewsBlurAppDelegate.m */; };
1D60589F0D05DD5A006BFB54 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; };
1DF5F4E00D08C38300B7A737 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */; };
@ -604,6 +617,13 @@
remoteGlobalIDString = 1749390F1C251BFE003D98AA;
remoteInfo = "Share Extension";
};
177551DD238E228A00E27818 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
proxyType = 1;
remoteGlobalIDString = 177551D2238E228A00E27818;
remoteInfo = widget;
};
FF8A949D1DE3BB77000A4C31 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
@ -631,6 +651,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
177551DF238E228A00E27818 /* NewsBlur Latest.appex in Embed App Extensions */,
1749391B1C251BFE003D98AA /* Share Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
@ -642,6 +663,8 @@
010EDEF81B2386B7003B79DE /* OnePasswordExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OnePasswordExtension.m; path = "Other Sources/OnePasswordExtension/OnePasswordExtension.m"; sourceTree = "<group>"; };
010EDEF91B2386B7003B79DE /* OnePasswordExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OnePasswordExtension.h; path = "Other Sources/OnePasswordExtension/OnePasswordExtension.h"; sourceTree = "<group>"; };
010EDEFB1B238722003B79DE /* 1Password.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = 1Password.xcassets; path = "Other Sources/OnePasswordExtension/1Password.xcassets"; sourceTree = "<group>"; };
17042DB82391D68A001BCD32 /* WidgetStory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetStory.swift; sourceTree = "<group>"; };
17042DBA23922A4D001BCD32 /* WidgetLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLoader.swift; sourceTree = "<group>"; };
1715D0292166B3F900227731 /* PremiumManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PremiumManager.h; sourceTree = "<group>"; };
1715D02A2166B3F900227731 /* PremiumManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PremiumManager.m; sourceTree = "<group>"; };
17362ADB23639B4E00A0FCCC /* OfflineFetchText.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OfflineFetchText.h; path = offline/OfflineFetchText.h; sourceTree = "<group>"; };
@ -684,10 +707,17 @@
1750658E1C5730FB00072BF5 /* barbutton_selection_off@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "barbutton_selection_off@3x.png"; sourceTree = "<group>"; };
175696A41C596ABC004C128D /* menu_icn_all.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_all.png; sourceTree = "<group>"; };
175696A51C596ABC004C128D /* menu_icn_all@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_all@2x.png"; sourceTree = "<group>"; };
175FAC4A23AB34EB002AC38C /* menu_icn_widget.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_widget.png; sourceTree = "<group>"; };
175FAC4B23AB34EB002AC38C /* menu_icn_widget@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_widget@2x.png"; sourceTree = "<group>"; };
1761295C1C630AEB00702FE4 /* mute_feed_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mute_feed_off.png; sourceTree = "<group>"; };
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 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>"; };
177551E3238E26BF00E27818 /* Widget Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Widget Extension.entitlements"; sourceTree = "<group>"; };
17876B9A1C9911D40055DD15 /* g_icn_folder_rss_sm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_folder_rss_sm.png; sourceTree = "<group>"; };
17876B9B1C9911D40055DD15 /* g_icn_folder_rss_sm@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "g_icn_folder_rss_sm@2x.png"; sourceTree = "<group>"; };
17876B9C1C9911D40055DD15 /* g_icn_folder_sm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_folder_sm.png; sourceTree = "<group>"; };
@ -739,6 +769,8 @@
17CBD3BE1BF66B6C003FCCAE /* MarkReadMenuViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MarkReadMenuViewController.m; sourceTree = "<group>"; };
17CBD3C01BF6ED2C003FCCAE /* menu_icn_markread.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_markread.png; sourceTree = "<group>"; };
17CBD3C11BF6ED2C003FCCAE /* menu_icn_markread@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_markread@2x.png"; sourceTree = "<group>"; };
17CE3F0523AC529B003152EF /* WidgetErrorTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WidgetErrorTableViewCell.xib; sourceTree = "<group>"; };
17CE3F0623AC529E003152EF /* WidgetErrorTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetErrorTableViewCell.swift; sourceTree = "<group>"; };
17CF7DD31C5C6AE40067BC5B /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
17E265DD1C0D17340060655F /* storyDetailViewDark.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = storyDetailViewDark.css; path = static/storyDetailViewDark.css; sourceTree = SOURCE_ROOT; };
17E57D551C0E592600EB3D4B /* storyDetailViewMedium.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = storyDetailViewMedium.css; path = static/storyDetailViewMedium.css; sourceTree = SOURCE_ROOT; };
@ -751,12 +783,16 @@
17E635A31C5432220075338E /* barbutton_selection@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "barbutton_selection@3x.png"; sourceTree = "<group>"; };
17E635AD1C548C580075338E /* FeedChooserItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedChooserItem.h; sourceTree = "<group>"; };
17E635AE1C548C580075338E /* FeedChooserItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedChooserItem.m; sourceTree = "<group>"; };
17E86ED3238E444B00863EC8 /* WidgetTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = WidgetTableViewCell.xib; sourceTree = "<group>"; };
17E86ED4238E444B00863EC8 /* WidgetTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetTableViewCell.swift; sourceTree = "<group>"; };
17EB505A1BE4411E0021358B /* choose_font.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = choose_font.png; sourceTree = "<group>"; };
17EB505B1BE4411E0021358B /* choose_font@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "choose_font@2x.png"; sourceTree = "<group>"; };
17EB505E1BE46A900021358B /* FontListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FontListViewController.h; sourceTree = "<group>"; };
17EB505F1BE46A900021358B /* FontListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FontListViewController.m; sourceTree = "<group>"; };
17EB50611BE46BB00021358B /* FontListViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = FontListViewController.xib; path = Classes/FontListViewController.xib; sourceTree = SOURCE_ROOT; };
17F156701BDABBF60092EBFD /* safari_shadow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "safari_shadow@2x.png"; sourceTree = "<group>"; };
17F363EF238E417300D5379D /* WidgetExtensionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetExtensionViewController.swift; sourceTree = "<group>"; };
17FB51D823AC81C500F5D5BF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
1D30AB110D05D00D00671497 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
1D3623240D0F684500981E51 /* NewsBlurAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = NewsBlurAppDelegate.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
1D3623250D0F684500981E51 /* NewsBlurAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = NewsBlurAppDelegate.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
@ -1436,6 +1472,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
177551D0238E228A00E27818 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
177551D5238E228A00E27818 /* NotificationCenter.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1D60588F0D05DD3D006BFB54 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -1570,12 +1614,31 @@
path = fonts;
sourceTree = "<group>";
};
177551D6238E228A00E27818 /* Widget Extension */ = {
isa = PBXGroup;
children = (
17F363EF238E417300D5379D /* WidgetExtensionViewController.swift */,
17E86ED4238E444B00863EC8 /* WidgetTableViewCell.swift */,
17E86ED3238E444B00863EC8 /* WidgetTableViewCell.xib */,
17CE3F0623AC529E003152EF /* WidgetErrorTableViewCell.swift */,
17CE3F0523AC529B003152EF /* WidgetErrorTableViewCell.xib */,
17042DB82391D68A001BCD32 /* WidgetStory.swift */,
17042DBA23922A4D001BCD32 /* WidgetLoader.swift */,
177551D9238E228A00E27818 /* MainInterface.storyboard */,
177551E3238E26BF00E27818 /* Widget Extension.entitlements */,
177551DC238E228A00E27818 /* Info.plist */,
17FB51D923AC81C500F5D5BF /* InfoPlist.strings */,
);
path = "Widget Extension";
sourceTree = "<group>";
};
19C28FACFE9D520D11CA2CBB /* Products */ = {
isa = PBXGroup;
children = (
1D6058910D05DD3D006BFB54 /* NewsBlur.app */,
174939101C251BFE003D98AA /* Share Extension.appex */,
FF8A94971DE3BB77000A4C31 /* Story Notification Service Extension.appex */,
177551D3238E228A00E27818 /* NewsBlur Latest.appex */,
);
name = Products;
sourceTree = "<group>";
@ -1590,6 +1653,7 @@
29B97315FDCFA39411CA2CEA /* Other Sources */,
174939111C251BFE003D98AA /* Share Extension */,
FF8A94981DE3BB77000A4C31 /* Story Notification Service Extension */,
177551D6238E228A00E27818 /* Widget Extension */,
29B97323FDCFA39411CA2CEA /* Frameworks */,
19C28FACFE9D520D11CA2CBB /* Products */,
FF8C49921BBC9D140010D894 /* NewsBlur.entitlements */,
@ -1686,6 +1750,7 @@
1DF5F4DF0D08C38300B7A737 /* UIKit.framework */,
FF8D1EBC1BAA311000725D8A /* SBJSON */,
FF8D1EA41BAA304E00725D8A /* Reachability */,
177551D4238E228A00E27818 /* NotificationCenter.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -1924,6 +1989,8 @@
17E635981C5431F50075338E /* menu_icn_mute@2x.png */,
17E635991C5431F50075338E /* menu_icn_organize.png */,
FF877CD21C6541F2007940C3 /* menu_icn_organize@2x.png */,
175FAC4A23AB34EB002AC38C /* menu_icn_widget.png */,
175FAC4B23AB34EB002AC38C /* menu_icn_widget@2x.png */,
FF688E5016E6B8D0003B7B42 /* traverse_background.png */,
FF688E5116E6B8D0003B7B42 /* traverse_background@2x.png */,
FF688E4C16E6B3E1003B7B42 /* traverse_done.png */,
@ -2685,6 +2752,23 @@
productReference = 174939101C251BFE003D98AA /* Share Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
177551D2238E228A00E27818 /* Widget Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 177551E2238E228A00E27818 /* Build configuration list for PBXNativeTarget "Widget Extension" */;
buildPhases = (
177551CF238E228A00E27818 /* Sources */,
177551D0238E228A00E27818 /* Frameworks */,
177551D1238E228A00E27818 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "Widget Extension";
productName = widget;
productReference = 177551D3238E228A00E27818 /* NewsBlur Latest.appex */;
productType = "com.apple.product-type.app-extension";
};
1D6058900D05DD3D006BFB54 /* NewsBlur */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1D6058960D05DD3E006BFB54 /* Build configuration list for PBXNativeTarget "NewsBlur" */;
@ -2700,6 +2784,7 @@
dependencies = (
1749391A1C251BFE003D98AA /* PBXTargetDependency */,
FF8A949E1DE3BB77000A4C31 /* PBXTargetDependency */,
177551DE238E228A00E27818 /* PBXTargetDependency */,
);
name = NewsBlur;
productName = NewsBlur;
@ -2729,6 +2814,7 @@
29B97313FDCFA39411CA2CEA /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1110;
ORGANIZATIONNAME = NewsBlur;
TargetAttributes = {
@ -2741,6 +2827,12 @@
};
};
};
177551D2238E228A00E27818 = {
CreatedOnToolsVersion = 11.2.1;
DevelopmentTeam = HR7P97SD72;
LastSwiftMigration = 1120;
ProvisioningStyle = Automatic;
};
1D6058900D05DD3D006BFB54 = {
DevelopmentTeam = HR7P97SD72;
LastSwiftMigration = 1020;
@ -2804,6 +2896,7 @@
1D6058900D05DD3D006BFB54 /* NewsBlur */,
1749390F1C251BFE003D98AA /* Share Extension */,
FF8A94961DE3BB77000A4C31 /* Story Notification Service Extension */,
177551D2238E228A00E27818 /* Widget Extension */,
);
};
/* End PBXProject section */
@ -2817,6 +2910,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
177551D1238E228A00E27818 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
17813FB723AC6E450057FB16 /* WidgetErrorTableViewCell.xib in Resources */,
17FB51D723AC81C500F5D5BF /* InfoPlist.strings in Resources */,
177551DB238E228A00E27818 /* MainInterface.storyboard in Resources */,
17E86ED5238E444B00863EC8 /* WidgetTableViewCell.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1D60588D0D05DD3D006BFB54 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -2905,6 +3009,7 @@
43A4C49B15B00A26008787B5 /* warning.png in Resources */,
17E265DE1C0D17340060655F /* storyDetailViewDark.css in Resources */,
17C67D9F2138B2D20027CCAE /* traverse_previous_vert@2x.png in Resources */,
175FAC4D23AB34EB002AC38C /* menu_icn_widget@2x.png in Resources */,
43A4C49C15B00A26008787B5 /* world.png in Resources */,
43C1680B15B3D99B00428BA3 /* 7-location-place.png in Resources */,
43B6A27515B6952F00CEA2E6 /* group.png in Resources */,
@ -2923,6 +3028,7 @@
43A4BADC15C866FA00F3B8D4 /* popoverArrowDown@2x.png in Resources */,
17AACFEA22279A3C00DE6EA4 /* autoscroll_resume.png in Resources */,
43A4BADD15C866FA00F3B8D4 /* popoverArrowDownSimple.png in Resources */,
175FAC4C23AB34EB002AC38C /* menu_icn_widget.png in Resources */,
43A4BADE15C866FA00F3B8D4 /* popoverArrowLeft.png in Resources */,
FFC486AC19CA410000F4758F /* logo_50.png in Resources */,
176129601C630AEB00702FE4 /* mute_feed_off.png in Resources */,
@ -3246,6 +3352,18 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
177551CF238E228A00E27818 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
17CE3F0723AC529E003152EF /* WidgetErrorTableViewCell.swift in Sources */,
17E86ED6238E444B00863EC8 /* WidgetTableViewCell.swift in Sources */,
17F363F2238E417300D5379D /* WidgetExtensionViewController.swift in Sources */,
17042DB92391D68A001BCD32 /* WidgetStory.swift in Sources */,
17042DBB23922A4D001BCD32 /* WidgetLoader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1D60588E0D05DD3D006BFB54 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -3420,6 +3538,11 @@
target = 1749390F1C251BFE003D98AA /* Share Extension */;
targetProxy = 174939191C251BFE003D98AA /* PBXContainerItemProxy */;
};
177551DE238E228A00E27818 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 177551D2238E228A00E27818 /* Widget Extension */;
targetProxy = 177551DD238E228A00E27818 /* PBXContainerItemProxy */;
};
FF8A949E1DE3BB77000A4C31 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = FF8A94961DE3BB77000A4C31 /* Story Notification Service Extension */;
@ -3436,6 +3559,22 @@
name = MainInterface.storyboard;
sourceTree = "<group>";
};
177551D9238E228A00E27818 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
177551DA238E228A00E27818 /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
17FB51D923AC81C500F5D5BF /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
17FB51D823AC81C500F5D5BF /* en */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
FF34FD411E9D93CB0062F8ED /* IASKLocalizable.strings */ = {
isa = PBXVariantGroup;
children = (
@ -3481,7 +3620,6 @@
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 107;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -3501,7 +3639,6 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 9.1.1;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -3531,7 +3668,6 @@
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -3545,7 +3681,6 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 9.1.1;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -3556,9 +3691,97 @@
};
name = Release;
};
177551E0238E228A00E27818 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Widget Extension/Widget Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HR7P97SD72;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = "Widget Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget;
PRODUCT_NAME = "NewsBlur Latest";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
177551E1238E228A00E27818 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Widget Extension/Widget Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_NS_ASSERTIONS = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = "Widget Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget;
PRODUCT_NAME = "NewsBlur Latest";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
1D6058940D05DD3E006BFB54 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_ARC = YES;
@ -3566,7 +3789,6 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_TEAM = HR7P97SD72;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -3587,7 +3809,6 @@
"\"$(SRCROOT)\"",
"\"$(SRCROOT)/Other Sources\"",
);
MARKETING_VERSION = 9.1.1;
OTHER_CPLUSPLUSFLAGS = "$(OTHER_CFLAGS)";
OTHER_LDFLAGS = (
"-lsqlite3.0",
@ -3609,6 +3830,7 @@
1D6058950D05DD3E006BFB54 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_ARC = YES;
@ -3616,7 +3838,6 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_TEAM = HR7P97SD72;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -3636,7 +3857,6 @@
"\"$(SRCROOT)\"",
"\"$(SRCROOT)/Other Sources\"",
);
MARKETING_VERSION = 9.1.1;
OTHER_LDFLAGS = (
"-lsqlite3.0",
"-ObjC",
@ -3675,6 +3895,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 107;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -3688,6 +3909,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/**";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MARKETING_VERSION = 9.1.1;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = "-ObjC";
PROVISIONING_PROFILE = "";
@ -3720,6 +3942,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 107;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = "compiler-default";
@ -3732,6 +3955,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/**";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MARKETING_VERSION = 9.1.1;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1";
OTHER_LDFLAGS = "-ObjC";
@ -3757,7 +3981,6 @@
CLANG_WARN_SUSPICIOUS_MOVES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 107;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HR7P97SD72;
GCC_C_LANGUAGE_STANDARD = gnu99;
@ -3772,7 +3995,6 @@
INFOPLIST_FILE = "Story Notification Service Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 9.1.1;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Story-Notification-Service-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -3795,7 +4017,6 @@
CLANG_WARN_SUSPICIOUS_MOVES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_TEAM = HR7P97SD72;
ENABLE_NS_ASSERTIONS = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
@ -3804,7 +4025,6 @@
INFOPLIST_FILE = "Story Notification Service Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 9.1.1;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NewsBlur.Story-Notification-Service-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -3826,6 +4046,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
177551E2238E228A00E27818 /* Build configuration list for PBXNativeTarget "Widget Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
177551E0238E228A00E27818 /* Debug */,
177551E1238E228A00E27818 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1D6058960D05DD3E006BFB54 /* Build configuration list for PBXNativeTarget "NewsBlur" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
wasCreatedForAppExtension = "YES"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "177551D2238E228A00E27818"
BuildableName = "NewsBlur Stories.appex"
BlueprintName = "Widget Extension"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1D6058900D05DD3D006BFB54"
BuildableName = "NewsBlur.app"
BlueprintName = "NewsBlur"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "1"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "177551D2238E228A00E27818"
BuildableName = "NewsBlur Stories.appex"
BlueprintName = "Widget Extension"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1D6058900D05DD3D006BFB54"
BuildableName = "NewsBlur.app"
BlueprintName = "NewsBlur"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1D6058900D05DD3D006BFB54"
BuildableName = "NewsBlur.app"
BlueprintName = "NewsBlur"
ReferencedContainer = "container:NewsBlur.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y4P-Rc-9qS">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Widget Extension View Controller-->
<scene sceneID="rRX-uD-WSg">
<objects>
<tableViewController id="Y4P-Rc-9qS" customClass="WidgetExtensionViewController" customModule="NewsBlur_" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="WP5-UW-zbV">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="dataSource" destination="Y4P-Rc-9qS" id="JAi-3i-yO4"/>
<outlet property="delegate" destination="Y4P-Rc-9qS" id="DMT-QM-cAI"/>
</connections>
</tableView>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="375" height="200"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="598-tA-QtX" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="30" y="57"/>
</scene>
</scenes>
</document>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widget-extension</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.newsblur.NewsBlur-Group</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,16 @@
//
// WidgetErrorTableViewCell.swift
// Widget Extension
//
// Created by David Sinclair on 2019-11-26.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
class WidgetErrorTableViewCell: UITableViewCell {
/// The reuse identifier for this table view cell.
static let reuseIdentifier = "WidgetErrorTableViewCell"
@IBOutlet var errorLabel: UILabel!
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="110" id="KGk-i7-Jjx" customClass="WidgetErrorTableViewCell" customModule="NewsBlur_" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjx" id="H2p-sc-9ux">
<rect key="frame" x="0.0" y="0.0" width="320" height="110"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="&lt;error&gt;" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c8v-22-SQx">
<rect key="frame" x="130.5" y="44.5" width="59" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="c8v-22-SQx" firstAttribute="centerX" secondItem="H2p-sc-9ux" secondAttribute="centerX" id="rUZ-br-Z2x"/>
<constraint firstItem="c8v-22-SQx" firstAttribute="centerY" secondItem="H2p-sc-9ux" secondAttribute="centerY" id="uTo-nZ-Tdx"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oax"/>
<connections>
<outlet property="errorLabel" destination="c8v-22-SQx" id="2zs-Gq-Owx"/>
</connections>
<point key="canvasLocation" x="33.600000000000001" y="85.457271364317847"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,549 @@
//
// WidgetViewController.swift
// Widget Extension
//
// Created by David Sinclair on 2019-11-26.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
import NotificationCenter
enum WidgetError: String, Error {
case notLoggedIn
case loading
case noFeeds
case noStories
}
class WidgetExtensionViewController: UITableViewController, NCWidgetProviding {
/// The base URL of the NewsBlur server.
var host: String?
/// 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()
/// Loaded stories.
var stories = [Story]()
/// An error to display instead of the stories, or `nil` if the stories should be displayed.
var error: WidgetError?
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 widgetFolder = "Widget"
static let storiesFilename = "Stories.json"
static let imageExtension = "png"
static let limit = 5
static let defaultRowHeight: CGFloat = 110
static let storyImageSize: CGFloat = 64 * 3
}
// MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Allow the today widget to be expanded or contracted.
extensionContext?.widgetLargestAvailableDisplayMode = .expanded
loadCachedStories()
// Register the table view cell.
let widgetTableViewCellNib = UINib(nibName: "WidgetTableViewCell", bundle: nil)
tableView.register(widgetTableViewCellNib, forCellReuseIdentifier: WidgetTableViewCell.reuseIdentifier)
let errorTableViewCellNib = UINib(nibName: "WidgetErrorTableViewCell", bundle: nil)
tableView.register(errorTableViewCellNib, forCellReuseIdentifier: WidgetErrorTableViewCell.reuseIdentifier)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
extensionContext?.widgetLargestAvailableDisplayMode = error == nil ? .expanded : .compact
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
imageCache.removeAll()
}
// MARK: - Widget provider protocol
typealias AnyDictionary = [String : Any]
typealias WidgetCompletion = (NCUpdateResult) -> Void
private var widgetCompletion: WidgetCompletion?
private typealias ImageDictionary = [String : UIImage]
private var imageCache = ImageDictionary()
private typealias LoaderDictionary = [String : Loader]
private var loaders = LoaderDictionary()
func widgetPerformUpdate(completionHandler: (@escaping WidgetCompletion)) {
if feeds.isEmpty {
if error == .noFeeds {
completionHandler(.noData)
} else {
error = .noFeeds
completionHandler(.newData)
}
return
}
let combinedFeeds = feeds.keys.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)
return
}
error = nil
widgetCompletion = completionHandler
loaders[Constant.storiesFilename] = Loader(url: url, completion: storyLoaderCompletion(result:))
}
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
switch activeDisplayMode {
case .compact:
// The compact view is a fixed size.
preferredContentSize = maxSize
case .expanded:
let height: CGFloat = rowHeight * CGFloat(numberOfTableRowsToDisplay)
preferredContentSize = CGSize(width: maxSize.width, height: min(height, maxSize.height))
@unknown default:
preconditionFailure("Unexpected value for activeDisplayMode.")
}
}
// MARK: - Content container protocol
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)
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfTableRowsToDisplay
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
precondition(Thread.isMainThread, "Table access not on the main thread")
if let error = error {
guard let cell = tableView.dequeueReusableCell(withIdentifier: WidgetErrorTableViewCell.reuseIdentifier, for: indexPath) as? WidgetErrorTableViewCell else {
preconditionFailure("Expected to dequeue a WidgetErrorTableViewCell")
}
switch error {
case .notLoggedIn:
cell.errorLabel.text = "Please log in to NewsBlur"
case .loading:
cell.errorLabel.text = "On its way..."
case .noFeeds:
cell.errorLabel.text = "Please choose sites to show"
case .noStories:
cell.errorLabel.text = "No stories for selected sites"
}
return cell
}
guard let cell = tableView.dequeueReusableCell(withIdentifier: WidgetTableViewCell.reuseIdentifier, for: indexPath) as? WidgetTableViewCell
else {
preconditionFailure("Expected to dequeue a WidgetTableViewCell")
}
let story = stories[indexPath.row]
cell.feedImageView.image = nil
// Completion handler passes the feed to confirm that this cell still wants that image (i.e. hasn't been reused).
feedImage(for: story.feed) { (image, feed) in
if story.feed == feed {
cell.feedImageView.image = image
}
}
if let name = feeds[story.feed] {
cell.feedLabel.text = name
} else {
cell.feedLabel.text = ""
}
cell.titleLabel.text = cleaned(story.title)
cell.contentLabel.text = cleaned(story.content)
cell.authorLabel.text = cleaned(story.author).uppercased()
cell.dateLabel.text = story.date
cell.thumbnailImageView.image = nil
storyImage(for: story.id, imageURL: story.imageURL) { (image, id) in
if story.id == id {
cell.thumbnailImageView.image = image
}
}
return cell
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return rowHeight
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let error = error {
if let appURL = URL(string: "newsblurwidget://?error=\(error.rawValue)") {
extensionContext?.open(appURL, completionHandler: nil)
}
} else {
let story = stories[indexPath.row]
if let appURL = URL(string: "newsblurwidget://?feedId=\(story.feed)&storyHash=\(story.id)") {
extensionContext?.open(appURL, completionHandler: nil)
}
}
tableView.deselectRow(at: indexPath, animated: true)
}
}
// MARK: - Helpers
private extension WidgetExtensionViewController {
var numberOfTableRowsToDisplay: Int {
if stories.isEmpty, error == nil {
error = .loading
}
if error != nil {
return 1
} else if extensionContext?.widgetActiveDisplayMode == NCWidgetDisplayMode.compact {
return 1
} else {
return min(stories.count, Constant.limit)
}
}
var rowHeight: CGFloat {
return extensionContext?.widgetMaximumSize(for: .compact).height ?? Constant.defaultRowHeight
}
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)
.trimmingCharacters(in: .whitespaces)
return clean.isEmpty ? " " : clean
}
func hostURL(with path: String) -> URL? {
guard let host = host else {
return nil
}
if let token = token {
return URL(string: host + path + "&secret_token=\(token)")
} else {
return URL(string: host + path)
}
}
func storyLoaderCompletion(result: Result<Data, Error>) {
defer {
widgetCompletion = nil
loaders[Constant.storiesFilename] = nil
}
if case .failure = result {
widgetCompletion?(.failed)
return
}
guard case .success(let data) = result else {
return
}
guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? AnyDictionary else {
widgetCompletion?(.failed)
return
}
guard let storyArray = dictionary["stories"] as? [AnyDictionary] else {
widgetCompletion?(.failed)
return
}
stories.removeAll()
for storyDict in storyArray {
stories.append(Story(from: storyDict))
}
saveStories()
if stories.isEmpty, error == nil {
error = .noStories
}
DispatchQueue.main.async {
self.extensionContext?.widgetLargestAvailableDisplayMode = self.error == nil ? .expanded : .compact
self.tableView.reloadData()
self.tableView.setNeedsDisplay()
self.widgetCompletion?(.newData)
}
}
var groupContainerURL: URL? {
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constant.group)
}
var widgetFolderURL: URL? {
return groupContainerURL?.appendingPathComponent(Constant.widgetFolder)
}
var storiesURL: URL? {
return widgetFolderURL?.appendingPathComponent(Constant.storiesFilename)
}
func createWidgetFolder() {
guard let url = widgetFolderURL else {
return
}
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}
func loadCachedStories() {
stories = []
guard let defaults = UserDefaults.init(suiteName: Constant.group) else {
return
}
host = defaults.string(forKey: Constant.host)
token = defaults.string(forKey: Constant.token)
if let dict = defaults.dictionary(forKey: Constant.feeds) as? FeedsDictionary {
feeds = dict
}
guard let url = storiesURL else {
return
}
do {
let json = try Data(contentsOf: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
stories = try decoder.decode([Story].self, from: json)
} catch {
print("Error \(error)")
}
}
func saveStories() {
guard let url = storiesURL else {
return
}
createWidgetFolder()
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
do {
let json = try encoder.encode(stories)
try json.write(to: url)
} catch {
print("Error \(error)")
}
}
func cachedImage(for identifier: String) -> UIImage? {
if let image = imageCache[identifier] {
return image
}
guard let folderURL = widgetFolderURL else {
return nil
}
do {
let imageURL = folderURL.appendingPathComponent(identifier).appendingPathExtension(Constant.imageExtension)
let data = try Data(contentsOf: imageURL)
guard let image = UIImage(data: data) else {
return nil
}
imageCache[identifier] = image
return image
} catch {
print("Image error: \(error)")
}
return nil
}
func save(image: UIImage, for identifier: String) {
guard let folderURL = widgetFolderURL else {
return
}
imageCache[identifier] = image
createWidgetFolder()
do {
let imageURL = folderURL.appendingPathComponent(identifier).appendingPathExtension(Constant.imageExtension)
try image.pngData()?.write(to: imageURL)
} catch {
print("Image error: \(error)")
}
}
typealias ImageCompletion = (UIImage?, String?) -> Void
func feedImage(for feed: String, completion: @escaping ImageCompletion) {
guard let url = hostURL(with: "/reader/favicons?feed_ids=\(feed)") else {
completion(nil, feed)
return
}
if let image = cachedImage(for: feed) {
completion(image, feed)
return
}
loaders[feed] = Loader(url: url) { (result) in
DispatchQueue.main.async {
defer {
self.loaders[feed] = nil
}
switch result {
case .success(let data):
guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? AnyDictionary,
let base64 = dictionary[feed] as? String,
let imageData = Data(base64Encoded: base64, options: .ignoreUnknownCharacters),
let image = UIImage(data: imageData) else {
completion(nil, feed)
return
}
self.save(image: image, for: feed)
completion(image, feed)
case .failure:
completion(nil, feed)
}
}
}
}
func storyImage(for identifier: String, imageURL: URL?, completion: @escaping ImageCompletion) {
guard let url = imageURL else {
completion(nil, identifier)
return
}
if let image = cachedImage(for: identifier) {
completion(image, identifier)
return
}
loaders[identifier] = Loader(url: url) { (result) in
DispatchQueue.main.async {
defer {
self.loaders[identifier] = nil
}
switch result {
case .success(let data):
guard let loadedImage = UIImage(data: data) else {
completion(nil, identifier)
return
}
let scaledImage = self.scale(image: loadedImage)
self.save(image: scaledImage, for: identifier)
completion(scaledImage, identifier)
case .failure:
completion(nil, identifier)
}
}
}
}
func scale(image: UIImage) -> UIImage {
guard image.size.width > Constant.storyImageSize || image.size.height > Constant.storyImageSize else {
return image
}
let size = CGSize(width: Constant.storyImageSize, height: Constant.storyImageSize)
UIGraphicsBeginImageContextWithOptions(size, true, 1)
image.draw(in: CGRect(origin: .zero, size: size))
defer {
UIGraphicsEndImageContext()
}
return UIGraphicsGetImageFromCurrentImageContext() ?? image
}
}

View file

@ -0,0 +1,67 @@
//
// WidgetLoader.swift
// Widget Extension
//
// Created by David Sinclair on 2019-11-29.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
/// Network loader for the widget.
class Loader: NSObject, URLSessionDataDelegate {
typealias Completion = (Result<Data, Error>) -> Void
private var completion: Completion
private var receivedData = Data()
init(url: URL, completion: @escaping Completion) {
self.completion = completion
super.init()
var request = URLRequest(url: url)
request.httpMethod = "GET"
// request.addValue(accept, forHTTPHeaderField: "Accept")
let config = URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
config.sharedContainerIdentifier = "group.com.newsblur.NewsBlur-Group"
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: request)
task.resume()
}
// MARK: - URL session delegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode) else {
completionHandler(.cancel)
return
}
completionHandler(.allow)
}
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
print("error: \(error.debugDescription)")
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("data: \(data)")
receivedData.append(data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
completion(.failure(error))
} else {
completion(.success(receivedData))
}
}
}

View file

@ -0,0 +1,122 @@
//
// WidgetStory.swift
// Widget Extension
//
// Created by David Sinclair on 2019-11-29.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
/// A story to display in the widget.
struct Story: Codable, Identifiable {
/// The version number.
let version = 1
/// The story hash.
let id: String
/// The feed ID.
let feed: String
/// The date and/or time as a string.
let date: String
/// The author of the story.
let author: String
/// The title of the story.
let title: String
/// The content of the story.
let content: String
/// The URL of the image, or `nil` if none.
let imageURL: URL?
/// Keys for the dictionary representation.
struct DictionaryKeys {
static let id = "story_hash"
static let feed = "story_feed_id"
static let date = "short_parsed_date"
static let author = "story_authors"
static let title = "story_title"
static let content = "story_content"
static let imageURLs = "image_urls"
}
/// Initializer from a dictionary.
///
/// - Parameter dictionary: Dictionary from the server.
init(from dictionary: [String : Any]) {
id = dictionary[DictionaryKeys.id] as? String ?? ""
feed = dictionary[DictionaryKeys.feed] as? String ?? "\(dictionary[DictionaryKeys.feed] as? Int ?? 0)"
date = dictionary[DictionaryKeys.date] as? String ?? ""
author = dictionary[DictionaryKeys.author] as? String ?? ""
title = dictionary[DictionaryKeys.title] as? String ?? ""
content = dictionary[DictionaryKeys.content] as? String ?? ""
if let images = dictionary[DictionaryKeys.imageURLs] as? [String], let first = images.first {
imageURL = URL(string: first)
} else {
imageURL = nil
}
}
/// Keys for the codable representation.
enum CodingKeys: String, CodingKey {
case version = "version"
case id = "id"
case feed = "feed"
case date = "date"
case author = "author"
case title = "title"
case content = "content"
case imageURL = "imageURL"
}
/// Initializer to load from the JSON data.
///
/// - Parameter decoder: The decoder from which to read data.
/// - Throws: An error if the data is invalid.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
feed = try container.decode(String.self, forKey: .feed)
date = try container.decode(String.self, forKey: .date)
author = try container.decode(String.self, forKey: .author)
title = try container.decode(String.self, forKey: .title)
content = try container.decode(String.self, forKey: .content)
imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
}
/// Encodes the story into the given encoder.
///
/// - Parameter encoder: The encoder to which to write data.
/// - Throws: An error if the data is invalid.
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(version, forKey: .version)
try container.encode(id, forKey: .id)
try container.encode(feed, forKey: .feed)
try container.encode(date, forKey: .date)
try container.encode(author, forKey: .author)
try container.encode(title, forKey: .title)
try container.encode(content, forKey: .content)
try container.encodeIfPresent(imageURL, forKey: .imageURL)
}
}
extension Story: Equatable {
static func ==(lhs: Story, rhs: Story) -> Bool {
return lhs.id == rhs.id
}
}
extension Story: CustomStringConvertible {
var description: String {
return "Story \(title) by \(author) (\(id))"
}
}

View file

@ -0,0 +1,22 @@
//
// WidgetTableViewCell.swift
// Widget Extension
//
// Created by David Sinclair on 2019-11-26.
// Copyright © 2019 NewsBlur. All rights reserved.
//
import UIKit
class WidgetTableViewCell: UITableViewCell {
/// The reuse identifier for this table view cell.
static let reuseIdentifier = "WidgetTableViewCell"
@IBOutlet var feedImageView: UIImageView!
@IBOutlet var feedLabel: UILabel!
@IBOutlet var titleLabel: UILabel!
@IBOutlet var contentLabel: UILabel!
@IBOutlet var authorLabel: UILabel!
@IBOutlet var dateLabel: UILabel!
@IBOutlet var thumbnailImageView: UIImageView!
}

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="110" id="KGk-i7-Jjw" customClass="WidgetTableViewCell" customModule="NewsBlur_Latest" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="110"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" 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"/>
<constraint firstAttribute="height" constant="64" id="ID2-Sx-mif"/>
</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"/>
<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"/>
<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>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="el3-VK-r6t">
<rect key="frame" x="20" y="5" width="16" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="dCu-9e-TSf"/>
<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"/>
<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 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"/>
<constraint firstAttribute="bottom" secondItem="Wai-eu-jJp" secondAttribute="bottom" constant="7" id="FBY-7w-ZZz"/>
<constraint firstItem="02U-BY-b7a" firstAttribute="firstBaseline" secondItem="Wai-eu-jJp" secondAttribute="firstBaseline" id="FQl-bS-Fk3"/>
<constraint firstItem="I7A-fd-pwi" firstAttribute="top" secondItem="pv8-bs-3wb" secondAttribute="bottom" constant="8" id="KH6-H3-6wb"/>
<constraint firstItem="pv8-bs-3wb" firstAttribute="centerY" secondItem="el3-VK-r6t" secondAttribute="centerY" id="LfK-hl-HxZ"/>
<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="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="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>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="authorLabel" destination="Wai-eu-jJp" id="25m-9w-Q54"/>
<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"/>
<outlet property="feedLabel" destination="pv8-bs-3wb" id="XEb-j4-JNA"/>
<outlet property="thumbnailImageView" destination="QWb-zc-x4f" id="OKC-XS-7wv"/>
<outlet property="titleLabel" destination="I7A-fd-pwi" id="9Ic-sM-Ypk"/>
</connections>
<point key="canvasLocation" x="33.600000000000001" y="85.457271364317847"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,4 @@
/* Localized versions of Info.plist keys */
CFBundleName = "NewsBlur";
"NewsBlur Latest" = "NewsBlur";