Refresh interval string localizations

This commit is contained in:
relikd
2019-07-08 22:40:16 +02:00
parent 31e0821080
commit dda219b570
10 changed files with 75 additions and 91 deletions

View File

@@ -11,6 +11,7 @@ and this project does NOT adhere to [Semantic Versioning](https://semver.org/spe
- 5xx server errors have a reload button which will initiate a new download with the same URL
- Adding feed: Cmd+R will reload the same URL
- Settings, Feeds: Cmd+R will reload the data source
- Refresh interval string localizations
### Fixed
- Changed error message text when user cancels creation of new feed item
@@ -19,6 +20,8 @@ and this project does NOT adhere to [Semantic Versioning](https://semver.org/spe
### Changed
- Interface builder files replaced with code equivalent
- Settings, Feeds: Single add button for feeds, groups, and separators
- Refresh interval hotkeys set to: Cmd+1 … Cmd+6
## [0.9.4] - 2019-04-02

View File

@@ -48,5 +48,4 @@ 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

@@ -141,22 +141,10 @@
/// @return Simplified description of the feed object.
- (NSString*)readableDescription {
switch (self.type) {
case GROUP: return [NSString stringWithFormat:@"%@:", self.name];
case FEED: return [NSString stringWithFormat:@"%@ (%@)", self.name, self.feed.meta.url];
case SEPARATOR: return @"-------------";
case GROUP: return [NSString stringWithFormat:@"%@", self.name];
case FEED:
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

@@ -33,7 +33,6 @@
if (self.errorCount < 0)
self.errorCount = 0;
int16_t n = self.errorCount + 1; // always increment errorCount (can be used to indicate bad feeds)
// TODO: remove logging
#ifdef DEBUG
NSLog(@"ERROR: Feed download failed: %@ (errorCount: %d)", self.url, n);
#endif

View File

@@ -39,8 +39,10 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
@interface NSDate (Interval)
+ (nonnull NSString*)stringForInterval:(Interval)intv rounded:(BOOL)flag;
+ (TimeUnitType)unitForInterval:(Interval)intv rounded:(BOOL)flag;
+ (nullable NSString*)intStringForInterval:(Interval)intv;
+ (nonnull NSString*)floatStringForInterval:(Interval)intv;
+ (nullable NSString*)stringForRemainingTime:(NSDate*)other;
+ (Interval)floatToIntInterval:(Interval)intv;
@end

View File

@@ -24,8 +24,6 @@
#import <QuartzCore/QuartzCore.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,
@@ -55,61 +53,58 @@ static const TimeUnitType _values[] = {
@implementation NSDate (Interval)
/// If @c flag @c = @c YES, print @c 1.1f float string with single char unit: e.g., 3.3m, 1.7h.
+ (nonnull NSString*)stringForInterval:(Interval)intv rounded:(BOOL)flag {
if (flag) {
/// Short interval formatter string (e.g., '30 min', '2 hrs')
+ (nullable NSString*)intStringForInterval:(Interval)intv {
TimeUnitType unit = [self unitForInterval:intv];
Interval num = intv / unit;
NSDateComponents *dc = [[NSDateComponents alloc] init];
switch (unit) {
case TimeUnitSeconds: dc.second = num; break;
case TimeUnitMinutes: dc.minute = num; break;
case TimeUnitHours: dc.hour = num; break;
case TimeUnitDays: dc.day = num; break;
case TimeUnitWeeks: dc.weekOfMonth = num; break;
case TimeUnitYears: dc.year = num; break;
}
return [NSDateComponentsFormatter localizedStringFromDateComponents:dc unitsStyle:NSDateComponentsFormatterUnitsStyleShort];
}
/// Print @c 1.1f float string with single char unit: e.g., 3.3m, 1.7h.
+ (nonnull NSString*)floatStringForInterval:(Interval)intv {
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 [NSString stringWithFormat:@"%1.1f%c", intv / (float)_values[i], "ywdhms"[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)]];
/// Short interval formatter string for remaining time until @c other date
+ (nullable NSString*)stringForRemainingTime:(NSDate*)other {
NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init];
formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; // e.g., '30 min'
formatter.maximumUnitCount = 1;
return [formatter stringFromTimeInterval: other.timeIntervalSinceNow];
}
/// @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
/// Round uneven intervals to highest unit interval. E.g., @c 1:40>2:00 or @c 1:03>1:00
+ (Interval)floatToIntInterval:(Interval)intv {
TimeUnitType unit = _values[[self floatUnitIndexForInterval:abs(intv)]];
return (Interval)(roundf((float)intv / unit) * unit);
}
/// @return Highest integer-dividable unit. E.g., '61 minutes'
+ (TimeUnitType)unitForInterval:(Interval)intv {
if (intv == 0) return TimeUnitMinutes; // fallback to 0 minutes
for (unsigned short i = 0; i < 5; i++) // try: years -> minutes
if (intv % _values[i] == 0) return _values[i];
return TimeUnitSeconds;
}
/// @return Highest non-zero unit type. Can be used with fractions e.g., '1.1 hours'.
+ (unsigned short)floatUnitIndexForInterval:(Interval)intv {
if (intv == 0) return 4; // fallback to 0 minutes
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
@@ -122,7 +117,7 @@ static const TimeUnitType _values[] = {
/// Configure both @c NSControl elements based on the provided interval @c intv.
+ (void)setInterval:(Interval)intv forPopup:(NSPopUpButton*)popup andField:(NSTextField*)field animate:(BOOL)flag {
TimeUnitType unit = [self unitForInterval:intv rounded:NO];
TimeUnitType unit = [self unitForInterval:intv];
int num = (int)(intv / unit);
if (flag && popup.selectedTag != unit) [self animateControlSize:popup];
if (flag && field.intValue != num) [self animateControlSize:field];
@@ -133,11 +128,12 @@ static const TimeUnitType _values[] = {
/// 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 addItemsWithTitles:@[NSLocalizedString(@"Years", nil), NSLocalizedString(@"Weeks", nil),
NSLocalizedString(@"Days", nil), NSLocalizedString(@"Hours", nil),
NSLocalizedString(@"Minutes", nil), NSLocalizedString(@"Seconds", nil)]];
for (int i = 0; i < 6; i++) {
[popup itemAtIndex:i].tag = _values[i];
[popup itemAtIndex:i].keyEquivalent = [NSString stringWithFormat:@"%d", i+1]; // Cmd+1 .. Cmd+6
}
[popup selectItemWithTag:unit];
}

View File

@@ -32,7 +32,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>7749</string>
<string>8018</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>

View File

@@ -87,10 +87,10 @@ NS_INLINE NSTextField* GrayLabel(NSString *text) {
/// Inline button with tag equal to refresh interval. @c 16px height.
- (NSButton*)createInlineButton:(NSNumber*)num callback:(nullable id<RefreshIntervalButtonDelegate>)callback {
NSButton *button = [NSView inlineButton:[NSDate stringForInterval:num.intValue rounded:YES]];
TimeUnitType unit = [NSDate unitForInterval:num.intValue rounded:YES];
button.tag = (NSInteger)(roundf(num.floatValue / unit) * unit); // rounded interval
// TODO: accessibility title: readable interval string
NSButton *button = [NSView inlineButton: [NSDate floatStringForInterval:num.intValue]];
Interval intv = [NSDate floatToIntInterval:num.intValue]; // rounded to highest unit
button.accessibilityTitle = [NSDate intStringForInterval:intv];
button.tag = (NSInteger)intv;
if (callback) {
[button action:@selector(refreshIntervalButtonClicked:) target:callback];
} else {

View File

@@ -29,6 +29,7 @@
#import "OpmlExport.h"
#import "FeedDownload.h"
#import "SettingsFeedsView.h"
#import "NSDate+Ext.h"
@interface SettingsFeeds ()
@property (strong) SettingsFeedsView *view; // override super
@@ -37,7 +38,6 @@
@property (strong) NSUndoManager *undoManager;
@property (strong) NSTimer *timerStatusInfo;
@property (strong) NSDateComponentsFormatter *intervalFormatter;
@end
@implementation SettingsFeeds
@@ -94,9 +94,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
/// Initialize status info timer
- (void)viewWillAppear {
[self.dataStore rearrangeObjects]; // needed to scroll outline view to top (if prefs open on another tab)
self.intervalFormatter = [[NSDateComponentsFormatter alloc] init];
self.intervalFormatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; // e.g., '30 min'
self.intervalFormatter.maximumUnitCount = 1;
self.timerStatusInfo = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:self selector:@selector(keepTimerRunning) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timerStatusInfo forMode:NSRunLoopCommonModes];
// start spinner if update is in progress when preferences open
@@ -108,7 +105,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
// in viewWillDisappear otherwise dealloc will not be called
[self.timerStatusInfo invalidate];
self.timerStatusInfo = nil;
self.intervalFormatter = nil;
}
/// Callback method to update status info. Will be called more often when interval is getting shorter.
@@ -120,14 +116,11 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
self.view.status.stringValue = @"";
return;
}
if (nextFire > 60) { // update 1/min
nextFire = fmod(nextFire, 60); // next update will align with minute
} else {
nextFire = 1; // update 1/sec
}
NSString *str = [self.intervalFormatter stringFromTimeInterval: date.timeIntervalSinceNow];
self.view.status.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil), str];
[self.timerStatusInfo setFireDate:[NSDate dateWithTimeIntervalSinceNow: nextFire]];
self.view.status.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil),
[NSDate stringForRemainingTime:date]];
// Next update is aligned with minute (fmod) else update 1/sec
NSDate *nextUpdate = [NSDate dateWithTimeIntervalSinceNow: (nextFire > 60 ? fmod(nextFire, 60) : 1)];
[self.timerStatusInfo setFireDate:nextUpdate];
}
}

View File

@@ -93,7 +93,7 @@
NSTableColumn *colRefresh = [[NSTableColumn alloc] initWithIdentifier:CustomCellRefresh];
colRefresh.title = NSLocalizedString(@"Refresh", nil);
colRefresh.width = 50;
colRefresh.width = 60;
colRefresh.resizingMask = NSTableColumnNoResizing;
[outline addTableColumn:colRefresh];
@@ -224,15 +224,19 @@ NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
self = [super initWithFrame:frameRect];
self.identifier = CustomCellRefresh;
self.textField = [[[[NSView label:@""] textRight] placeIn:self x:0 yTop:0] sizeToRight:0];
self.textField.accessibilityLabel = NSLocalizedString(@"Refresh interval", nil);
self.textField.accessibilityTitle = @" "; // otherwise groups and separators will say 'text'
return self;
}
- (void)setObjectValue:(FeedGroup*)fg {
NSString *str = [fg refreshString];
NSString *str = @"";
if (fg.type == FEED) {
int32_t refresh = fg.feed.meta.refresh;
str = (refresh <= 0 ? @"∞" : [NSDate intStringForInterval:refresh]); // ƒ Ø
}
self.textField.objectValue = str;
// TODO: accessibility title: readable interval string
self.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
self.textField.accessibilityLabel = (str.length > 1 ? NSLocalizedString(@"Refresh interval", nil) : nil);
}
@end