Refactoring refresh interval handling

This commit is contained in:
relikd
2019-01-27 04:01:07 +01:00
parent f0258fb246
commit cd0a1a3fd7
15 changed files with 190 additions and 103 deletions

View File

@@ -29,7 +29,6 @@
#import "FeedArticle+CoreDataClass.h"
#import "StoreCoordinator.h"
#import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@implementation Feed (Ext)
@@ -46,7 +45,7 @@
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];
[fg.feed.meta setRefreshAndSchedule:kDefaultFeedRefreshInterval];
return fg.feed;
}

View File

@@ -44,4 +44,5 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
// Printing
- (NSString*)readableDescription;
- (nonnull NSString*)refreshString;
@end

View File

@@ -23,8 +23,7 @@
#import "FeedGroup+Ext.h"
#import "FeedMeta+Ext.h"
#import "Feed+Ext.h"
#import <Cocoa/Cocoa.h>
#import "NSDate+Ext.h"
@implementation FeedGroup (Ext)
@@ -118,8 +117,19 @@
case SEPARATOR: return @"-------------";
case GROUP: return [NSString stringWithFormat:@"%@", self.name];
case FEED:
return [NSString stringWithFormat:@"%@ (%@) - %@", self.name, self.feed.meta.url, self.refreshStr];
return [NSString stringWithFormat:@"%@ (%@) - %@", self.name, self.feed.meta.url, [self refreshString]];
}
}
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (nonnull NSString*)refreshString {
if (self.type == FEED) {
int32_t refresh = self.feed.meta.refresh;
if (refresh <= 0)
return @"∞"; // ƒ Ø
return [NSDate stringForInterval:refresh rounded:NO];
}
return @"";
}
@end

View File

@@ -22,22 +22,14 @@
#import "FeedMeta+CoreDataClass.h"
/// Easy memorable @c int16_t enum for refresh unit index
typedef NS_ENUM(int16_t, RefreshUnitType) {
RefreshUnitSeconds = 0, RefreshUnitMinutes = 1, RefreshUnitHours = 2, RefreshUnitDays = 3, RefreshUnitWeeks = 4
};
static const int32_t kDefaultFeedRefreshInterval = 30 * 60;
@interface FeedMeta (Ext)
@property (readonly) BOOL refreshIntervalDisabled; // self.refreshNum <= 0
@property (readonly) int32_t refreshInterval; // self.refreshNum * RefreshUnitValue
// HTTP response
- (void)setErrorAndPostponeSchedule;
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response;
// Setter
- (void)setUrlIfChanged:(NSString*)url;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit;
- (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval;
- (BOOL)setRefreshAndSchedule:(int32_t)refresh;
@end

View File

@@ -24,30 +24,8 @@
#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)
#pragma mark - Getter
/// Check whether update interval is disabled by user (refresh interval is 0).
- (BOOL)refreshIntervalDisabled {
return (self.refreshNum <= 0);
}
/// @return Time interval respecting the selected unit. E.g., returns @c 180 for @c '3m'
- (int32_t)refreshInterval {
return self.refreshNum * RefreshUnitValues[self.refreshUnit % 5];
}
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
- (NSString*)readableRefreshString {
if (self.refreshIntervalDisabled)
return @"∞"; // ƒ Ø
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
}
#pragma mark - HTTP response
/// Increment @c errorCount and set new @c scheduled date (2^N minutes, max. 5.7 days).
@@ -68,7 +46,7 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd
self.errorCount = 0; // reset counter
NSDictionary *header = [response allHeaderFields];
[self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified"
[self scheduleNow:[self refreshInterval]];
[self scheduleNow:self.refresh];
}
#pragma mark - Setter
@@ -85,44 +63,22 @@ static const int32_t RefreshUnitValues[] = {1, 60, 3600, 86400, 604800}; // smhd
}
/**
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).
Set @c refresh and calculate new @c scheduled date.
@return @c YES if refresh interval has changed
*/
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit {
BOOL intervalChanged = (self.refreshNum != refresh || self.refreshUnit != unit);
if (self.refreshNum != refresh) self.refreshNum = refresh;
if (self.refreshUnit != unit) self.refreshUnit = unit;
if (intervalChanged) {
[self scheduleNow:[self refreshInterval]];
NSString *str = [self readableRefreshString];
if (![self.feed.group.refreshStr isEqualToString:str])
self.feed.group.refreshStr = str;
- (BOOL)setRefreshAndSchedule:(int32_t)refresh {
if (self.refresh != refresh) {
self.refresh = refresh;
[self scheduleNow:self.refresh];
return YES;
}
return intervalChanged;
return NO;
}
/**
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
}
/// Calculate date from @c refreshNum and @c refreshUnit and set as next scheduled feed update.
/// Set next scheduled feed update or @c nil if @c refresh @c <= @c 0.
- (void)scheduleNow:(NSTimeInterval)future {
if (self.refreshIntervalDisabled) { // update deactivated; manually update with force update all
if (self.refresh <= 0) { // update deactivated; manually update with force update all
if (self.scheduled != nil) // already nil? Avoid unnecessary core data edits
self.scheduled = nil;
} else {

View File

@@ -26,8 +26,7 @@
// TODO: Add support for media player? image feed?
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
// TODO: Disable 'update all' menu item during update?
// TODO: List of hidden preferences for readme
// TODO: Do we need to search for favicon in places other than '../favicon.ico'?
/**
@c notification.object is @c NSNumber of type @c NSUInteger.

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G4015" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14315.18" systemVersion="17G5019" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1">
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
@@ -25,7 +25,6 @@
</entity>
<entity name="FeedGroup" representedClassName="FeedGroup" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refreshStr" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedGroup" inverseName="parent" inverseEntity="FeedGroup" syncable="YES"/>
@@ -40,8 +39,7 @@
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="modified" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refreshNum" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refreshUnit" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" customClassName="NSUInteger" syncable="YES"/>
<attribute name="refresh" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed" syncable="YES"/>
@@ -49,8 +47,8 @@
<elements>
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="180"/>
<element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/>
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="150"/>
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="135"/>
<element name="FeedIcon" positionX="-202.79296875" positionY="137.71875" width="128" height="75"/>
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="165"/>
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="150"/>
</elements>
</model>

24
baRSS/Helper/NSDate+Ext.h Normal file
View File

@@ -0,0 +1,24 @@
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
typedef int32_t Interval;
typedef NS_ENUM(int32_t, TimeUnitType) {
TimeUnitSeconds = 1,
TimeUnitMinutes = 60,
TimeUnitHours = 60 * 60,
TimeUnitDays = 24 * 60 * 60,
TimeUnitWeeks = 7 * 24 * 60 * 60,
TimeUnitYears = 365 * 24 * 60 * 60
};
@interface NSDate (Ext)
+ (nonnull NSString*)stringForInterval:(Interval)intv rounded:(BOOL)flag;
@end
@interface NSDate (RefreshControlsUI)
+ (Interval)intervalForPopup:(NSPopUpButton*)unit andField:(NSTextField*)value;
+ (void)setInterval:(Interval)intv forPopup:(NSPopUpButton*)popup andField:(NSTextField*)field;
+ (void)populateUnitsMenu:(NSPopUpButton*)popup selected:(TimeUnitType)unit;
@end

101
baRSS/Helper/NSDate+Ext.m Normal file
View File

@@ -0,0 +1,101 @@
#import "NSDate+Ext.h"
static const char _shortnames[] = {'y','w','d','h','m','s'};
static const char *_names[] = {"Years", "Weeks", "Days", "Hours", "Minutes", "Seconds"};
static const TimeUnitType _values[] = {
TimeUnitYears,
TimeUnitWeeks,
TimeUnitDays,
TimeUnitHours,
TimeUnitMinutes,
TimeUnitSeconds,
};
@implementation NSDate (Ext)
+ (nonnull NSString*)stringForInterval:(Interval)intv rounded:(BOOL)flag {
if (flag) {
unsigned short i = [self floatUnitIndexForInterval:abs(intv)];
return [NSString stringWithFormat:@"%1.1f%c", intv / (float)_values[i], _shortnames[i]];
}
unsigned short i = [self exactUnitIndexForInterval:abs(intv)];
return [NSString stringWithFormat:@"%d%c", intv / _values[i], _shortnames[i]];
}
/// @return Highest non-zero unit ( @c flag=YES ). Or highest integer-dividable unit ( @c flag=NO ).
+ (TimeUnitType)unitForInterval:(Interval)intv rounded:(BOOL)flag {
if (flag) {
return _values[[self floatUnitIndexForInterval:abs(intv)]];
}
return _values[[self exactUnitIndexForInterval:abs(intv)]];
}
/// @return Highest unit type that allows integer division. E.g., '61 minutes'.
+ (unsigned short)exactUnitIndexForInterval:(Interval)intv {
for (unsigned short i = 0; i < 5; i++)
if (intv % _values[i] == 0) return i;
return 5; // seconds
}
/// @return Highest non-zero unit type. Can be used with fractions e.g., '1.1 hours'.
+ (unsigned short)floatUnitIndexForInterval:(Interval)intv {
for (unsigned short i = 0; i < 5; i++)
if (intv > _values[i]) return i;
return 5; // seconds
}
/* NOT USED
/// Convert any unit to the next smaller one. Unit does not have to be exact.
+ (TimeUnitType)smallerUnit:(TimeUnitType)unit {
if (unit <= TimeUnitHours) return TimeUnitSeconds;
if (unit <= TimeUnitDays) return TimeUnitMinutes; // > hours
if (unit <= TimeUnitWeeks) return TimeUnitHours; // > days
if (unit <= TimeUnitYears) return TimeUnitDays; // > weeks
return TimeUnitWeeks; // > years
}
/// @return Formatted string from @c timeIntervalSinceNow.
- (nonnull NSString*)intervalStringWithDecimal:(BOOL)flag {
return [NSDate stringForInterval:(Interval)[self timeIntervalSinceNow] rounded:flag];
}
/// @return Highest non-zero unit ( @c flag=YES ). Or highest integer-dividable unit ( @c flag=NO ).
- (TimeUnitType)unitWithDecimal:(BOOL)flag {
Interval absIntv = abs((Interval)[self timeIntervalSinceNow]);
if (flag) {
return _values[ [NSDate floatUnitIndexForInterval:absIntv] ];
}
return _values[ [NSDate exactUnitIndexForInterval:absIntv] ];
}
*/
@end
@implementation NSDate (RefreshControlsUI)
/// @return Interval by multiplying the text field value with the currently selected popup unit.
+ (Interval)intervalForPopup:(NSPopUpButton*)unit andField:(NSTextField*)value {
return value.intValue * (Interval)unit.selectedTag;
}
/// Configure both @c NSControl elements based on the provided interval @c intv.
+ (void)setInterval:(Interval)intv forPopup:(NSPopUpButton*)popup andField:(NSTextField*)field {
TimeUnitType unit = [self unitForInterval:intv rounded:NO];
[popup selectItemWithTag:unit];
field.intValue = (int)(intv / unit);
}
/// Insert all @c TimeUnitType items into popup button. Save unit value into @c tag attribute.
+ (void)populateUnitsMenu:(NSPopUpButton*)popup selected:(TimeUnitType)unit {
[popup removeAllItems];
for (NSUInteger i = 0; i < 6; i++) {
[popup addItemWithTitle:[NSString stringWithUTF8String:_names[i]]];
NSMenuItem *item = popup.lastItem;
[item setKeyEquivalent:[[NSString stringWithFormat:@"%c", _shortnames[i]] uppercaseString]];
item.tag = _values[i];
}
[popup selectItemWithTag:unit];
}
@end

View File

@@ -27,6 +27,8 @@
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#import "Statistics.h"
#import "NSDate+Ext.h"
#import <QuartzCore/QuartzCore.h>
@@ -89,6 +91,7 @@
[super viewDidLoad];
self.previousURL = @"";
self.refreshNum.intValue = 30;
[NSDate populateUnitsMenu:self.refreshUnit selected:TimeUnitMinutes];
self.warningIndicator.image = nil;
[self.warningIndicator.cell setHighlightsBy:NSNoCellMask];
[self populateTextFields:self.feedGroup];
@@ -102,12 +105,8 @@
self.name.objectValue = fg.name;
self.url.objectValue = fg.feed.meta.url;
self.previousURL = self.url.stringValue;
self.refreshNum.intValue = fg.feed.meta.refreshNum;
NSInteger unit = (NSInteger)fg.feed.meta.refreshUnit;
if (unit < 0 || unit > self.refreshUnit.numberOfItems - 1)
unit = self.refreshUnit.numberOfItems - 1;
[self.refreshUnit selectItemAtIndex:unit];
self.warningIndicator.image = [fg.feed iconImage16];
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.refreshUnit andField:self.refreshNum];
[self statsForCoreDataObject];
}
@@ -122,7 +121,8 @@
[self.feedGroup setNameIfChanged:self.name.stringValue];
FeedMeta *meta = feed.meta;
[meta setUrlIfChanged:self.previousURL];
[meta setRefresh:self.refreshNum.intValue unit:(int16_t)self.refreshUnit.indexOfSelectedItem]; // updateTimer will be scheduled once preferences is closed
[meta setRefreshAndSchedule:[NSDate intervalForPopup:self.refreshUnit andField:self.refreshNum]];
// updateTimer will be scheduled once preferences is closed
if (self.didDownloadFeed) {
[meta setEtag:self.httpEtag modified:self.httpDate];
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];

View File

@@ -86,24 +86,12 @@
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TUi-VS-ge4">
<rect key="frame" x="198" y="-3" width="125" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Minutes" bezelStyle="rounded" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" altersStateOfSelectedItem="NO" selectedItem="CsM-KR-zzs" id="O0p-Tc-KQ1">
<popUpButtonCell key="cell" type="push" title="-- list --" bezelStyle="rounded" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" altersStateOfSelectedItem="NO" selectedItem="lQ1-ai-wYn" id="O0p-Tc-KQ1">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" showsStateColumn="NO" autoenablesItems="NO" id="7hX-7Y-rtT">
<items>
<menuItem title="Seconds" keyEquivalent="s" id="VD1-1h-Hdh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Minutes" state="on" keyEquivalent="m" id="CsM-KR-zzs">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Hours" keyEquivalent="h" id="Nqd-L9-4V8">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Days" keyEquivalent="d" id="5c2-Mb-3aw">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Weeks" keyEquivalent="w" id="mJE-8n-iKF">
<menuItem title="-- list --" id="lQ1-ai-wYn">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>

View File

@@ -170,9 +170,9 @@
meta.url = [item attributeForKey:OPMLXMLURLKey];
id refresh = [item attributeForKey:@"refreshInterval"]; // baRSS specific
if (refresh) {
[meta setRefreshAndUnitFromInterval:(int32_t)[refresh integerValue]];
[meta setRefreshAndSchedule:(int32_t)[refresh integerValue]];
} else {
[meta setRefresh:30 unit:RefreshUnitMinutes];
[meta setRefreshAndSchedule:kDefaultFeedRefreshInterval]; // TODO: set -1, then auto
}
}
[list addObject:newFeed.feed];
@@ -232,7 +232,7 @@
[outline addAttribute:[NSXMLNode attributeWithName:OPMLHMTLURLKey stringValue:item.feed.link]];
[outline addAttribute:[NSXMLNode attributeWithName:OPMLXMLURLKey stringValue:item.feed.meta.url]];
[outline addAttribute:[NSXMLNode attributeWithName:OPMLTypeKey stringValue:@"rss"]];
NSString *intervalStr = [NSString stringWithFormat:@"%d", item.feed.meta.refreshInterval];
NSString *intervalStr = [NSString stringWithFormat:@"%d", item.feed.meta.refresh];
[outline addAttribute:[NSXMLNode attributeWithName:@"refreshInterval" stringValue:intervalStr]]; // baRSS specific
// TODO: option to export unread state?
}

View File

@@ -408,8 +408,9 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil];
if (isRefreshColumn) {
cellView.textField.objectValue = fg.refreshStr;
cellView.textField.textColor = (fg.refreshStr.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
NSString *str = [fg refreshString];
cellView.textField.stringValue = str;
cellView.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
} else if (isSeperator) {
return cellView; // refresh cell already skipped with the above if condition
} else {