Refactoring Part 3: Feed configuration and CoreData Model

This commit is contained in:
relikd
2018-12-09 01:49:26 +01:00
parent ae4700faca
commit 4c1ec7c474
28 changed files with 751 additions and 520 deletions

View File

@@ -25,9 +25,12 @@
@class RSParsedFeed;
@interface Feed (Ext)
- (void)updateWithRSS:(RSParsedFeed*)obj;
- (NSArray<FeedItem*>*)sortedArticles;
// Generator methods / Feed update
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)context;
- (void)calculateAndSetIndexPathString;
- (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag;
// Article properties
- (NSArray<FeedArticle*>*)sortedArticles;
- (int)markAllItemsRead;
- (int)markAllItemsUnread;
@end

View File

@@ -21,23 +21,48 @@
// SOFTWARE.
#import "Feed+Ext.h"
#import "FeedConfig+Ext.h"
#import "FeedItem+CoreDataClass.h"
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#import "FeedArticle+CoreDataClass.h"
#import "Constants.h"
#import <RSXML/RSXML.h>
@implementation Feed (Ext)
/// Instantiates new @c Feed and @c FeedMeta entities in context.
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)moc {
Feed *feed = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:moc];
feed.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:moc];
return feed;
}
/// Call @c indexPathString on @c .group and update @c .indexPath if current value is different.
- (void)calculateAndSetIndexPathString {
NSString *pthStr = [self.group indexPathString];
if (![self.indexPath isEqualToString:pthStr])
self.indexPath = pthStr;
}
#pragma mark - Update Feed Items -
/**
Replace feed title, subtitle and link (if changed). Also adds new articles and removes old ones.
*/
- (void)updateWithRSS:(RSParsedFeed*)obj {
- (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag {
if (![self.title isEqualToString:obj.title]) self.title = obj.title;
if (![self.subtitle isEqualToString:obj.subtitle]) self.subtitle = obj.subtitle;
if (![self.link isEqualToString:obj.link]) self.link = obj.link;
NSMutableSet<NSString*> *urls = [[self.items valueForKeyPath:@"link"] mutableCopy];
if ([self addMissingArticles:obj updateLinks:urls]) // will remove links in 'urls' that should be kept
int32_t unreadBefore = self.unreadCount;
NSMutableSet<NSString*> *urls = [[self.articles valueForKeyPath:@"link"] mutableCopy];
[self addMissingArticles:obj updateLinks:urls]; // will remove links in 'urls' that should be kept
if (urls.count > 0)
[self deleteArticlesWithLink:urls]; // remove old, outdated articles
if (flag) {
NSNumber *cDiff = [NSNumber numberWithInteger:self.unreadCount - unreadBefore];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff];
}
}
/**
@@ -47,7 +72,7 @@
@return @c YES if new items were added, @c NO otherwise.
*/
- (BOOL)addMissingArticles:(RSParsedFeed*)obj updateLinks:(NSMutableSet<NSString*>*)urls {
int latestID = [[self.items valueForKeyPath:@"@max.sortIndex"] intValue];
int latestID = [[self.articles valueForKeyPath:@"@max.sortIndex"] intValue];
__block int newOnes = 0;
[obj.articles enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(RSParsedArticle * _Nonnull article, BOOL * _Nonnull stop) {
// reverse enumeration ensures correct article order
@@ -68,17 +93,17 @@
Create article based on input and insert into core data storage.
*/
- (void)insertArticle:(RSParsedArticle*)entry atIndex:(int)idx {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:self.managedObjectContext];
b.sortIndex = (int32_t)idx;
b.unread = YES;
b.guid = entry.guid;
b.title = entry.title;
b.abstract = entry.abstract;
b.body = entry.body;
b.author = entry.author;
b.link = entry.link;
b.published = entry.datePublished;
[self addItemsObject:b];
FeedArticle *fa = [[FeedArticle alloc] initWithEntity:FeedArticle.entity insertIntoManagedObjectContext:self.managedObjectContext];
fa.sortIndex = (int32_t)idx;
fa.unread = YES;
fa.guid = entry.guid;
fa.title = entry.title;
fa.abstract = entry.abstract;
fa.body = entry.body;
fa.author = entry.author;
fa.link = entry.link;
fa.published = entry.datePublished;
[self addArticlesObject:fa];
}
/**
@@ -87,28 +112,29 @@
- (void)deleteArticlesWithLink:(NSMutableSet<NSString*>*)urls {
if (!urls || urls.count == 0)
return;
self.articleCount -= (int32_t)urls.count;
for (FeedItem *item in self.items) {
if ([urls containsObject:item.link]) {
[urls removeObject:item.link];
if (item.unread)
for (FeedArticle *fa in self.articles) {
if ([urls containsObject:fa.link]) {
[urls removeObject:fa.link];
if (fa.unread)
self.unreadCount -= 1;
// TODO: keep unread articles?
[item.managedObjectContext deleteObject:item];
[fa.managedObjectContext deleteObject:fa];
if (urls.count == 0)
break;
}
}
}
#pragma mark - Article Properties -
/**
@return Articles sorted by attribute @c sortIndex with descending order (newest items first).
*/
- (NSArray<FeedItem*>*)sortedArticles {
if (self.items.count == 0)
- (NSArray<FeedArticle*>*)sortedArticles {
if (self.articles.count == 0)
return nil;
return [self.items sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
return [self.articles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
}
/**
@@ -135,9 +161,9 @@
@param readFlag @c YES: mark items read; @c NO: mark items unread
*/
- (int)markAllArticlesRead:(BOOL)readFlag {
for (FeedItem *i in self.items) {
if (i.unread == readFlag)
i.unread = !readFlag;
for (FeedArticle *fa in self.articles) {
if (fa.unread == readFlag)
fa.unread = !readFlag;
}
int32_t oldCount = self.unreadCount;
int32_t newCount = (readFlag ? 0 : self.articleCount);

View File

@@ -20,28 +20,25 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "FeedConfig+CoreDataClass.h"
#import "FeedGroup+CoreDataClass.h"
@class FeedItem, RSParsedFeed;
@interface FeedConfig (Ext)
/// Enum type to distinguish different @c FeedConfig types
@interface FeedGroup (Ext)
/// Enum type to distinguish different @c FeedGroup types: @c GROUP, @c FEED, @c SEPARATOR
typedef enum int16_t {
/// Other types: @c GROUP, @c FEED, @c SEPARATOR
GROUP = 0,
FEED = 1,
SEPARATOR = 2
} FeedConfigType;
} FeedGroupType;
@property (getter=typ, setter=setTyp:) FeedConfigType typ;
@property (readonly) FeedGroupType typ;
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)context;
- (void)setName:(NSString*)name andRefreshString:(NSString*)refreshStr;
// Handle children and parents
- (NSString*)indexPathString;
- (NSMutableArray<FeedConfig*>*)allParents;
- (NSMutableArray<FeedGroup*>*)allParents;
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
// Update feed and meta
- (void)updateRSSFeed:(RSParsedFeed*)obj;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (void)calculateAndSetScheduled;
// Printing
- (NSString*)readableRefreshString;
- (NSString*)readableDescription;
@end

View File

@@ -20,16 +20,31 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "FeedConfig+Ext.h"
#import "FeedGroup+Ext.h"
#import "FeedMeta+Ext.h"
#import "Feed+Ext.h"
#import "FeedMeta+CoreDataClass.h"
#import "Constants.h"
@implementation FeedConfig (Ext)
/// Enum tpye getter see @c FeedConfigType
- (FeedConfigType)typ { return (FeedConfigType)self.type; }
/// Enum type setter see @c FeedConfigType
- (void)setTyp:(FeedConfigType)typ { self.type = typ; }
@implementation FeedGroup (Ext)
/// Enum tpye getter see @c FeedGroupType
- (FeedGroupType)typ { return (FeedGroupType)self.type; }
/// Enum type setter see @c FeedGroupType
- (void)setTyp:(FeedGroupType)typ { self.type = typ; }
/// Create new instance and set @c Feed and @c FeedMeta if group type is @c FEED
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)moc {
FeedGroup *fg = [[FeedGroup alloc] initWithEntity: FeedGroup.entity insertIntoManagedObjectContext:moc];
fg.typ = type;
if (type == FEED)
fg.feed = [Feed newFeedAndMetaInContext:moc];
return fg;
}
/// Set name and refreshStr attributes. @note Only values that differ will be updated.
- (void)setName:(NSString*)name andRefreshString:(NSString*)refreshStr {
if (![self.name isEqualToString: name]) self.name = name;
if (![self.refreshStr isEqualToString:refreshStr]) self.refreshStr = refreshStr;
}
#pragma mark - Handle Children And Parents -
@@ -43,14 +58,14 @@
}
/// @return Children sorted by attribute @c sortIndex (same order as in preferences).
- (NSArray<FeedConfig*>*)sortedChildren {
- (NSArray<FeedGroup*>*)sortedChildren {
if (self.children.count == 0)
return nil;
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
}
/// @return @c NSArray of all ancestors: First object is root. Last object is the @c FeedConfig that executed the command.
- (NSMutableArray<FeedConfig*>*)allParents {
/// @return @c NSArray of all ancestors: First object is root. Last object is the @c FeedGroup that executed the command.
- (NSMutableArray<FeedGroup*>*)allParents {
if (self.parent == nil)
return [NSMutableArray arrayWithObject:self];
NSMutableArray *arr = [self.parent allParents];
@@ -71,8 +86,8 @@
block(self.feed, &stopEarly);
if (stopEarly) return NO;
} else {
for (FeedConfig *fc in (ordered ? [self sortedChildren] : self.children)) {
if (![fc iterateSorted:ordered overDescendantFeeds:block])
for (FeedGroup *fg in (ordered ? [self sortedChildren] : self.children)) {
if (![fg iterateSorted:ordered overDescendantFeeds:block])
return NO;
}
}
@@ -80,57 +95,16 @@
}
#pragma mark - Update Feed And Meta -
/// Delete any existing feed object and parse new one. Read state will be copied.
- (void)updateRSSFeed:(RSParsedFeed*)obj {
if (!self.feed) {
self.feed = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:self.managedObjectContext];
self.feed.indexPath = [self indexPathString];
}
int32_t unreadBefore = self.feed.unreadCount;
[self.feed updateWithRSS:obj];
NSNumber *cDiff = [NSNumber numberWithInteger:self.feed.unreadCount - unreadBefore];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff];
}
/// Update FeedMeta or create new one if needed.
- (void)setEtag:(NSString*)etag modified:(NSString*)modified {
if (!self.meta) {
self.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:self.managedObjectContext];
}
if (![self.meta.httpEtag isEqualToString:etag]) self.meta.httpEtag = etag;
if (![self.meta.httpModified isEqualToString:modified]) self.meta.httpModified = modified;
}
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
- (void)calculateAndSetScheduled {
self.scheduled = [[NSDate date] dateByAddingTimeInterval:[self timeInterval]];
}
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
- (NSTimeInterval)timeInterval {
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
return self.refreshNum * unit[self.refreshUnit % 5];
}
#pragma mark - Printing -
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (NSString*)readableRefreshString {
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
}
/// @return Simplified description of the feed object.
- (NSString*)readableDescription {
switch (self.typ) {
case SEPARATOR: return @"-------------";
case GROUP: return [NSString stringWithFormat:@"%@", self.name];
case FEED:
return [NSString stringWithFormat:@"%@ (%@) - %@", self.name, self.url, [self readableRefreshString]];
return [NSString stringWithFormat:@"%@ (%@) - %@", self.name, self.feed.meta.url, self.refreshStr];
}
}

View File

@@ -0,0 +1,33 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "FeedMeta+CoreDataClass.h"
@interface FeedMeta (Ext)
- (void)setErrorAndPostponeSchedule;
- (void)calculateAndSetScheduled;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(int16_t)unit;
- (NSString*)readableRefreshString;
@end

View File

@@ -0,0 +1,71 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "FeedMeta+Ext.h"
@implementation FeedMeta (Ext)
/// Increment @c errorCount (max. 19) and set new @c scheduled (2^N seconds, max. 6 days).
- (void)setErrorAndPostponeSchedule {
int16_t n = self.errorCount + 1;
self.errorCount = (n < 1 ? 1 : (n > 19 ? 19 : n)); // between: 2 sec and 6 days
NSTimeInterval retryWaitTime = pow(2, self.errorCount); // 2^n seconds
self.scheduled = [NSDate dateWithTimeIntervalSinceNow:retryWaitTime];
}
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
- (void)calculateAndSetScheduled {
NSTimeInterval interval = [self timeInterval]; // 0 if refresh = 0 (update deactivated)
self.scheduled = (interval <= 0 ? nil : [[NSDate date] dateByAddingTimeInterval:interval]);
}
/// Set etag and modified attributes. @note Only values that differ will be updated.
- (void)setEtag:(NSString*)etag modified:(NSString*)modified {
if (![self.etag isEqualToString:etag]) self.etag = etag;
if (![self.modified isEqualToString:modified]) self.modified = modified;
}
/**
Set download url and refresh interval (popup button selection). @note Only values that differ will be updated.
@return @c YES if refresh interval has changed
*/
- (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(int16_t)unit {
BOOL intervalChanged = (self.refreshNum != refresh || self.refreshUnit != unit);
if (![self.url isEqualToString:url]) self.url = url;
if (self.refreshNum != refresh) self.refreshNum = refresh;
if (self.refreshUnit != unit) self.refreshUnit = unit;
return intervalChanged;
}
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
- (NSTimeInterval)timeInterval {
static const int unit[] = {1, 60, 3600, 86400, 604800}; // smhdw
return self.refreshNum * unit[self.refreshUnit % 5];
}
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (NSString*)readableRefreshString {
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
}
@end