OPML export / import + bug fixes + Refactoring (RSXML 2.0, StoreCoordinator, Feed type)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
|
||||
Reference in New Issue
Block a user