From b314b6f289b33f14217f191aa5510f68d2d712d9 Mon Sep 17 00:00:00 2001 From: David Sinclair Date: Fri, 20 Dec 2019 19:09:20 -0800 Subject: [PATCH] #1162 (widget) Done! See my comment in the issue for details. --- .../ios/Classes/FeedChooserViewController.h | 3 +- .../ios/Classes/FeedChooserViewController.m | 103 +++- clients/ios/Classes/FeedsMenuViewController.m | 26 +- .../ios/Classes/FeedsMenuViewController.xib | 15 +- .../Classes/FontSettingsViewController.xib | 33 +- clients/ios/Classes/NewsBlurAppDelegate.h | 2 + clients/ios/Classes/NewsBlurAppDelegate.m | 55 +- clients/ios/Classes/NewsBlurViewController.m | 24 +- clients/ios/NewsBlur-iPhone-Info.plist | 10 + .../ios/NewsBlur.xcodeproj/project.pbxproj | 253 +++++++- .../xcschemes/Widget Extension.xcscheme | 103 ++++ clients/ios/Resources/menu_icn_widget.png | Bin 0 -> 2000 bytes clients/ios/Resources/menu_icn_widget@2x.png | Bin 0 -> 4199 bytes .../Base.lproj/MainInterface.storyboard | 32 + clients/ios/Widget Extension/Info.plist | 31 + .../Widget Extension.entitlements | 10 + .../WidgetErrorTableViewCell.swift | 16 + .../WidgetErrorTableViewCell.xib | 38 ++ .../WidgetExtensionViewController.swift | 549 ++++++++++++++++++ .../ios/Widget Extension/WidgetLoader.swift | 67 +++ .../ios/Widget Extension/WidgetStory.swift | 122 ++++ .../WidgetTableViewCell.swift | 22 + .../Widget Extension/WidgetTableViewCell.xib | 103 ++++ .../en.lproj/InfoPlist.strings | 4 + 24 files changed, 1556 insertions(+), 65 deletions(-) create mode 100644 clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme create mode 100644 clients/ios/Resources/menu_icn_widget.png create mode 100644 clients/ios/Resources/menu_icn_widget@2x.png create mode 100644 clients/ios/Widget Extension/Base.lproj/MainInterface.storyboard create mode 100644 clients/ios/Widget Extension/Info.plist create mode 100644 clients/ios/Widget Extension/Widget Extension.entitlements create mode 100644 clients/ios/Widget Extension/WidgetErrorTableViewCell.swift create mode 100644 clients/ios/Widget Extension/WidgetErrorTableViewCell.xib create mode 100644 clients/ios/Widget Extension/WidgetExtensionViewController.swift create mode 100644 clients/ios/Widget Extension/WidgetLoader.swift create mode 100644 clients/ios/Widget Extension/WidgetStory.swift create mode 100644 clients/ios/Widget Extension/WidgetTableViewCell.swift create mode 100644 clients/ios/Widget Extension/WidgetTableViewCell.xib create mode 100644 clients/ios/Widget Extension/en.lproj/InfoPlist.strings diff --git a/clients/ios/Classes/FeedChooserViewController.h b/clients/ios/Classes/FeedChooserViewController.h index 47c7c6855..7742e74ea 100644 --- a/clients/ios/Classes/FeedChooserViewController.h +++ b/clients/ios/Classes/FeedChooserViewController.h @@ -12,7 +12,8 @@ typedef NS_ENUM(NSUInteger, FeedChooserOperation) { FeedChooserOperationMuteSites = 0, - FeedChooserOperationOrganizeSites = 1 + FeedChooserOperationOrganizeSites = 1, + FeedChooserOperationWidgetSites = 2 }; diff --git a/clients/ios/Classes/FeedChooserViewController.m b/clients/ios/Classes/FeedChooserViewController.m index 1ce2465f5..3c1140328 100644 --- a/clients/ios/Classes/FeedChooserViewController.m +++ b/clients/ios/Classes/FeedChooserViewController.m @@ -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]; } diff --git a/clients/ios/Classes/FeedsMenuViewController.m b/clients/ios/Classes/FeedsMenuViewController.m index 0f4717b29..0392a9666 100644 --- a/clients/ios/Classes/FeedsMenuViewController.m +++ b/clients/ios/Classes/FeedsMenuViewController.m @@ -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; diff --git a/clients/ios/Classes/FeedsMenuViewController.xib b/clients/ios/Classes/FeedsMenuViewController.xib index 6268b07e3..337ec1826 100644 --- a/clients/ios/Classes/FeedsMenuViewController.xib +++ b/clients/ios/Classes/FeedsMenuViewController.xib @@ -1,11 +1,9 @@ - - - - + + - + @@ -22,7 +20,7 @@ - + @@ -31,7 +29,7 @@ - + @@ -44,7 +42,7 @@ - + @@ -62,6 +60,7 @@ + diff --git a/clients/ios/Classes/FontSettingsViewController.xib b/clients/ios/Classes/FontSettingsViewController.xib index 5dc6ce390..993ce9018 100644 --- a/clients/ios/Classes/FontSettingsViewController.xib +++ b/clients/ios/Classes/FontSettingsViewController.xib @@ -1,11 +1,9 @@ - - - - + + - + @@ -26,7 +24,7 @@ - + @@ -40,7 +38,7 @@ - + @@ -54,7 +52,7 @@ - + @@ -65,7 +63,7 @@ - + @@ -76,7 +74,7 @@ - + @@ -87,7 +85,7 @@ - + @@ -100,7 +98,7 @@ - + @@ -113,14 +111,15 @@ + - - - - - + + + + + diff --git a/clients/ios/Classes/NewsBlurAppDelegate.h b/clients/ios/Classes/NewsBlurAppDelegate.h index 9c5cb218c..6c7a9c1e3 100644 --- a/clients/ios/Classes/NewsBlurAppDelegate.h +++ b/clients/ios/Classes/NewsBlurAppDelegate.h @@ -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; diff --git a/clients/ios/Classes/NewsBlurAppDelegate.m b/clients/ios/Classes/NewsBlurAppDelegate.m index 3a57f180e..a51c6fc18 100644 --- a/clients/ios/Classes/NewsBlurAppDelegate.m +++ b/clients/ios/Classes/NewsBlurAppDelegate.m @@ -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]; diff --git a/clients/ios/Classes/NewsBlurViewController.m b/clients/ios/Classes/NewsBlurViewController.m index 3fea06974..a1fb0bd77 100644 --- a/clients/ios/Classes/NewsBlurViewController.m +++ b/clients/ios/Classes/NewsBlurViewController.m @@ -566,9 +566,10 @@ static NSArray *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 *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 diff --git a/clients/ios/NewsBlur-iPhone-Info.plist b/clients/ios/NewsBlur-iPhone-Info.plist index 1e71fd74b..403bb1422 100644 --- a/clients/ios/NewsBlur-iPhone-Info.plist +++ b/clients/ios/NewsBlur-iPhone-Info.plist @@ -56,6 +56,16 @@ pocketapp16638 + + CFBundleTypeRole + Editor + CFBundleURLName + com.newsblur.NewsBlur.widget + CFBundleURLSchemes + + newsblurwidget + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/clients/ios/NewsBlur.xcodeproj/project.pbxproj b/clients/ios/NewsBlur.xcodeproj/project.pbxproj index 097bf25e2..a75c3b496 100755 --- a/clients/ios/NewsBlur.xcodeproj/project.pbxproj +++ b/clients/ios/NewsBlur.xcodeproj/project.pbxproj @@ -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 = ""; }; 010EDEF91B2386B7003B79DE /* OnePasswordExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OnePasswordExtension.h; path = "Other Sources/OnePasswordExtension/OnePasswordExtension.h"; sourceTree = ""; }; 010EDEFB1B238722003B79DE /* 1Password.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = 1Password.xcassets; path = "Other Sources/OnePasswordExtension/1Password.xcassets"; sourceTree = ""; }; + 17042DB82391D68A001BCD32 /* WidgetStory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetStory.swift; sourceTree = ""; }; + 17042DBA23922A4D001BCD32 /* WidgetLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLoader.swift; sourceTree = ""; }; 1715D0292166B3F900227731 /* PremiumManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PremiumManager.h; sourceTree = ""; }; 1715D02A2166B3F900227731 /* PremiumManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PremiumManager.m; sourceTree = ""; }; 17362ADB23639B4E00A0FCCC /* OfflineFetchText.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OfflineFetchText.h; path = offline/OfflineFetchText.h; sourceTree = ""; }; @@ -684,10 +707,17 @@ 1750658E1C5730FB00072BF5 /* barbutton_selection_off@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "barbutton_selection_off@3x.png"; sourceTree = ""; }; 175696A41C596ABC004C128D /* menu_icn_all.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_all.png; sourceTree = ""; }; 175696A51C596ABC004C128D /* menu_icn_all@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_all@2x.png"; sourceTree = ""; }; + 175FAC4A23AB34EB002AC38C /* menu_icn_widget.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_widget.png; sourceTree = ""; }; + 175FAC4B23AB34EB002AC38C /* menu_icn_widget@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_widget@2x.png"; sourceTree = ""; }; 1761295C1C630AEB00702FE4 /* mute_feed_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mute_feed_off.png; sourceTree = ""; }; 1761295D1C630AEB00702FE4 /* mute_feed_off@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mute_feed_off@2x.png"; sourceTree = ""; }; 1761295E1C630AEB00702FE4 /* mute_feed_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mute_feed_on.png; sourceTree = ""; }; 1761295F1C630AEB00702FE4 /* mute_feed_on@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mute_feed_on@2x.png"; sourceTree = ""; }; + 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 = ""; }; + 177551DC238E228A00E27818 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 177551E3238E26BF00E27818 /* Widget Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Widget Extension.entitlements"; sourceTree = ""; }; 17876B9A1C9911D40055DD15 /* g_icn_folder_rss_sm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_folder_rss_sm.png; sourceTree = ""; }; 17876B9B1C9911D40055DD15 /* g_icn_folder_rss_sm@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "g_icn_folder_rss_sm@2x.png"; sourceTree = ""; }; 17876B9C1C9911D40055DD15 /* g_icn_folder_sm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_folder_sm.png; sourceTree = ""; }; @@ -739,6 +769,8 @@ 17CBD3BE1BF66B6C003FCCAE /* MarkReadMenuViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MarkReadMenuViewController.m; sourceTree = ""; }; 17CBD3C01BF6ED2C003FCCAE /* menu_icn_markread.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_markread.png; sourceTree = ""; }; 17CBD3C11BF6ED2C003FCCAE /* menu_icn_markread@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_markread@2x.png"; sourceTree = ""; }; + 17CE3F0523AC529B003152EF /* WidgetErrorTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WidgetErrorTableViewCell.xib; sourceTree = ""; }; + 17CE3F0623AC529E003152EF /* WidgetErrorTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetErrorTableViewCell.swift; sourceTree = ""; }; 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 = ""; }; 17E635AD1C548C580075338E /* FeedChooserItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedChooserItem.h; sourceTree = ""; }; 17E635AE1C548C580075338E /* FeedChooserItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedChooserItem.m; sourceTree = ""; }; + 17E86ED3238E444B00863EC8 /* WidgetTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = WidgetTableViewCell.xib; sourceTree = ""; }; + 17E86ED4238E444B00863EC8 /* WidgetTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetTableViewCell.swift; sourceTree = ""; }; 17EB505A1BE4411E0021358B /* choose_font.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = choose_font.png; sourceTree = ""; }; 17EB505B1BE4411E0021358B /* choose_font@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "choose_font@2x.png"; sourceTree = ""; }; 17EB505E1BE46A900021358B /* FontListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FontListViewController.h; sourceTree = ""; }; 17EB505F1BE46A900021358B /* FontListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FontListViewController.m; sourceTree = ""; }; 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 = ""; }; + 17F363EF238E417300D5379D /* WidgetExtensionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetExtensionViewController.swift; sourceTree = ""; }; + 17FB51D823AC81C500F5D5BF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 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 = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 1D3623250D0F684500981E51 /* NewsBlurAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = NewsBlurAppDelegate.m; sourceTree = ""; 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 = ""; }; + 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 = ""; + }; 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 = ""; @@ -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 = ""; @@ -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 = ""; }; + 177551D9238E228A00E27818 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 177551DA238E228A00E27818 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; + 17FB51D923AC81C500F5D5BF /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 17FB51D823AC81C500F5D5BF /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; 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 = ( diff --git a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme new file mode 100644 index 000000000..a1c963474 --- /dev/null +++ b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Widget Extension.xcscheme @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/ios/Resources/menu_icn_widget.png b/clients/ios/Resources/menu_icn_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..5107a1addf7c9ad54949e2aa1a7f97d5cdc6ea21 GIT binary patch literal 2000 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/ios/Widget Extension/Info.plist b/clients/ios/Widget Extension/Info.plist new file mode 100644 index 000000000..62602bed0 --- /dev/null +++ b/clients/ios/Widget Extension/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.widget-extension + + + diff --git a/clients/ios/Widget Extension/Widget Extension.entitlements b/clients/ios/Widget Extension/Widget Extension.entitlements new file mode 100644 index 000000000..d252cd79b --- /dev/null +++ b/clients/ios/Widget Extension/Widget Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.newsblur.NewsBlur-Group + + + diff --git a/clients/ios/Widget Extension/WidgetErrorTableViewCell.swift b/clients/ios/Widget Extension/WidgetErrorTableViewCell.swift new file mode 100644 index 000000000..927497fae --- /dev/null +++ b/clients/ios/Widget Extension/WidgetErrorTableViewCell.swift @@ -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! +} diff --git a/clients/ios/Widget Extension/WidgetErrorTableViewCell.xib b/clients/ios/Widget Extension/WidgetErrorTableViewCell.xib new file mode 100644 index 000000000..2d6ade62f --- /dev/null +++ b/clients/ios/Widget Extension/WidgetErrorTableViewCell.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/ios/Widget Extension/WidgetExtensionViewController.swift b/clients/ios/Widget Extension/WidgetExtensionViewController.swift new file mode 100644 index 000000000..f649b3c85 --- /dev/null +++ b/clients/ios/Widget Extension/WidgetExtensionViewController.swift @@ -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) { + 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 + } +} diff --git a/clients/ios/Widget Extension/WidgetLoader.swift b/clients/ios/Widget Extension/WidgetLoader.swift new file mode 100644 index 000000000..7ad047c68 --- /dev/null +++ b/clients/ios/Widget Extension/WidgetLoader.swift @@ -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) -> 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)) + } + } +} diff --git a/clients/ios/Widget Extension/WidgetStory.swift b/clients/ios/Widget Extension/WidgetStory.swift new file mode 100644 index 000000000..37158d12c --- /dev/null +++ b/clients/ios/Widget Extension/WidgetStory.swift @@ -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))" + } +} diff --git a/clients/ios/Widget Extension/WidgetTableViewCell.swift b/clients/ios/Widget Extension/WidgetTableViewCell.swift new file mode 100644 index 000000000..3c8b224f0 --- /dev/null +++ b/clients/ios/Widget Extension/WidgetTableViewCell.swift @@ -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! +} diff --git a/clients/ios/Widget Extension/WidgetTableViewCell.xib b/clients/ios/Widget Extension/WidgetTableViewCell.xib new file mode 100644 index 000000000..419182046 --- /dev/null +++ b/clients/ios/Widget Extension/WidgetTableViewCell.xib @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/ios/Widget Extension/en.lproj/InfoPlist.strings b/clients/ios/Widget Extension/en.lproj/InfoPlist.strings new file mode 100644 index 000000000..c1a7ae9c0 --- /dev/null +++ b/clients/ios/Widget Extension/en.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +/* Localized versions of Info.plist keys */ + +CFBundleName = "NewsBlur"; +"NewsBlur Latest" = "NewsBlur";