OPML export / import + bug fixes + Refactoring (RSXML 2.0, StoreCoordinator, Feed type)

This commit is contained in:
relikd
2019-01-14 22:50:22 +01:00
parent 2bd7078cbd
commit c391bc0b39
24 changed files with 961 additions and 342 deletions

View File

@@ -27,11 +27,14 @@
@interface Feed (Ext)
// Generator methods / Feed update
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)context;
+ (instancetype)appendToRootWithDefaultIntervalInContext:(NSManagedObjectContext*)moc;
- (void)calculateAndSetIndexPathString;
- (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag;
// Article properties
- (NSArray<FeedArticle*>*)sortedArticles;
- (int)markAllItemsRead;
- (int)markAllItemsUnread;
// Icon
- (NSImage*)iconImage16;
- (BOOL)setIcon:(NSImage*)img replaceExisting:(BOOL)overwrite;
@end

View File

@@ -27,6 +27,7 @@
#import "FeedGroup+Ext.h"
#import "FeedIcon+CoreDataClass.h"
#import "FeedArticle+CoreDataClass.h"
#import "StoreCoordinator.h"
#import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@@ -40,6 +41,15 @@
return feed;
}
/// Instantiates new @c FeedGroup with @c FEED type, set the update interval to @c 30min and @c sortIndex to last root index.
+ (instancetype)appendToRootWithDefaultIntervalInContext:(NSManagedObjectContext*)moc {
NSInteger lastIndex = [StoreCoordinator numberRootItemsInContext:moc];
FeedGroup *fg = [FeedGroup newGroup:FEED inContext:moc];
[fg setParent:nil andSortIndex:(int32_t)lastIndex];
[fg.feed.meta setRefresh:30 unit:RefreshUnitMinutes];
return fg.feed;
}
/// Call @c indexPathString on @c .group and update @c .indexPath if current value is different.
- (void)calculateAndSetIndexPathString {
NSString *pthStr = [self.group indexPathString];
@@ -59,12 +69,14 @@
if (![self.subtitle isEqualToString:obj.subtitle]) self.subtitle = obj.subtitle;
if (![self.link isEqualToString:obj.link]) self.link = obj.link;
if (self.group.name.length == 0) // in case a blank group was initialized
self.group.name = obj.title;
int32_t unreadBefore = self.unreadCount;
// Add and remove articles
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
[self deleteArticlesWithLink:urls]; // remove old, outdated articles
// Get new total article count and post unread-count-change notification
int32_t totalCount = (int32_t)self.articles.count;
if (self.articleCount != totalCount)
@@ -144,18 +156,21 @@
- (void)deleteArticlesWithLink:(NSMutableSet<NSString*>*)urls {
if (!urls || urls.count == 0)
return;
self.articleCount -= (int32_t)urls.count;
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?
[fa.managedObjectContext deleteObject:fa];
[self.managedObjectContext deleteObject:fa];
if (urls.count == 0)
break;
}
}
NSSet<FeedArticle*> *delArticles = [self.managedObjectContext deletedObjects];
if (delArticles.count > 0) {
[self removeArticles:delArticles];
}
}
@@ -217,14 +232,32 @@
return newCount - oldCount;
}
#pragma mark - Icon -
/**
@return Return @c 16x16px image. Either from core data storage or generated default RSS icon.
*/
- (NSImage*)iconImage16 {
NSData *imgData = self.icon.icon;
if (imgData) {
return [[NSImage alloc] initWithData:imgData];
} else {
if (imgData)
{
NSImage *img = [[NSImage alloc] initWithData:imgData];
[img setSize:NSMakeSize(16, 16)];
return img;
}
else if (self.articleCount == 0)
{
static NSImage *warningIcon;
if (!warningIcon) {
warningIcon = [NSImage imageNamed:NSImageNameCaution];
[warningIcon setSize:NSMakeSize(16, 16)];
}
return warningIcon;
}
else
{
static NSImage *defaultRSSIcon;
if (!defaultRSSIcon)
defaultRSSIcon = [RSSIcon iconWithSize:16];
@@ -232,4 +265,25 @@
}
}
/**
Set (or overwrite) favicon icon or delete relationship if icon is @c nil.
@param overwrite If @c NO write image only if non is set already. Use @c YES if you want to @c nil.
*/
- (BOOL)setIcon:(NSImage*)img replaceExisting:(BOOL)overwrite {
if (overwrite || !self.icon) { // write if forced or image empty
if (img && [img isValid]) {
if (!self.icon)
self.icon = [[FeedIcon alloc] initWithEntity:FeedIcon.entity insertIntoManagedObjectContext:self.managedObjectContext];
self.icon.icon = [img TIFFRepresentation];
return YES;
} else if (self.icon) {
[self.managedObjectContext deleteObject:self.icon];
self.icon = nil;
return YES;
}
}
return NO;
}
@end

View File

@@ -29,13 +29,16 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
GROUP = 0, FEED = 1, SEPARATOR = 2
};
@property (readonly) FeedGroupType typ;
/// Overwrites @c type attribute with enum. Use one of: @c GROUP, @c FEED, @c SEPARATOR.
@property (nonatomic) FeedGroupType type;
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)context;
- (void)setName:(NSString*)name andRefreshString:(NSString*)refreshStr;
- (void)setParent:(FeedGroup *)parent andSortIndex:(int32_t)sortIndex;
- (void)setNameIfChanged:(NSString*)name;
- (NSImage*)groupIconImage16;
// Handle children and parents
- (NSString*)indexPathString;
- (NSArray<FeedGroup*>*)sortedChildren;
- (NSMutableArray<FeedGroup*>*)allParents;
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
// Printing

View File

@@ -27,25 +27,27 @@
#import <Cocoa/Cocoa.h>
@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;
fg.type = 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;
- (void)setParent:(FeedGroup *)parent andSortIndex:(int32_t)sortIndex {
self.parent = parent;
self.sortIndex = sortIndex;
if (self.type == FEED)
[self.feed calculateAndSetIndexPathString];
}
/// Set @c name attribute but only if value differs.
- (void)setNameIfChanged:(NSString*)name {
if (![self.name isEqualToString: name])
self.name = name;
}
/// @return Return static @c 16x16px NSImageNameFolder image.
@@ -112,7 +114,7 @@
/// @return Simplified description of the feed object.
- (NSString*)readableDescription {
switch (self.typ) {
switch (self.type) {
case SEPARATOR: return @"-------------";
case GROUP: return [NSString stringWithFormat:@"%@", self.name];
case FEED:

View File

@@ -23,18 +23,19 @@
#import "FeedMeta+CoreDataClass.h"
@interface FeedMeta (Ext)
/// Easy memorable enum type for refresh unit index
/// Easy memorable @c int16_t enum for refresh unit index
typedef NS_ENUM(int16_t, RefreshUnitType) {
/// Other types: @c GROUP, @c FEED, @c SEPARATOR
RefreshUnitSeconds = 0, RefreshUnitMinutes = 1, RefreshUnitHours = 2, RefreshUnitDays = 3, RefreshUnitWeeks = 4
};
- (void)setErrorAndPostponeSchedule;
- (void)calculateAndSetScheduled;
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response;
- (void)setUrlIfChanged:(NSString*)url;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (void)setEtagAndModified:(NSHTTPURLResponse*)http;
- (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(RefreshUnitType)unit;
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit;
- (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval;
- (int32_t)refreshInterval;
- (NSString*)readableRefreshString;
@end

View File

@@ -21,6 +21,11 @@
// SOFTWARE.
#import "FeedMeta+Ext.h"
#import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
/// smhdw: [1, 60, 3600, 86400, 604800]
static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhdw
@implementation FeedMeta (Ext)
@@ -36,41 +41,70 @@
NSLog(@"ERROR: Feed download failed: %@ (errorCount: %d)", self.url, n);
}
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response {
self.errorCount = 0; // reset counter
NSDictionary *header = [response allHeaderFields];
[self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified"
[self calculateAndSetScheduled];
}
/// 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)
NSTimeInterval interval = [self refreshInterval]; // 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.
/// Set @c url attribute but only if value differs.
- (void)setUrlIfChanged:(NSString*)url {
if (![self.url isEqualToString:url]) self.url = url;
}
/// Set @c etag and @c modified attributes. 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;
}
/// Read header field "Etag" and "Date" and set @c .etag and @c .modified.
- (void)setEtagAndModified:(NSHTTPURLResponse*)http {
NSDictionary *header = [http allHeaderFields];
[self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified"
}
/**
Set download url and refresh interval (popup button selection). @note Only values that differ will be updated.
Set @c refresh and @c unit from popup button selection. Only values that differ will be updated.
Also, calculate and set new @c scheduled date and update FeedGroup @c refreshStr (if changed).
@return @c YES if refresh interval has changed
*/
- (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(RefreshUnitType)unit {
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)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;
if (self.refreshNum != refresh) self.refreshNum = refresh;
if (self.refreshUnit != unit) self.refreshUnit = unit;
if (intervalChanged) {
[self calculateAndSetScheduled];
NSString *str = [self readableRefreshString];
if (![self.feed.group.refreshStr isEqualToString:str])
self.feed.group.refreshStr = str;
}
return intervalChanged;
}
/**
Set properties @c refreshNum and @c refreshUnit to highest possible (integer-dividable-)unit.
Only values that differ will be updated.
Also, calculate and set new @c scheduled date and update FeedGroup @c refreshStr (if changed).
@return @c YES if refresh interval has changed
*/
- (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval {
for (RefreshUnitType i = 4; i >= 0; i--) { // start with weeks
if (interval % RefreshUnitValues[i] == 0) { // find first unit that is dividable
return [self setRefresh:abs(interval) / RefreshUnitValues[i] unit:i];
}
}
return NO; // since loop didn't return, no value was changed
}
/// @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];
- (int32_t)refreshInterval {
return self.refreshNum * RefreshUnitValues[self.refreshUnit % 5];
}
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )