Moving files around
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
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)
|
||||
+ (NSString*)dayStringISO8601;
|
||||
+ (NSString*)dayStringLocalized;
|
||||
@end
|
||||
|
||||
|
||||
@interface NSDate (Interval)
|
||||
+ (nullable NSString*)intStringForInterval:(Interval)intv;
|
||||
+ (nonnull NSString*)floatStringForInterval:(Interval)intv;
|
||||
+ (nullable NSString*)stringForRemainingTime:(NSDate*)other;
|
||||
+ (Interval)floatToIntInterval:(Interval)intv;
|
||||
@end
|
||||
|
||||
|
||||
@interface NSDate (RefreshControlsUI)
|
||||
+ (Interval)intervalForPopup:(NSPopUpButton*)unit andField:(NSTextField*)value;
|
||||
+ (void)setInterval:(Interval)intv forPopup:(NSPopUpButton*)popup andField:(NSTextField*)field animate:(BOOL)flag;
|
||||
+ (void)populateUnitsMenu:(NSPopUpButton*)popup selected:(TimeUnitType)unit;
|
||||
@end
|
||||
|
||||
|
||||
@interface NSDate (Statistics)
|
||||
+ (NSDictionary*)refreshIntervalStatistics:(NSArray<NSDate*> *)list;
|
||||
@end
|
||||
@@ -1,198 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import QuartzCore;
|
||||
#import "NSDate+Ext.h"
|
||||
|
||||
static TimeUnitType const _values[] = {
|
||||
TimeUnitYears,
|
||||
TimeUnitWeeks,
|
||||
TimeUnitDays,
|
||||
TimeUnitHours,
|
||||
TimeUnitMinutes,
|
||||
TimeUnitSeconds,
|
||||
};
|
||||
|
||||
|
||||
@implementation NSDate (Ext)
|
||||
|
||||
/// @return Day as string in iso format: @c YYYY-MM-DD'T'hh:mm:ss'Z'
|
||||
+ (NSString*)dayStringISO8601 {
|
||||
return [[[NSISO8601DateFormatter alloc] init] stringFromDate:[NSDate date]];
|
||||
// NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
|
||||
// return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
|
||||
}
|
||||
|
||||
/// @return Day as string in localized short format, e.g., @c DD.MM.YY
|
||||
+ (NSString*)dayStringLocalized {
|
||||
return [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterNoStyle];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NSDate (Interval)
|
||||
|
||||
/// 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], "ywdhms"[i]];
|
||||
}
|
||||
|
||||
/// 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];
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
@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 animate:(BOOL)flag {
|
||||
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];
|
||||
[popup selectItemWithTag:unit];
|
||||
field.intValue = num;
|
||||
}
|
||||
|
||||
/// Insert all @c TimeUnitType items into popup button. Save unit value into @c tag attribute.
|
||||
+ (void)populateUnitsMenu:(NSPopUpButton*)popup selected:(TimeUnitType)unit {
|
||||
[popup removeAllItems];
|
||||
[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];
|
||||
}
|
||||
|
||||
/// Helper method to animate @c NSControl to draw user attention. View will be scalled up in a fraction of a second.
|
||||
+ (void)animateControlSize:(NSView*)control {
|
||||
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform"];
|
||||
CATransform3D tr = CATransform3DIdentity;
|
||||
tr = CATransform3DTranslate(tr, NSMidX(control.bounds), NSMidY(control.bounds), 0);
|
||||
tr = CATransform3DScale(tr, 1.1, 1.1, 1);
|
||||
tr = CATransform3DTranslate(tr, -NSMidX(control.bounds), -NSMidY(control.bounds), 0);
|
||||
scale.toValue = [NSValue valueWithCATransform3D:tr];
|
||||
scale.duration = 0.15f;
|
||||
scale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
|
||||
[control.layer addAnimation:scale forKey:scale.keyPath];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NSDate (Statistics)
|
||||
|
||||
/**
|
||||
@return @c nil if list contains less than 2 entries. Otherwise: @{min, max, avg, median, earliest, latest}
|
||||
*/
|
||||
+ (NSDictionary*)refreshIntervalStatistics:(NSArray<NSDate*> *)list {
|
||||
if (!list || list.count == 0)
|
||||
return nil;
|
||||
|
||||
NSDate *earliest = [NSDate distantFuture];
|
||||
NSDate *latest = [NSDate distantPast];
|
||||
NSDate *prev = nil;
|
||||
NSMutableArray<NSNumber*> *differences = [NSMutableArray array];
|
||||
for (NSDate *d in list) {
|
||||
if (![d isKindOfClass:[NSDate class]]) // because valueForKeyPath: can return NSNull
|
||||
continue;
|
||||
earliest = [d earlierDate:earliest];
|
||||
latest = [d laterDate:latest];
|
||||
if (prev) {
|
||||
int dif = abs((int)[d timeIntervalSinceDate:prev]);
|
||||
[differences addObject:[NSNumber numberWithInt:dif]];
|
||||
}
|
||||
prev = d;
|
||||
}
|
||||
if (differences.count == 0)
|
||||
return nil;
|
||||
|
||||
[differences sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"integerValue" ascending:YES]]];
|
||||
|
||||
NSUInteger i = (differences.count/2);
|
||||
NSNumber *median = differences[i];
|
||||
if ((differences.count % 2) == 0) { // even feed count, use median of two values
|
||||
median = [NSNumber numberWithInteger:(median.integerValue + differences[i-1].integerValue) / 2];
|
||||
}
|
||||
return @{@"min" : differences.firstObject,
|
||||
@"max" : differences.lastObject,
|
||||
@"avg" : [differences valueForKeyPath:@"@avg.self"],
|
||||
@"median" : median,
|
||||
@"earliest" : earliest,
|
||||
@"latest" : latest };
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
@interface NSError (Ext)
|
||||
// Generators
|
||||
+ (instancetype)statusCode:(NSInteger)code reason:(nullable NSString*)reason;
|
||||
+ (instancetype)canceledByUser;
|
||||
+ (instancetype)feedURLNotFound:(NSURL*)url;
|
||||
@end
|
||||
@@ -1,129 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import RSXML2.RSXMLError;
|
||||
#import "NSError+Ext.h"
|
||||
|
||||
@implementation NSError (Ext)
|
||||
|
||||
static const char* CodeDescription(NSInteger code) {
|
||||
switch (code) {
|
||||
/* --- Informational --- */
|
||||
case 100: return "Continue";
|
||||
case 101: return "Switching Protocols";
|
||||
case 102: return "Processing";
|
||||
case 103: return "Early Hints";
|
||||
/* --- Success --- */
|
||||
case 200: return "OK";
|
||||
case 201: return "Created";
|
||||
case 202: return "Accepted";
|
||||
case 203: return "Non-Authoritative Information";
|
||||
case 204: return "No Content";
|
||||
case 205: return "Reset Content";
|
||||
case 206: return "Partial Content";
|
||||
case 207: return "Multi-Status";
|
||||
case 208: return "Already Reported";
|
||||
case 226: return "IM Used";
|
||||
/* --- Redirection --- */
|
||||
case 300: return "Multiple Choices";
|
||||
case 301: return "Moved Permanently";
|
||||
case 302: return "Found";
|
||||
case 303: return "See Other";
|
||||
case 304: return "Not Modified";
|
||||
case 305: return "Use Proxy";
|
||||
case 306: return "Switch Proxy";
|
||||
case 307: return "Temporary Redirect";
|
||||
case 308: return "Permanent Redirect";
|
||||
/* --- Client error --- */
|
||||
case 400: return "Bad Request";
|
||||
case 401: return "Unauthorized";
|
||||
case 402: return "Payment Required";
|
||||
case 403: return "Forbidden";
|
||||
case 404: return "Not Found";
|
||||
case 405: return "Method Not Allowed";
|
||||
case 406: return "Not Acceptable";
|
||||
case 407: return "Proxy Authentication Required";
|
||||
case 408: return "Request Timeout";
|
||||
case 409: return "Conflict";
|
||||
case 410: return "Gone";
|
||||
case 411: return "Length Required";
|
||||
case 412: return "Precondition Failed";
|
||||
case 413: return "Payload Too Large";
|
||||
case 414: return "URI Too Long";
|
||||
case 415: return "Unsupported Media Type";
|
||||
case 416: return "Range Not Satisfiable";
|
||||
case 417: return "Expectation Failed";
|
||||
case 418: return "I'm a teapot";
|
||||
case 421: return "Misdirected Request";
|
||||
case 422: return "Unprocessable Entity";
|
||||
case 423: return "Locked";
|
||||
case 424: return "Failed Dependency";
|
||||
case 425: return "Too Early";
|
||||
case 426: return "Upgrade Required";
|
||||
case 428: return "Precondition Required";
|
||||
case 429: return "Too Many Requests";
|
||||
case 431: return "Request Header Fields Too Large";
|
||||
case 451: return "Unavailable For Legal Reasons";
|
||||
/* --- Server error --- */
|
||||
case 500: return "Internal Server Error";
|
||||
case 501: return "Not Implemented";
|
||||
case 502: return "Bad Gateway";
|
||||
case 503: return "Service Unavailable";
|
||||
case 504: return "Gateway Timeout";
|
||||
case 505: return "HTTP Version Not Supported";
|
||||
case 506: return "Variant Also Negotiates";
|
||||
case 507: return "Insufficient Storage";
|
||||
case 508: return "Loop Detected";
|
||||
case 510: return "Not Extended";
|
||||
case 511: return "Network Authentication Required";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// | MARK: - Generators
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// Generate @c NSError from HTTP status code. E.g., @c code @c = @c 404 will return "404 Not Found".
|
||||
+ (instancetype)statusCode:(NSInteger)code reason:(nullable NSString*)reason {
|
||||
NSMutableDictionary *info = [NSMutableDictionary dictionaryWithCapacity:2];
|
||||
info[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"%ld %s.", code, CodeDescription(code)];
|
||||
if (reason) info[NSLocalizedRecoverySuggestionErrorKey] = reason;
|
||||
|
||||
NSInteger errCode = NSURLErrorUnknown;
|
||||
if (code < 500) { if (code >= 400) errCode = NSURLErrorResourceUnavailable; }
|
||||
else if (code < 600) errCode = NSURLErrorBadServerResponse;
|
||||
return [self errorWithDomain:NSURLErrorDomain code:errCode userInfo:info];
|
||||
}
|
||||
|
||||
/// Generate @c NSError for user canceled operation. With title "Operation canceled.".
|
||||
+ (instancetype)canceledByUser {
|
||||
NSDictionary *info = @{ NSLocalizedDescriptionKey: NSLocalizedString(@"Operation canceled.", nil) };
|
||||
return [self errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:info];
|
||||
}
|
||||
|
||||
/// Generate @c NSError for webpages that don't contain feed urls.
|
||||
+ (instancetype)feedURLNotFound:(NSURL*)url {
|
||||
return RSXMLMakeErrorWrongParser(RSXMLErrorExpectingFeed, RSXMLErrorExpectingHTML, url);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
@interface NSString (Ext)
|
||||
+ (NSString*)plainTextFromHTMLData:(NSData*)data;
|
||||
- (nonnull NSString*)htmlToPlainText;
|
||||
@end
|
||||
@@ -1,114 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#import "NSString+Ext.h"
|
||||
|
||||
@implementation NSString (Ext)
|
||||
|
||||
/// Init string with @c NSUTF8StringEncoding and call @c htmlToPlainText
|
||||
+ (NSString*)plainTextFromHTMLData:(NSData*)data {
|
||||
if (!data) return nil;
|
||||
return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] htmlToPlainText];
|
||||
}
|
||||
|
||||
/**
|
||||
Simple HTML parser to extract TEXT elements and semi-structured elements like list items.
|
||||
Ignores @c <head> , @c <style> and @c <script> tags.
|
||||
*/
|
||||
- (nonnull NSString*)htmlToPlainText {
|
||||
NSScanner *scanner = [NSScanner scannerWithString:self];
|
||||
scanner.charactersToBeSkipped = NSCharacterSet.newlineCharacterSet; // ! else, some spaces are dropped
|
||||
NSCharacterSet *angleBrackets = [NSCharacterSet characterSetWithCharactersInString:@"<>"];
|
||||
unichar prev = '>';
|
||||
int order = 0; // ul & ol
|
||||
NSString *skip = nil; // head, style, script
|
||||
|
||||
NSMutableString *result = [NSMutableString stringWithString:@" "];
|
||||
while ([scanner isAtEnd] == NO) {
|
||||
NSString *tag = nil;
|
||||
if ([scanner scanUpToCharactersFromSet:angleBrackets intoString:&tag]) {
|
||||
// parse html tag depending on type
|
||||
if (prev == '<') {
|
||||
if (skip) {
|
||||
// skip everything between <head>, <style>, and <script> tags
|
||||
if (CLOSE(tag, skip))
|
||||
skip = nil;
|
||||
continue;
|
||||
}
|
||||
if (OPEN(tag, @"a")) [result appendString:@" "];
|
||||
else if (OPEN(tag, @"head")) skip = @"/head";
|
||||
else if (OPEN(tag, @"style")) skip = @"/style";
|
||||
else if (OPEN(tag, @"script")) skip = @"/script";
|
||||
else if (CLOSE(tag, @"/p") || OPEN(tag, @"label") || OPEN(tag, @"br"))
|
||||
[result appendString:@"\n"];
|
||||
else if (OPEN(tag, @"h1") || OPEN(tag, @"h2") || OPEN(tag, @"h3") ||
|
||||
OPEN(tag, @"h4") || OPEN(tag, @"h5") || OPEN(tag, @"h6") ||
|
||||
CLOSE(tag, @"/h1") || CLOSE(tag, @"/h2") || CLOSE(tag, @"/h3") ||
|
||||
CLOSE(tag, @"/h4") || CLOSE(tag, @"/h5") || CLOSE(tag, @"/h6"))
|
||||
[result appendString:@"\n"];
|
||||
else if (OPEN(tag, @"ol")) order = 1;
|
||||
else if (OPEN(tag, @"ul")) order = 0;
|
||||
else if (OPEN(tag, @"li")) {
|
||||
// ordered and unordered list items
|
||||
unichar last = [result characterAtIndex:result.length - 1];
|
||||
if (last != '\n') {
|
||||
[result appendString:@"\n"];
|
||||
}
|
||||
if (order > 0) [result appendFormat:@" %d. ", order++];
|
||||
else [result appendString:@" • "];
|
||||
}
|
||||
} else {
|
||||
// append text inbetween tags
|
||||
if (!skip) {
|
||||
[result appendString:tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (![scanner isAtEnd]) {
|
||||
unichar next = [self characterAtIndex:scanner.scanLocation];
|
||||
if (prev == next) {
|
||||
if (!skip)
|
||||
[result appendFormat:@"%c", prev];
|
||||
}
|
||||
prev = next;
|
||||
++scanner.scanLocation;
|
||||
}
|
||||
}
|
||||
// collapsing multiple horizontal whitespaces (\h) into one (the first one)
|
||||
[[NSRegularExpression regularExpressionWithPattern:@"(\\h)[\\h]+" options:0 error:nil]
|
||||
replaceMatchesInString:result options:0 range:NSMakeRange(0, result.length) withTemplate:@"$1"];
|
||||
return [result stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helper methods
|
||||
|
||||
|
||||
static inline BOOL OPEN(NSString *tag, NSString *match) {
|
||||
return ([tag isEqualToString:match] || [tag hasPrefix:[match stringByAppendingString:@" "]]);
|
||||
}
|
||||
|
||||
static inline BOOL CLOSE(NSString *tag, NSString *match) {
|
||||
return [tag isEqualToString:match];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
@interface NSURL (Ext)
|
||||
+ (NSURL*)faviconsCacheURL;
|
||||
- (BOOL)existsAndIsDir:(BOOL)dir;
|
||||
- (BOOL)mkdir;
|
||||
- (void)remove;
|
||||
- (void)moveTo:(NSURL*)destination;
|
||||
@end
|
||||
@@ -1,76 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#import "NSURL+Ext.h"
|
||||
#import "UserPrefs.h" // appName in +faviconsCacheURL
|
||||
|
||||
@implementation NSURL (Ext)
|
||||
|
||||
/// @return Directory URL pointing to "Application Support/baRSS/favicons". Does @b not create directory!
|
||||
+ (NSURL*)faviconsCacheURL {
|
||||
static NSURL *path = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
path = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
|
||||
path = [path URLByAppendingPathComponent:[UserPrefs appName] isDirectory:YES];
|
||||
path = [path URLByAppendingPathComponent:@"favicons" isDirectory:YES];
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
/// @return @c YES if and only if item exists at URL and item matches @c dir flag
|
||||
- (BOOL)existsAndIsDir:(BOOL)dir {
|
||||
BOOL d;
|
||||
return self.path && [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&d] && d == dir;
|
||||
}
|
||||
|
||||
/**
|
||||
Create directory at URL. If directory exists, this method does nothing.
|
||||
@return @c YES if dir created successfully. @c NO if dir already exists or an error occured.
|
||||
*/
|
||||
- (BOOL)mkdir {
|
||||
if ([self existsAndIsDir:YES]) return NO;
|
||||
NSError *err;
|
||||
BOOL b = [[NSFileManager defaultManager] createDirectoryAtURL:self withIntermediateDirectories:YES attributes:nil error:&err];
|
||||
if (err) [NSApp presentError:err];
|
||||
return b;
|
||||
}
|
||||
|
||||
/// Delete file or folder at URL. If item does not exist, this method does nothing.
|
||||
- (void)remove {
|
||||
BOOL success = [[NSFileManager defaultManager] removeItemAtURL:self error:nil];
|
||||
#ifdef DEBUG
|
||||
if (success) printf("DEL %s\n", self.absoluteString.UTF8String);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Move file to destination (by replacing any existing file)
|
||||
- (void)moveTo:(NSURL*)destination {
|
||||
[[NSFileManager defaultManager] removeItemAtURL:destination error:nil];
|
||||
[[NSFileManager defaultManager] moveItemAtURL:self toURL:destination error:nil];
|
||||
#ifdef DEBUG
|
||||
printf("MOVE %s\n", self.absoluteString.UTF8String);
|
||||
printf(" ↳ %s\n", destination.absoluteString.UTF8String);
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,97 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#import "NSURLRequest+Ext.h"
|
||||
#import "NSString+Ext.h"
|
||||
#import "NSError+Ext.h"
|
||||
|
||||
/// @return Shared URL session with caches disabled, enabled gzip encoding and custom user agent.
|
||||
static NSURLSession* NonCachingURLSession(void) {
|
||||
static NSURLSession *session = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
conf.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
|
||||
conf.HTTPShouldSetCookies = NO;
|
||||
conf.HTTPCookieStorage = nil; // disables '~/Library/Cookies/'
|
||||
conf.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||
conf.URLCache = nil; // disables '~/Library/Caches/de.relikd.baRSS/'
|
||||
conf.HTTPAdditionalHeaders = @{ @"User-Agent": @"baRSS (macOS)",
|
||||
@"Accept-Encoding": @"gzip" };
|
||||
session = [NSURLSession sessionWithConfiguration:conf];
|
||||
});
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@implementation NSURLRequest (Ext)
|
||||
|
||||
/// @return New request from URL. Ensures that at least @c http scheme is set.
|
||||
+ (instancetype)withURL:(NSString*)urlStr {
|
||||
NSURL *url = [NSURL URLWithString:urlStr];
|
||||
if (!url.scheme)
|
||||
url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", urlStr]]; // will redirect to https
|
||||
return [self requestWithURL:url];
|
||||
}
|
||||
|
||||
/// Perform request with non caching @c NSURLSession . If HTTP status code is @c 304 then @c data @c = @c nil.
|
||||
- (NSURLSessionDataTask*)dataTask:(nonnull void(^)(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response))block {
|
||||
NSURLSessionDataTask *task = [NonCachingURLSession() dataTaskWithRequest:self completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
|
||||
NSInteger status = [httpResponse statusCode];
|
||||
#ifdef DEBUG
|
||||
/*if (status != 304)*/ printf("GET %ld %s\n", status, self.URL.absoluteString.UTF8String);
|
||||
#endif
|
||||
if (error || status == 304) {
|
||||
data = nil; // if status == 304, data & error nil
|
||||
} else if (status >= 400 && status < 600) { // catch Client & Server errors
|
||||
error = [NSError statusCode:status reason:(status >= 500 ? [NSString plainTextFromHTMLData:data] : nil)];
|
||||
data = nil;
|
||||
}
|
||||
block(data, error, httpResponse);
|
||||
}];
|
||||
[task resume];
|
||||
return task;
|
||||
}
|
||||
|
||||
/// Prepare a download task and immediatelly perform request with non caching URL session.
|
||||
- (NSURLSessionDownloadTask*)downloadTask:(void(^)(NSURL * _Nullable path, NSError * _Nullable error))block {
|
||||
NSURLSessionDownloadTask *task = [NonCachingURLSession() downloadTaskWithRequest:self completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
block(location, error);
|
||||
}];
|
||||
[task resume];
|
||||
return task;
|
||||
}
|
||||
|
||||
/*
|
||||
Developer Tip, error log:
|
||||
|
||||
Task <..> HTTP load failed (error code: -1003 [12:8])
|
||||
Task <..> finished with error - code: -1003 --- NSURLErrorCannotFindHost
|
||||
==> NSURLErrorCannotFindHost in #import <Foundation/NSURLError.h>
|
||||
|
||||
TIC TCP Conn Failed [21:0x1d417fb00]: 1:65 Err(65) --- EHOSTUNREACH, No route to host
|
||||
TIC Read Status [9:0x0]: 1:57 --- ENOTCONN, Socket is not connected
|
||||
==> EHOSTUNREACH in #import <sys/errno.h>
|
||||
*/
|
||||
|
||||
@end
|
||||
@@ -1,108 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
/***/ static CGFloat const PAD_WIN = 20; // window padding
|
||||
/***/ static CGFloat const PAD_L = 16;
|
||||
/***/ static CGFloat const PAD_M = 8;
|
||||
/***/ static CGFloat const PAD_S = 4;
|
||||
/***/ static CGFloat const PAD_XS = 2;
|
||||
|
||||
/***/ static CGFloat const HEIGHT_LABEL = 17;
|
||||
/***/ static CGFloat const HEIGHT_LABEL_SMALL = 14;
|
||||
/***/ static CGFloat const HEIGHT_INPUTFIELD = 21;
|
||||
/***/ static CGFloat const HEIGHT_BUTTON = 21;
|
||||
/***/ static CGFloat const HEIGHT_INLINEBUTTON = 16;
|
||||
/***/ static CGFloat const HEIGHT_POPUP = 21;
|
||||
/***/ static CGFloat const HEIGHT_SPINNER = 16;
|
||||
/***/ static CGFloat const HEIGHT_CHECKBOX = 14;
|
||||
|
||||
/// Static variable to calculate origin center coordinate in its @c superview. The value of this var isn't used.
|
||||
static CGFloat const CENTER = -0.015625;
|
||||
|
||||
/// Calculate @c origin.y going down from the top border of its @c superview
|
||||
static inline CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) - NSMinY(view.frame) - view.alignmentRectInsets.bottom; }
|
||||
/// @c MAX()
|
||||
static inline CGFloat Max(CGFloat a, CGFloat b) { return a < b ? b : a; }
|
||||
/// @c Max(NSWidth(a.frame),NSWidth(b.frame))
|
||||
static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.frame), NSWidth(b.frame)); }
|
||||
|
||||
|
||||
/*
|
||||
Allmost all methods return @c self to allow method chaining
|
||||
*/
|
||||
|
||||
@interface NSView (Ext)
|
||||
// UI: TextFields
|
||||
+ (NSTextField*)label:(NSString*)text;
|
||||
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w;
|
||||
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad;
|
||||
// UI: Buttons
|
||||
+ (NSButton*)button:(NSString*)text;
|
||||
+ (NSButton*)buttonImageSquare:(nonnull NSImageName)name;
|
||||
+ (NSButton*)buttonIcon:(nonnull NSImageName)name size:(CGFloat)size;
|
||||
+ (NSButton*)helpButton;
|
||||
+ (NSButton*)inlineButton:(NSString*)text;
|
||||
+ (NSPopUpButton*)popupButton:(CGFloat)w;
|
||||
// UI: Others
|
||||
+ (NSImageView*)imageView:(NSImageName)name size:(CGFloat)size;
|
||||
+ (NSButton*)checkbox:(BOOL)flag;
|
||||
+ (NSProgressIndicator*)activitySpinner;
|
||||
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action;
|
||||
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries;
|
||||
// UI: Enclosing Container
|
||||
+ (NSPopover*)popover:(NSSize)size;
|
||||
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect;
|
||||
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad;
|
||||
// Insert UI elements in parent view
|
||||
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x y:(CGFloat)y;
|
||||
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x yTop:(CGFloat)y;
|
||||
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x y:(CGFloat)y;
|
||||
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x yTop:(CGFloat)y;
|
||||
// Modify existing UI elements
|
||||
- (instancetype)sizableWidthAndHeight;
|
||||
- (instancetype)sizeToRight:(CGFloat)rightPadding;
|
||||
- (instancetype)sizeWidthToFit;
|
||||
- (instancetype)tooltip:(NSString*)tt;
|
||||
// Debugging
|
||||
- (instancetype)colorLayer:(NSColor*)color;
|
||||
+ (NSView*)redCube:(CGFloat)size;
|
||||
@end
|
||||
|
||||
|
||||
@interface NSControl (Ext)
|
||||
- (instancetype)action:(SEL)selector target:(id)target;
|
||||
- (instancetype)large;
|
||||
- (instancetype)small;
|
||||
- (instancetype)tiny;
|
||||
- (instancetype)bold;
|
||||
- (instancetype)textRight;
|
||||
- (instancetype)textCenter;
|
||||
@end
|
||||
|
||||
|
||||
@interface NSTextField (Ext)
|
||||
- (instancetype)gray;
|
||||
- (instancetype)selectable;
|
||||
- (instancetype)multiline:(NSSize)size;
|
||||
@end
|
||||
@@ -1,381 +0,0 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#import "NSView+Ext.h"
|
||||
|
||||
@implementation NSView (Ext)
|
||||
|
||||
#pragma mark - UI: TextFields -
|
||||
|
||||
/// Create label with non-editable text. Ensures uniform fontsize and text color. @c 17px height.
|
||||
+ (NSTextField*)label:(NSString*)text {
|
||||
NSTextField *label = [NSTextField labelWithString:text];
|
||||
[label setFrameSize: NSMakeSize(0, HEIGHT_LABEL)];
|
||||
label.font = [NSFont systemFontOfSize: NSFont.systemFontSize];
|
||||
label.textColor = [NSColor controlTextColor];
|
||||
label.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
// label.backgroundColor = [NSColor yellowColor];
|
||||
// label.drawsBackground = YES;
|
||||
return [label sizeWidthToFit];
|
||||
}
|
||||
|
||||
/// Create input text field with placeholder text. @c 21px height.
|
||||
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w {
|
||||
NSTextField *input = [NSTextField textFieldWithString:@""];
|
||||
[input setFrameSize: NSMakeSize(w, HEIGHT_INPUTFIELD)];
|
||||
input.alignment = NSTextAlignmentJustified;
|
||||
input.placeholderString = placeholder;
|
||||
input.font = [NSFont systemFontOfSize: NSFont.systemFontSize];
|
||||
input.textColor = [NSColor controlTextColor];
|
||||
return input;
|
||||
}
|
||||
|
||||
/// Create view with @c NSTextField subviews with right-aligned and row-centered text from @c labels.
|
||||
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad {
|
||||
CGFloat w = 0, y = 0;
|
||||
CGFloat off = (h - HEIGHT_LABEL) / 2;
|
||||
NSView *parent = [[NSView alloc] init];
|
||||
for (NSUInteger i = 0; i < labels.count; i++) {
|
||||
NSTextField *lbl = [[NSView label:labels[i]] placeIn:parent xRight:0 yTop:y + off];
|
||||
w = Max(w, NSWidth(lbl.frame));
|
||||
y += h + pad;
|
||||
}
|
||||
[parent setFrameSize: NSMakeSize(w, y - pad)];
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UI: Buttons -
|
||||
|
||||
|
||||
/// Create button. @c 21px height.
|
||||
+ (NSButton*)button:(NSString*)text {
|
||||
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_BUTTON)];
|
||||
btn.font = [NSFont systemFontOfSize:NSFont.systemFontSize];
|
||||
btn.bezelStyle = NSBezelStyleRounded;
|
||||
btn.title = text;
|
||||
return [btn sizeWidthToFit];
|
||||
}
|
||||
|
||||
/// Create @c NSBezelStyleSmallSquare image button. @c 25x21px
|
||||
+ (NSButton*)buttonImageSquare:(nonnull NSImageName)name {
|
||||
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 25, HEIGHT_BUTTON)];
|
||||
btn.bezelStyle = NSBezelStyleSmallSquare;
|
||||
btn.image = [NSImage imageNamed:name];
|
||||
if (!btn.image) btn.title = name; // fallback to text
|
||||
return btn;
|
||||
}
|
||||
|
||||
/// Create pure image button with no border.
|
||||
+ (NSButton*)buttonIcon:(nonnull NSImageName)name size:(CGFloat)size {
|
||||
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
|
||||
btn.bezelStyle = NSBezelStyleRounded;
|
||||
btn.bordered = NO;
|
||||
btn.image = [NSImage imageNamed:name];
|
||||
return btn;
|
||||
}
|
||||
|
||||
/// Create round button with question mark. @c 21x21px
|
||||
+ (NSButton*)helpButton {
|
||||
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 21, 21)];
|
||||
btn.bezelStyle = NSBezelStyleHelpButton;
|
||||
btn.title = @"";
|
||||
return btn;
|
||||
}
|
||||
|
||||
/// Create gray inline button with rounded corners. @c 16px height.
|
||||
+ (NSButton*)inlineButton:(NSString*)text {
|
||||
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_INLINEBUTTON)];
|
||||
btn.font = [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight: NSFontWeightBold];
|
||||
btn.bezelStyle = NSBezelStyleInline;
|
||||
btn.controlSize = NSControlSizeSmall;
|
||||
btn.title = text;
|
||||
return [btn sizeWidthToFit];
|
||||
}
|
||||
|
||||
/// Create empty drop down button. @c 21px height.
|
||||
+ (NSPopUpButton*)popupButton:(CGFloat)w {
|
||||
return [[NSPopUpButton alloc] initWithFrame: NSMakeRect(0, 0, w, HEIGHT_POPUP) pullsDown:NO];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UI: Others -
|
||||
|
||||
|
||||
/// Create @c ImageView with square @c size
|
||||
+ (NSImageView*)imageView:(NSImageName)name size:(CGFloat)size {
|
||||
NSImageView *imgView = [[NSImageView alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
|
||||
if (name) imgView.image = [NSImage imageNamed:name];
|
||||
return imgView;
|
||||
}
|
||||
|
||||
/// Create checkbox. @c 14px height.
|
||||
+ (NSButton*)checkbox:(BOOL)flag {
|
||||
NSButton *check = [NSButton checkboxWithTitle:@"" target:nil action:nil];
|
||||
check.title = @""; // needed, otherwise will print "Button"
|
||||
check.frame = NSMakeRect(0, 0, HEIGHT_CHECKBOX, HEIGHT_CHECKBOX);
|
||||
check.state = (flag? NSControlStateValueOn : NSControlStateValueOff);
|
||||
return check;
|
||||
}
|
||||
|
||||
/// Create progress spinner. @c 16px size.
|
||||
+ (NSProgressIndicator*)activitySpinner {
|
||||
NSProgressIndicator *spin = [[NSProgressIndicator alloc] initWithFrame: NSMakeRect(0, 0, HEIGHT_SPINNER, HEIGHT_SPINNER)];
|
||||
spin.indeterminate = YES;
|
||||
spin.displayedWhenStopped = NO;
|
||||
spin.style = NSProgressIndicatorSpinningStyle;
|
||||
spin.controlSize = NSControlSizeSmall;
|
||||
return spin;
|
||||
}
|
||||
|
||||
/// Create grouping view with vertically, left-aligned radio buttons. Action is identical for all buttons (grouping).
|
||||
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action {
|
||||
if (entries.count == 0)
|
||||
return nil;
|
||||
CGFloat w = 0, h = 0;
|
||||
NSView *parent = [[NSView alloc] init];
|
||||
for (NSUInteger i = entries.count; i > 0; i--) {
|
||||
NSButton *btn = [NSButton radioButtonWithTitle:entries[i-1] target:target action:action];
|
||||
btn.tag = (NSInteger)i-1;
|
||||
if (btn.tag == 0)
|
||||
btn.state = NSControlStateValueOn;
|
||||
w = Max(w, NSWidth(btn.frame)); // find max width (before alignmentRect:)
|
||||
[btn placeIn:parent x:0 y:h];
|
||||
h += NSHeight([btn alignmentRectForFrame:btn.frame]) + PAD_XS;
|
||||
}
|
||||
[parent setFrameSize: NSMakeSize(w, h - PAD_XS)];
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// Same as @c radioGroup:target:action: but using dummy action to ignore radio button click events.
|
||||
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries {
|
||||
return [self radioGroup:entries target:self action:@selector(donothing)];
|
||||
}
|
||||
|
||||
/// Solely used to group radio buttons
|
||||
+ (void)donothing {}
|
||||
|
||||
|
||||
#pragma mark - UI: Enclosing Container -
|
||||
|
||||
|
||||
/// Create transient popover with initial view controller and view @c size
|
||||
+ (NSPopover*)popover:(NSSize)size {
|
||||
NSPopover *pop = [[NSPopover alloc] init];
|
||||
pop.behavior = NSPopoverBehaviorTransient;
|
||||
pop.contentViewController = [[NSViewController alloc] init];
|
||||
pop.contentViewController.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, size.width, size.height)];
|
||||
return pop;
|
||||
}
|
||||
|
||||
/// Insert @c scrollView, remove @c self from current view and set as @c documentView for the newly created scroll view.
|
||||
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect {
|
||||
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight];
|
||||
scroll.borderType = NSBezelBorder;
|
||||
scroll.hasVerticalScroller = YES;
|
||||
scroll.horizontalScrollElasticity = NSScrollElasticityNone;
|
||||
[self addSubview:scroll];
|
||||
|
||||
if (content.superview) [content removeFromSuperview]; // remove if added already (e.g., helper methods above)
|
||||
content.frame = NSMakeRect(0, 0, scroll.contentSize.width, scroll.contentSize.height);
|
||||
scroll.documentView = content;
|
||||
return scroll;
|
||||
}
|
||||
|
||||
/// Create view with @c NSTextField label in front of the view.
|
||||
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad {
|
||||
NSView *parent = [[NSView alloc] initWithFrame: NSZeroRect];
|
||||
NSTextField *label = [NSView label:str];
|
||||
[label placeIn:parent x:pad yTop:pad];
|
||||
[other placeIn:parent x:pad + NSWidth(label.frame) yTop:pad];
|
||||
[parent setFrameSize: NSMakeSize(NSMaxX(other.frame), NSHeight(other.frame) + 2 * pad)];
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Insert UI elements in parent view -
|
||||
|
||||
|
||||
/**
|
||||
Set frame origin and insert @c self in @c parent view with @c frameForAlignmentRect:.
|
||||
You may use @c CENTER to automatically calculate midpoint in parent view.
|
||||
The @c autoresizingMask will be set accordingly.
|
||||
*/
|
||||
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x y:(CGFloat)y {
|
||||
if (x == CENTER) {
|
||||
x = (NSWidth(parent.frame) - NSWidth(self.frame)) / 2;
|
||||
self.autoresizingMask |= NSViewMinXMargin | NSViewMaxXMargin;
|
||||
}
|
||||
if (y == CENTER) {
|
||||
y = (NSHeight(parent.frame) - NSHeight(self.frame)) / 2;
|
||||
self.autoresizingMask |= NSViewMinYMargin | NSViewMaxYMargin;
|
||||
}
|
||||
[self setFrameOrigin: NSMakePoint(x, y)];
|
||||
self.frame = [self frameForAlignmentRect:self.frame];
|
||||
[parent addSubview:self];
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Same as @c placeIn:x:y: but measure position from top instead of bottom. Also sets @c autoresizingMask.
|
||||
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x yTop:(CGFloat)y {
|
||||
return [[self placeIn:parent x:x y:NSHeight(parent.frame) - NSHeight(self.frame) - y] alignTop];
|
||||
}
|
||||
|
||||
/// Same as @c placeIn:x:y: but measure position from right instead of left. Also sets @c autoresizingMask.
|
||||
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x y:(CGFloat)y {
|
||||
return [[self placeIn:parent x:NSWidth(parent.frame) - NSWidth(self.frame) - x y:y] alignRight];
|
||||
}
|
||||
|
||||
/// Set origin by measuring from top right (@c CENTER is not allowed here). Also sets @c autoresizingMask.
|
||||
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x yTop:(CGFloat)y {
|
||||
[self setFrameOrigin: NSMakePoint(NSWidth(parent.frame) - NSWidth(self.frame) - x,
|
||||
NSHeight(parent.frame) - NSHeight(self.frame) - y)];
|
||||
self.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
|
||||
self.frame = [self frameForAlignmentRect:self.frame];
|
||||
[parent addSubview:self];
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Modify existing UI elements -
|
||||
|
||||
|
||||
// Aligned Frame Origins
|
||||
// pad - view.alignmentRectInsets.left;
|
||||
// pad - view.alignmentRectInsets.bottom;
|
||||
// NSWidth(view.superview.frame) - NSWidth(view.frame) - pad + view.alignmentRectInsets.right;
|
||||
// NSHeight(view.superview.frame) - NSHeight(view.frame) - pad + view.alignmentRectInsets.top;
|
||||
|
||||
/// Modify @c .autoresizingMask; Clear @c NSViewMaxYMargin flag and set @c NSViewMinYMargin
|
||||
- (instancetype)alignTop { self.autoresizingMask = (self.autoresizingMask & ~NSViewMaxYMargin) | NSViewMinYMargin; return self; }
|
||||
|
||||
/// Modify @c .autoresizingMask; Clear @c NSViewMaxXMargin flag and set @c NSViewMinXMargin
|
||||
- (instancetype)alignRight { self.autoresizingMask = (self.autoresizingMask & ~NSViewMaxXMargin) | NSViewMinXMargin; return self; }
|
||||
|
||||
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable @c | @c NSViewHeightSizable flags
|
||||
- (instancetype)sizableWidthAndHeight { self.autoresizingMask |= NSViewWidthSizable | NSViewHeightSizable; return self; }
|
||||
|
||||
/// Extend frame in its @c superview and stick to right with padding. Adds @c NSViewWidthSizable to @c autoresizingMask
|
||||
- (instancetype)sizeToRight:(CGFloat)rightPadding {
|
||||
SetFrameWidth(self, NSWidth(self.superview.frame) - NSMinX(self.frame) - rightPadding + self.alignmentRectInsets.right);
|
||||
self.autoresizingMask |= NSViewWidthSizable;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Set @c width to @c fittingSize.width but keep original height.
|
||||
- (instancetype)sizeWidthToFit {
|
||||
SetFrameWidth(self, self.fittingSize.width);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Set @c tooltip and @c accessibilityTitle of view and return self
|
||||
- (instancetype)tooltip:(NSString*)tt {
|
||||
self.toolTip = tt;
|
||||
if (self.accessibilityLabel.length == 0)
|
||||
self.accessibilityLabel = tt;
|
||||
else
|
||||
self.accessibilityValueDescription = tt;
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Helper method to set frame width and keep same height
|
||||
static inline void SetFrameWidth(NSView *view, CGFloat w) {
|
||||
[view setFrameSize: NSMakeSize(w, NSHeight(view.frame))];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Debugging -
|
||||
|
||||
|
||||
/// Set background color on @c .layer
|
||||
- (instancetype)colorLayer:(NSColor*)color {
|
||||
self.layer = [CALayer layer];
|
||||
self.layer.backgroundColor = color.CGColor;
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSView*)redCube:(CGFloat)size {
|
||||
return [[[NSView alloc] initWithFrame: NSMakeRect(0, 0, size, size)] colorLayer:NSColor.redColor];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - NSControl specific -
|
||||
|
||||
|
||||
@implementation NSControl (Ext)
|
||||
|
||||
/// Set @c target and @c action simultaneously
|
||||
- (instancetype)action:(SEL)selector target:(id)target {
|
||||
self.action = selector;
|
||||
self.target = target;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Set system font with current @c pointSize @c + @c 2. A label will be @c 19px height.
|
||||
- (instancetype)large { SetFontAndResize(self, [NSFont systemFontOfSize: self.font.pointSize + 2]); return self; }
|
||||
|
||||
/// Set system font with @c smallSystemFontSize and perform @c sizeToFit. A label will be @c 14px height.
|
||||
- (instancetype)small { SetFontAndResize(self, [NSFont systemFontOfSize: NSFont.smallSystemFontSize]); return self; }
|
||||
|
||||
/// Set monospaced font with @c labelFontSize regular and perform @c sizeToFit. A label will be @c 13px height.
|
||||
- (instancetype)tiny { SetFontAndResize(self, [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight: NSFontWeightRegular]); return self; }
|
||||
|
||||
/// Set system bold font with current @c pointSize
|
||||
- (instancetype)bold { SetFontAndResize(self, [NSFont boldSystemFontOfSize: self.font.pointSize]); return self; }
|
||||
|
||||
/// Set @c .alignment to @c NSTextAlignmentRight
|
||||
- (instancetype)textRight { self.alignment = NSTextAlignmentRight; return self; }
|
||||
|
||||
/// Set @c .alignment to @c NSTextAlignmentCenter
|
||||
- (instancetype)textCenter { self.alignment = NSTextAlignmentCenter; return self; }
|
||||
|
||||
/// Helper method to set new font, subsequently run @c sizeToFit
|
||||
static inline void SetFontAndResize(NSControl *control, NSFont *font) {
|
||||
control.font = font; [control sizeToFit];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NSTextField (Ext)
|
||||
|
||||
/// Set text color to @c systemGrayColor
|
||||
- (instancetype)gray { self.textColor = [NSColor systemGrayColor]; return self; }
|
||||
|
||||
/// Set @c .selectable to @c YES
|
||||
- (instancetype)selectable { self.selectable = YES; return self; }
|
||||
|
||||
/// Set @c .maximumNumberOfLines @c = @c 7 and @c preferredMaxLayoutWidth.
|
||||
- (instancetype)multiline:(NSSize)size {
|
||||
[self setFrameSize:size];
|
||||
self.preferredMaxLayoutWidth = size.width;
|
||||
self.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
self.usesSingleLineMode = NO;
|
||||
self.maximumNumberOfLines = 7; // used in ModalFeedEditView
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2019 Oleg Geier
|
||||
// Copyright (c) 2018 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -22,8 +22,30 @@
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
@interface NSURLRequest (Ext)
|
||||
+ (instancetype)withURL:(NSString*)urlStr;
|
||||
- (NSURLSessionDataTask*)dataTask:(nonnull void(^)(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response))block;
|
||||
- (NSURLSessionDownloadTask*)downloadTask:(void(^)(NSURL * _Nullable path, NSError * _Nullable error))block;
|
||||
@interface UserPrefs : NSObject
|
||||
// User Preferences Plist
|
||||
+ (BOOL)defaultYES:(NSString*)key;
|
||||
+ (BOOL)defaultNO:(NSString*)key;
|
||||
+ (NSUInteger)defaultUInt:(NSUInteger)defaultInt forKey:(NSString*)key;
|
||||
|
||||
+ (NSString*)getHttpApplication;
|
||||
+ (void)setHttpApplication:(NSString*)bundleID;
|
||||
+ (BOOL)openURLsWithPreferredBrowser:(NSArray<NSURL*>*)urls;
|
||||
|
||||
// Hidden Plist Properties
|
||||
+ (NSUInteger)openFewLinksLimit; // Change with: 'defaults write de.relikd.baRSS openFewLinksLimit -int 10'
|
||||
+ (NSUInteger)shortArticleNamesLimit; // Change with: 'defaults write de.relikd.baRSS shortArticleNamesLimit -int 50'
|
||||
+ (NSUInteger)articlesInMenuLimit; // Change with: 'defaults write de.relikd.baRSS articlesInMenuLimit -int 40'
|
||||
|
||||
// Application Info Plist
|
||||
+ (NSString*)appName;
|
||||
+ (NSString*)appVersion;
|
||||
+ (NSString*)appVersionWithBuildNo;
|
||||
|
||||
// Core Data Properties
|
||||
+ (BOOL)dbIsUnusedInitalState;
|
||||
+ (BOOL)dbIsCurrentFileVersion;
|
||||
+ (BOOL)dbIsCurrentAppVersion;
|
||||
+ (void)dbUpdateFileVersion;
|
||||
+ (void)dbUpdateAppVersion;
|
||||
@end
|
||||
144
baRSS/Helper/UserPrefs.m
Normal file
144
baRSS/Helper/UserPrefs.m
Normal file
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2018 Oleg Geier
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#import "UserPrefs.h"
|
||||
#import "StoreCoordinator.h"
|
||||
|
||||
@implementation UserPrefs
|
||||
|
||||
#pragma mark - User Preferences Plist
|
||||
|
||||
/// @return @c YES if key is not set. Otherwise, return user defaults property from plist.
|
||||
+ (BOOL)defaultYES:(NSString*)key {
|
||||
if ([[NSUserDefaults standardUserDefaults] objectForKey:key] == NULL) {
|
||||
return YES;
|
||||
}
|
||||
return [[NSUserDefaults standardUserDefaults] boolForKey:key];
|
||||
}
|
||||
|
||||
/// @return @c NO if key is not set. Otherwise, return user defaults property from plist.
|
||||
+ (BOOL)defaultNO:(NSString*)key {
|
||||
return [[NSUserDefaults standardUserDefaults] boolForKey:key];
|
||||
}
|
||||
|
||||
/// @return Return @c defaultInt if key is not set. Otherwise, return user defaults property from plist.
|
||||
+ (NSUInteger)defaultUInt:(NSUInteger)defaultInt forKey:(NSString*)key {
|
||||
NSInteger ret = [[NSUserDefaults standardUserDefaults] integerForKey:key];
|
||||
if (ret > 0) return (NSUInteger)ret;
|
||||
return defaultInt;
|
||||
}
|
||||
|
||||
/// @return User configured custom browser. Or @c nil if not set yet. (which will fallback to default browser)
|
||||
+ (NSString*)getHttpApplication {
|
||||
return [[NSUserDefaults standardUserDefaults] stringForKey:@"defaultHttpApplication"];
|
||||
}
|
||||
|
||||
/// Store custom browser bundle id to user defaults.
|
||||
+ (void)setHttpApplication:(NSString*)bundleID {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:bundleID forKey:@"defaultHttpApplication"];
|
||||
}
|
||||
|
||||
/**
|
||||
Open web links in default browser or a browser the user selected in the preferences.
|
||||
|
||||
@param urls A list of @c NSURL objects that will be opened immediatelly in bulk.
|
||||
@return @c YES if @c urls are opened successfully. @c NO on error.
|
||||
*/
|
||||
+ (BOOL)openURLsWithPreferredBrowser:(NSArray<NSURL*>*)urls {
|
||||
if (urls.count == 0) return NO;
|
||||
return [[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:[self getHttpApplication] options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Hidden Plist Properties -
|
||||
|
||||
|
||||
/// @return The limit on how many links should be opened at the same time, if user holds the option key.
|
||||
/// Default: @c 10
|
||||
+ (NSUInteger)openFewLinksLimit { return [self defaultUInt:10 forKey:@"openFewLinksLimit"]; }
|
||||
|
||||
/// @return The limit on when to truncate article titles (Short names setting must be active).
|
||||
/// Default: @c 60
|
||||
+ (NSUInteger)shortArticleNamesLimit { return [self defaultUInt:60 forKey:@"shortArticleNamesLimit"]; }
|
||||
|
||||
/// @return The maximum number of articles displayed per feed (Limit articles setting must be active).
|
||||
/// Default: @c 40
|
||||
+ (NSUInteger)articlesInMenuLimit { return [self defaultUInt:40 forKey:@"articlesInMenuLimit"]; }
|
||||
|
||||
|
||||
#pragma mark - Application Info Plist
|
||||
|
||||
|
||||
/// @return The application name, e.g., 'baRSS' or 'baRSS Beta'
|
||||
+ (NSString*)appName {
|
||||
return [[NSBundle mainBundle] infoDictionary][(NSString*)kCFBundleNameKey];
|
||||
}
|
||||
|
||||
/// @return The application version number, e.g., '0.9.4'
|
||||
+ (NSString*)appVersion {
|
||||
return [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
|
||||
}
|
||||
|
||||
/// @return The application version number including build number, e.g., '0.9.4 (9906)'
|
||||
+ (NSString*)appVersionWithBuildNo {
|
||||
NSString *buildNo = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"];
|
||||
return [[self appVersion] stringByAppendingFormat:@" (%@)", buildNo];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Core Data Properties -
|
||||
|
||||
|
||||
/// Helper method that retrieves and transforms option value to int
|
||||
+ (int)dbIntForKey:(NSString*)key defaultsTo:(int)otherwise {
|
||||
NSString *str = [StoreCoordinator optionForKey:key];
|
||||
if (!str) return otherwise;
|
||||
int num = [NSDecimalNumber decimalNumberWithString:str].intValue;
|
||||
return isnan(num) ? otherwise : num;
|
||||
}
|
||||
|
||||
/// Check whether the database was just initialized (first install)
|
||||
+ (BOOL)dbIsUnusedInitalState {
|
||||
return [StoreCoordinator optionForKey:@"db-version"] == nil;
|
||||
}
|
||||
|
||||
/// Check whether the stored database version is up to date
|
||||
+ (BOOL)dbIsCurrentFileVersion {
|
||||
return [self dbIntForKey:@"db-version" defaultsTo:-1] == dbFileVersion;
|
||||
}
|
||||
|
||||
/// Write current database version to core data
|
||||
+ (void)dbUpdateFileVersion {
|
||||
[StoreCoordinator setOption:@"db-version" value:[NSString stringWithFormat:@"%d", dbFileVersion]];
|
||||
}
|
||||
|
||||
/// Check whether the stored application version is up to date
|
||||
+ (BOOL)dbIsCurrentAppVersion {
|
||||
return [[StoreCoordinator optionForKey:@"app-version"] isEqualToString:[self appVersion]];
|
||||
}
|
||||
|
||||
/// Write current application version to core data
|
||||
+ (void)dbUpdateAppVersion {
|
||||
[StoreCoordinator setOption:@"app-version" value:[self appVersion]];
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user