Refactoring refresh interval handling
This commit is contained in:
14
README.md
14
README.md
@@ -26,12 +26,24 @@ All basic functionality is there. What's missing?
|
|||||||
|
|
||||||
- Authenticated feeds
|
- Authenticated feeds
|
||||||
- Online sync with other services
|
- Online sync with other services
|
||||||
- Automatic feed detection (e.g., YouTube)
|
|
||||||
- Text / UI localisation
|
- Text / UI localisation
|
||||||
|
- App icon & UI icons
|
||||||
|
|
||||||
All in all, the software is in a usable state. The remaining features will be added in the coming weeks.
|
All in all, the software is in a usable state. The remaining features will be added in the coming weeks.
|
||||||
|
|
||||||
|
|
||||||
|
Hidden options
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1) When holding down the option key, the menu will show an item to open only a few unread items at a time. This number can be changed with the following Terminal command (default: 10):
|
||||||
|
|
||||||
|
defaults write de.relikd.baRSS openFewLinksLimit -int 10
|
||||||
|
|
||||||
|
2) In preferences you can choose to show 'Short article names'. This will limit the number of displayed characters to 60 (default). With this Terminal command you can customize this number:
|
||||||
|
|
||||||
|
defaults write de.relikd.baRSS shortArticleNamesLimit -int 50
|
||||||
|
|
||||||
|
|
||||||
ToDo
|
ToDo
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; };
|
54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; };
|
||||||
54ACC29521061E270020715F /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* FeedDownload.m */; };
|
54ACC29521061E270020715F /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* FeedDownload.m */; };
|
||||||
54ACC29821061FBA0020715F /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29721061FBA0020715F /* Preferences.m */; };
|
54ACC29821061FBA0020715F /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29721061FBA0020715F /* Preferences.m */; };
|
||||||
|
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54BB048821FD2AB500C303A5 /* NSDate+Ext.m */; };
|
||||||
54CC04382162532A00A48795 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CC04372162532A00A48795 /* main.m */; };
|
54CC04382162532A00A48795 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CC04372162532A00A48795 /* main.m */; };
|
||||||
54CC043E2162566900A48795 /* baRSS-Helper.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 54CC042C2162532800A48795 /* baRSS-Helper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
54CC043E2162566900A48795 /* baRSS-Helper.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 54CC042C2162532800A48795 /* baRSS-Helper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E8831E211B509D00064188 /* ModalFeedEdit.m */; };
|
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E8831E211B509D00064188 /* ModalFeedEdit.m */; };
|
||||||
@@ -115,6 +116,8 @@
|
|||||||
54ACC29421061E270020715F /* FeedDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedDownload.m; sourceTree = "<group>"; };
|
54ACC29421061E270020715F /* FeedDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedDownload.m; sourceTree = "<group>"; };
|
||||||
54ACC29621061FBA0020715F /* Preferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = "<group>"; };
|
54ACC29621061FBA0020715F /* Preferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = "<group>"; };
|
||||||
54ACC29721061FBA0020715F /* Preferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = "<group>"; };
|
54ACC29721061FBA0020715F /* Preferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = "<group>"; };
|
||||||
|
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Ext.h"; sourceTree = "<group>"; };
|
||||||
|
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Ext.m"; sourceTree = "<group>"; };
|
||||||
54CC042C2162532800A48795 /* baRSS-Helper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "baRSS-Helper.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
54CC042C2162532800A48795 /* baRSS-Helper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "baRSS-Helper.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
54CC04362162532A00A48795 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
54CC04362162532A00A48795 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
54CC04372162532A00A48795 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
54CC04372162532A00A48795 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||||
@@ -183,6 +186,8 @@
|
|||||||
54209E932117325100F3B5EF /* DrawImage.m */,
|
54209E932117325100F3B5EF /* DrawImage.m */,
|
||||||
544936F921F1E66100DEE9AA /* Statistics.h */,
|
544936F921F1E66100DEE9AA /* Statistics.h */,
|
||||||
544936FA21F1E66100DEE9AA /* Statistics.m */,
|
544936FA21F1E66100DEE9AA /* Statistics.m */,
|
||||||
|
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */,
|
||||||
|
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */,
|
||||||
);
|
);
|
||||||
path = Helper;
|
path = Helper;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -411,6 +416,7 @@
|
|||||||
544B011D2114EE9100386E5C /* AppHook.m in Sources */,
|
544B011D2114EE9100386E5C /* AppHook.m in Sources */,
|
||||||
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */,
|
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */,
|
||||||
54ACC29521061E270020715F /* FeedDownload.m in Sources */,
|
54ACC29521061E270020715F /* FeedDownload.m in Sources */,
|
||||||
|
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */,
|
||||||
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
|
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
|
||||||
544936FB21F1E66100DEE9AA /* Statistics.m in Sources */,
|
544936FB21F1E66100DEE9AA /* Statistics.m in Sources */,
|
||||||
540F704521B6C16C0022E69D /* FeedMeta+Ext.m in Sources */,
|
540F704521B6C16C0022E69D /* FeedMeta+Ext.m in Sources */,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
#import "FeedArticle+CoreDataClass.h"
|
#import "FeedArticle+CoreDataClass.h"
|
||||||
#import "StoreCoordinator.h"
|
#import "StoreCoordinator.h"
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <RSXML/RSXML.h>
|
#import <RSXML/RSXML.h>
|
||||||
|
|
||||||
@implementation Feed (Ext)
|
@implementation Feed (Ext)
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
NSInteger lastIndex = [StoreCoordinator numberRootItemsInContext:moc];
|
NSInteger lastIndex = [StoreCoordinator numberRootItemsInContext:moc];
|
||||||
FeedGroup *fg = [FeedGroup newGroup:FEED inContext:moc];
|
FeedGroup *fg = [FeedGroup newGroup:FEED inContext:moc];
|
||||||
[fg setParent:nil andSortIndex:(int32_t)lastIndex];
|
[fg setParent:nil andSortIndex:(int32_t)lastIndex];
|
||||||
[fg.feed.meta setRefresh:30 unit:RefreshUnitMinutes];
|
[fg.feed.meta setRefreshAndSchedule:kDefaultFeedRefreshInterval];
|
||||||
return fg.feed;
|
return fg.feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,4 +44,5 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
|
|||||||
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
|
- (BOOL)iterateSorted:(BOOL)ordered overDescendantFeeds:(void(^)(Feed *feed, BOOL* cancel))block;
|
||||||
// Printing
|
// Printing
|
||||||
- (NSString*)readableDescription;
|
- (NSString*)readableDescription;
|
||||||
|
- (nonnull NSString*)refreshString;
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -23,8 +23,7 @@
|
|||||||
#import "FeedGroup+Ext.h"
|
#import "FeedGroup+Ext.h"
|
||||||
#import "FeedMeta+Ext.h"
|
#import "FeedMeta+Ext.h"
|
||||||
#import "Feed+Ext.h"
|
#import "Feed+Ext.h"
|
||||||
|
#import "NSDate+Ext.h"
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
|
||||||
@implementation FeedGroup (Ext)
|
@implementation FeedGroup (Ext)
|
||||||
|
|
||||||
@@ -118,8 +117,19 @@
|
|||||||
case SEPARATOR: return @"-------------";
|
case SEPARATOR: return @"-------------";
|
||||||
case GROUP: return [NSString stringWithFormat:@"%@", self.name];
|
case GROUP: return [NSString stringWithFormat:@"%@", self.name];
|
||||||
case FEED:
|
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
|
@end
|
||||||
|
|||||||
@@ -22,22 +22,14 @@
|
|||||||
|
|
||||||
#import "FeedMeta+CoreDataClass.h"
|
#import "FeedMeta+CoreDataClass.h"
|
||||||
|
|
||||||
/// Easy memorable @c int16_t enum for refresh unit index
|
static const int32_t kDefaultFeedRefreshInterval = 30 * 60;
|
||||||
typedef NS_ENUM(int16_t, RefreshUnitType) {
|
|
||||||
RefreshUnitSeconds = 0, RefreshUnitMinutes = 1, RefreshUnitHours = 2, RefreshUnitDays = 3, RefreshUnitWeeks = 4
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@interface FeedMeta (Ext)
|
@interface FeedMeta (Ext)
|
||||||
@property (readonly) BOOL refreshIntervalDisabled; // self.refreshNum <= 0
|
|
||||||
@property (readonly) int32_t refreshInterval; // self.refreshNum * RefreshUnitValue
|
|
||||||
|
|
||||||
// HTTP response
|
// HTTP response
|
||||||
- (void)setErrorAndPostponeSchedule;
|
- (void)setErrorAndPostponeSchedule;
|
||||||
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response;
|
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response;
|
||||||
// Setter
|
// Setter
|
||||||
- (void)setUrlIfChanged:(NSString*)url;
|
- (void)setUrlIfChanged:(NSString*)url;
|
||||||
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
|
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
|
||||||
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit;
|
- (BOOL)setRefreshAndSchedule:(int32_t)refresh;
|
||||||
- (BOOL)setRefreshAndUnitFromInterval:(int32_t)interval;
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -24,30 +24,8 @@
|
|||||||
#import "Feed+Ext.h"
|
#import "Feed+Ext.h"
|
||||||
#import "FeedGroup+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)
|
@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
|
#pragma mark - HTTP response
|
||||||
|
|
||||||
/// Increment @c errorCount and set new @c scheduled date (2^N minutes, max. 5.7 days).
|
/// 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
|
self.errorCount = 0; // reset counter
|
||||||
NSDictionary *header = [response allHeaderFields];
|
NSDictionary *header = [response allHeaderFields];
|
||||||
[self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified"
|
[self setEtag:header[@"Etag"] modified:header[@"Date"]]; // @"Expires", @"Last-Modified"
|
||||||
[self scheduleNow:[self refreshInterval]];
|
[self scheduleNow:self.refresh];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Setter
|
#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.
|
Set @c refresh and calculate new @c scheduled date.
|
||||||
Also, calculate and set new @c scheduled date and update FeedGroup @c refreshStr (if changed).
|
|
||||||
|
|
||||||
@return @c YES if refresh interval has changed
|
@return @c YES if refresh interval has changed
|
||||||
*/
|
*/
|
||||||
- (BOOL)setRefresh:(int32_t)refresh unit:(RefreshUnitType)unit {
|
- (BOOL)setRefreshAndSchedule:(int32_t)refresh {
|
||||||
BOOL intervalChanged = (self.refreshNum != refresh || self.refreshUnit != unit);
|
if (self.refresh != refresh) {
|
||||||
if (self.refreshNum != refresh) self.refreshNum = refresh;
|
self.refresh = refresh;
|
||||||
if (self.refreshUnit != unit) self.refreshUnit = unit;
|
[self scheduleNow:self.refresh];
|
||||||
|
return YES;
|
||||||
if (intervalChanged) {
|
|
||||||
[self scheduleNow:[self refreshInterval]];
|
|
||||||
NSString *str = [self readableRefreshString];
|
|
||||||
if (![self.feed.group.refreshStr isEqualToString:str])
|
|
||||||
self.feed.group.refreshStr = str;
|
|
||||||
}
|
}
|
||||||
return intervalChanged;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Set next scheduled feed update or @c nil if @c refresh @c <= @c 0.
|
||||||
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.
|
|
||||||
- (void)scheduleNow:(NSTimeInterval)future {
|
- (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
|
if (self.scheduled != nil) // already nil? Avoid unnecessary core data edits
|
||||||
self.scheduled = nil;
|
self.scheduled = nil;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
// TODO: Add support for media player? image feed?
|
// TODO: Add support for media player? image feed?
|
||||||
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
|
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
|
||||||
// TODO: Disable 'update all' menu item during update?
|
// 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.
|
@c notification.object is @c NSNumber of type @c NSUInteger.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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">
|
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
</entity>
|
</entity>
|
||||||
<entity name="FeedGroup" representedClassName="FeedGroup" syncable="YES" codeGenerationType="class">
|
<entity name="FeedGroup" representedClassName="FeedGroup" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
<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="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"/>
|
<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"/>
|
<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="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="modified" 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="refresh" 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="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="url" optional="YES" attributeType="String" 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"/>
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed" syncable="YES"/>
|
||||||
@@ -49,8 +47,8 @@
|
|||||||
<elements>
|
<elements>
|
||||||
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="180"/>
|
<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="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="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>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
24
baRSS/Helper/NSDate+Ext.h
Normal file
24
baRSS/Helper/NSDate+Ext.h
Normal 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
101
baRSS/Helper/NSDate+Ext.m
Normal 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
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
#import "FeedMeta+Ext.h"
|
#import "FeedMeta+Ext.h"
|
||||||
#import "FeedGroup+Ext.h"
|
#import "FeedGroup+Ext.h"
|
||||||
#import "Statistics.h"
|
#import "Statistics.h"
|
||||||
|
#import "NSDate+Ext.h"
|
||||||
|
|
||||||
#import <QuartzCore/QuartzCore.h>
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
self.previousURL = @"";
|
self.previousURL = @"";
|
||||||
self.refreshNum.intValue = 30;
|
self.refreshNum.intValue = 30;
|
||||||
|
[NSDate populateUnitsMenu:self.refreshUnit selected:TimeUnitMinutes];
|
||||||
self.warningIndicator.image = nil;
|
self.warningIndicator.image = nil;
|
||||||
[self.warningIndicator.cell setHighlightsBy:NSNoCellMask];
|
[self.warningIndicator.cell setHighlightsBy:NSNoCellMask];
|
||||||
[self populateTextFields:self.feedGroup];
|
[self populateTextFields:self.feedGroup];
|
||||||
@@ -102,12 +105,8 @@
|
|||||||
self.name.objectValue = fg.name;
|
self.name.objectValue = fg.name;
|
||||||
self.url.objectValue = fg.feed.meta.url;
|
self.url.objectValue = fg.feed.meta.url;
|
||||||
self.previousURL = self.url.stringValue;
|
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];
|
self.warningIndicator.image = [fg.feed iconImage16];
|
||||||
|
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.refreshUnit andField:self.refreshNum];
|
||||||
[self statsForCoreDataObject];
|
[self statsForCoreDataObject];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +121,8 @@
|
|||||||
[self.feedGroup setNameIfChanged:self.name.stringValue];
|
[self.feedGroup setNameIfChanged:self.name.stringValue];
|
||||||
FeedMeta *meta = feed.meta;
|
FeedMeta *meta = feed.meta;
|
||||||
[meta setUrlIfChanged:self.previousURL];
|
[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) {
|
if (self.didDownloadFeed) {
|
||||||
[meta setEtag:self.httpEtag modified:self.httpDate];
|
[meta setEtag:self.httpEtag modified:self.httpDate];
|
||||||
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
|
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
|
||||||
|
|||||||
@@ -86,24 +86,12 @@
|
|||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TUi-VS-ge4">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TUi-VS-ge4">
|
||||||
<rect key="frame" x="198" y="-3" width="125" height="26"/>
|
<rect key="frame" x="198" y="-3" width="125" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<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"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="menu"/>
|
<font key="font" metaFont="menu"/>
|
||||||
<menu key="menu" showsStateColumn="NO" autoenablesItems="NO" id="7hX-7Y-rtT">
|
<menu key="menu" showsStateColumn="NO" autoenablesItems="NO" id="7hX-7Y-rtT">
|
||||||
<items>
|
<items>
|
||||||
<menuItem title="Seconds" keyEquivalent="s" id="VD1-1h-Hdh">
|
<menuItem title="-- list --" id="lQ1-ai-wYn">
|
||||||
<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">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
</items>
|
</items>
|
||||||
|
|||||||
@@ -170,9 +170,9 @@
|
|||||||
meta.url = [item attributeForKey:OPMLXMLURLKey];
|
meta.url = [item attributeForKey:OPMLXMLURLKey];
|
||||||
id refresh = [item attributeForKey:@"refreshInterval"]; // baRSS specific
|
id refresh = [item attributeForKey:@"refreshInterval"]; // baRSS specific
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
[meta setRefreshAndUnitFromInterval:(int32_t)[refresh integerValue]];
|
[meta setRefreshAndSchedule:(int32_t)[refresh integerValue]];
|
||||||
} else {
|
} else {
|
||||||
[meta setRefresh:30 unit:RefreshUnitMinutes];
|
[meta setRefreshAndSchedule:kDefaultFeedRefreshInterval]; // TODO: set -1, then auto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[list addObject:newFeed.feed];
|
[list addObject:newFeed.feed];
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
[outline addAttribute:[NSXMLNode attributeWithName:OPMLHMTLURLKey stringValue:item.feed.link]];
|
[outline addAttribute:[NSXMLNode attributeWithName:OPMLHMTLURLKey stringValue:item.feed.link]];
|
||||||
[outline addAttribute:[NSXMLNode attributeWithName:OPMLXMLURLKey stringValue:item.feed.meta.url]];
|
[outline addAttribute:[NSXMLNode attributeWithName:OPMLXMLURLKey stringValue:item.feed.meta.url]];
|
||||||
[outline addAttribute:[NSXMLNode attributeWithName:OPMLTypeKey stringValue:@"rss"]];
|
[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
|
[outline addAttribute:[NSXMLNode attributeWithName:@"refreshInterval" stringValue:intervalStr]]; // baRSS specific
|
||||||
// TODO: option to export unread state?
|
// TODO: option to export unread state?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,8 +408,9 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
|||||||
NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil];
|
NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil];
|
||||||
|
|
||||||
if (isRefreshColumn) {
|
if (isRefreshColumn) {
|
||||||
cellView.textField.objectValue = fg.refreshStr;
|
NSString *str = [fg refreshString];
|
||||||
cellView.textField.textColor = (fg.refreshStr.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
|
cellView.textField.stringValue = str;
|
||||||
|
cellView.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
|
||||||
} else if (isSeperator) {
|
} else if (isSeperator) {
|
||||||
return cellView; // refresh cell already skipped with the above if condition
|
return cellView; // refresh cell already skipped with the above if condition
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user