Welcome message
This commit is contained in:
@@ -16,6 +16,8 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
|||||||
- *Settings, Feeds:* Drag & Drop feed titles and urls as text
|
- *Settings, Feeds:* Drag & Drop feed titles and urls as text
|
||||||
- *Settings, Feeds:* OPML export with selected items only
|
- *Settings, Feeds:* OPML export with selected items only
|
||||||
- *UI:* Accessibility hints for most UI elements
|
- *UI:* Accessibility hints for most UI elements
|
||||||
|
- *UI*: Show welcome message upon first usage (empty db)
|
||||||
|
- *DB*: Table for options. E.g., with what version was the db last used
|
||||||
- Associate OPML files (double click and right click actions in Finder)
|
- Associate OPML files (double click and right click actions in Finder)
|
||||||
- Quick Look preview for OPML files
|
- Quick Look preview for OPML files
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#import "DrawImage.h"
|
#import "DrawImage.h"
|
||||||
#import "SettingsFeeds+DragDrop.h"
|
#import "SettingsFeeds+DragDrop.h"
|
||||||
#import "UserPrefs.h"
|
#import "UserPrefs.h"
|
||||||
|
#import "StoreCoordinator.h"
|
||||||
|
|
||||||
@interface AppHook()
|
@interface AppHook()
|
||||||
@property (strong) NSWindowController *prefWindow;
|
@property (strong) NSWindowController *prefWindow;
|
||||||
@@ -41,17 +42,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
|
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
|
||||||
[self migrateVersionUpdate];
|
RegisterImageViewNames();
|
||||||
_statusItem = [BarStatusItem new];
|
_statusItem = [BarStatusItem new];
|
||||||
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
|
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
|
||||||
[appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:)
|
[appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:)
|
||||||
forEventClass:kInternetEventClass andEventID:kAEGetURL];
|
forEventClass:kInternetEventClass andEventID:kAEGetURL];
|
||||||
|
[self migrateVersionUpdate];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||||
RegisterImageViewNames();
|
if ([StoreCoordinator isEmpty]) {
|
||||||
// feed://https://feeds.feedburner.com/simpledesktops
|
[_statusItem showWelcomeMessage];
|
||||||
|
}
|
||||||
[FeedDownload registerNetworkChangeNotification]; // will call update scheduler
|
[FeedDownload registerNetworkChangeNotification]; // will call update scheduler
|
||||||
|
[_statusItem asyncReloadUnreadCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||||
@@ -59,6 +63,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
|
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
|
||||||
|
// feed://https://feeds.feedburner.com/simpledesktops
|
||||||
NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
|
NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
|
||||||
NSString *scheme = [[[NSURL URLWithString:url] scheme] lowercaseString];
|
NSString *scheme = [[[NSURL URLWithString:url] scheme] lowercaseString];
|
||||||
url = [url substringFromIndex:scheme.length + 1]; // + ':'
|
url = [url substringFromIndex:scheme.length + 1]; // + ':'
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ static const int dbFileVersion = 1; // update in case database structure changes
|
|||||||
+ (NSArray<Feed*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc;
|
+ (NSArray<Feed*>*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc;
|
||||||
|
|
||||||
// Count elements
|
// Count elements
|
||||||
|
+ (BOOL)isEmpty;
|
||||||
+ (NSUInteger)countTotalUnread;
|
+ (NSUInteger)countTotalUnread;
|
||||||
+ (NSUInteger)countRootItemsInContext:(NSManagedObjectContext*)moc;
|
+ (NSUInteger)countRootItemsInContext:(NSManagedObjectContext*)moc;
|
||||||
+ (NSArray<NSDictionary*>*)countAggregatedUnread;
|
+ (NSArray<NSDictionary*>*)countAggregatedUnread;
|
||||||
|
|||||||
@@ -112,6 +112,11 @@
|
|||||||
|
|
||||||
#pragma mark - Count Elements
|
#pragma mark - Count Elements
|
||||||
|
|
||||||
|
/// @return @c YES if core data has no stored @c FeedGroup
|
||||||
|
+ (BOOL)isEmpty {
|
||||||
|
return [[FeedGroup fetchRequest] fetchFirst:[self getMainContext]] == nil;
|
||||||
|
}
|
||||||
|
|
||||||
/// @return Sum of all unread @c FeedArticle items.
|
/// @return Sum of all unread @c FeedArticle items.
|
||||||
+ (NSUInteger)countTotalUnread {
|
+ (NSUInteger)countTotalUnread {
|
||||||
return [[[FeedArticle fetchRequest] where:@"unread = YES"] fetchCount: [self getMainContext]];
|
return [[[FeedArticle fetchRequest] where:@"unread = YES"] fetchCount: [self getMainContext]];
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ static const CGFloat CENTER = -0.015625;
|
|||||||
|
|
||||||
/// Calculate @c origin.y going down from the top border of its @c superview
|
/// Calculate @c origin.y going down from the top border of its @c superview
|
||||||
NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) - NSMinY(view.frame) - view.alignmentRectInsets.bottom; }
|
NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) - NSMinY(view.frame) - view.alignmentRectInsets.bottom; }
|
||||||
|
/// @c MAX()
|
||||||
|
NS_INLINE CGFloat Max(CGFloat a, CGFloat b) { return a < b ? b : a; }
|
||||||
|
/// @c Max(NSWidth(a.frame),NSWidth(b.frame))
|
||||||
|
NS_INLINE CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.frame), NSWidth(b.frame)); }
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -66,6 +70,7 @@ NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame)
|
|||||||
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action;
|
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action;
|
||||||
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries;
|
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries;
|
||||||
// UI: Enclosing Container
|
// UI: Enclosing Container
|
||||||
|
+ (NSPopover*)popover:(NSSize)size;
|
||||||
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect;
|
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect;
|
||||||
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad;
|
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad;
|
||||||
// Insert UI elements in parent view
|
// Insert UI elements in parent view
|
||||||
@@ -98,4 +103,5 @@ NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame)
|
|||||||
@interface NSTextField (Ext)
|
@interface NSTextField (Ext)
|
||||||
- (instancetype)gray;
|
- (instancetype)gray;
|
||||||
- (instancetype)selectable;
|
- (instancetype)selectable;
|
||||||
|
- (instancetype)multiline:(NSSize)size;
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -56,8 +56,7 @@
|
|||||||
NSView *parent = [[NSView alloc] init];
|
NSView *parent = [[NSView alloc] init];
|
||||||
for (NSUInteger i = 0; i < labels.count; i++) {
|
for (NSUInteger i = 0; i < labels.count; i++) {
|
||||||
NSTextField *lbl = [[NSView label:labels[i]] placeIn:parent xRight:0 yTop:y + off];
|
NSTextField *lbl = [[NSView label:labels[i]] placeIn:parent xRight:0 yTop:y + off];
|
||||||
if (w < NSWidth(lbl.frame))
|
w = Max(w, NSWidth(lbl.frame));
|
||||||
w = NSWidth(lbl.frame);
|
|
||||||
y += h + pad;
|
y += h + pad;
|
||||||
}
|
}
|
||||||
[parent setFrameSize: NSMakeSize(w, y - pad)];
|
[parent setFrameSize: NSMakeSize(w, y - pad)];
|
||||||
@@ -151,8 +150,7 @@
|
|||||||
btn.tag = (NSInteger)i-1;
|
btn.tag = (NSInteger)i-1;
|
||||||
if (btn.tag == 0)
|
if (btn.tag == 0)
|
||||||
btn.state = NSControlStateValueOn;
|
btn.state = NSControlStateValueOn;
|
||||||
if (w < NSWidth(btn.frame)) // find max width (before alignmentRect:)
|
w = Max(w, NSWidth(btn.frame)); // find max width (before alignmentRect:)
|
||||||
w = NSWidth(btn.frame);
|
|
||||||
[btn placeIn:parent x:0 y:h];
|
[btn placeIn:parent x:0 y:h];
|
||||||
h += NSHeight([btn alignmentRectForFrame:btn.frame]) + PAD_XS;
|
h += NSHeight([btn alignmentRectForFrame:btn.frame]) + PAD_XS;
|
||||||
}
|
}
|
||||||
@@ -172,6 +170,15 @@
|
|||||||
#pragma mark - UI: Enclosing Container -
|
#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.
|
/// 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*)wrapContent:(NSView*)content inScrollView:(NSRect)rect {
|
||||||
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight];
|
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight];
|
||||||
@@ -352,4 +359,14 @@ NS_INLINE void SetFontAndResize(NSControl *control, NSFont *font) {
|
|||||||
/// Set @c .selectable to @c YES
|
/// Set @c .selectable to @c YES
|
||||||
- (instancetype)selectable { self.selectable = YES; return self; }
|
- (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
|
@end
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>9923</string>
|
<string>10141</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.news</string>
|
<string>public.app-category.news</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -338,7 +338,7 @@
|
|||||||
|
|
||||||
// apply fitting size and display
|
// apply fitting size and display
|
||||||
self.view.warningPopover.contentSize = newSize;
|
self.view.warningPopover.contentSize = newSize;
|
||||||
[self.view.warningPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSRectEdgeMinY];
|
[self.view.warningPopover showRelativeToRect:NSZeroRect ofView:sender preferredEdge:NSRectEdgeMinY];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Either hit by Cmd+R or reload button inside warning popover error description
|
/// Either hit by Cmd+R or reload button inside warning popover error description
|
||||||
|
|||||||
@@ -70,23 +70,11 @@
|
|||||||
|
|
||||||
/// Prepare popover controller to display errors during download
|
/// Prepare popover controller to display errors during download
|
||||||
- (void)prepareWarningPopover {
|
- (void)prepareWarningPopover {
|
||||||
NSPopover *pop = [[NSPopover alloc] init];
|
self.warningPopover = [NSView popover: NSMakeSize(300, 100)];
|
||||||
pop.behavior = NSPopoverBehaviorTransient;
|
NSView *content = self.warningPopover.contentViewController.view;
|
||||||
pop.contentViewController = [[NSViewController alloc] init];
|
|
||||||
|
|
||||||
NSView *content = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 300, 100)];
|
|
||||||
pop.contentViewController.view = content;
|
|
||||||
|
|
||||||
// User visible error description text (after click on warning button)
|
// User visible error description text (after click on warning button)
|
||||||
NSTextField *txt = [[[NSView label:@""] selectable] sizableWidthAndHeight];
|
self.warningText = [[[[[NSView label:@""] selectable] sizableWidthAndHeight]
|
||||||
txt.frame = NSInsetRect(content.frame, 4, 2);
|
multiline:NSMakeSize(292, 96)] placeIn:content x:4 y:2];
|
||||||
txt.preferredMaxLayoutWidth = NSWidth(txt.frame);
|
|
||||||
txt.lineBreakMode = NSLineBreakByWordWrapping;
|
|
||||||
txt.maximumNumberOfLines = 7;
|
|
||||||
[content addSubview:txt];
|
|
||||||
|
|
||||||
self.warningPopover = pop;
|
|
||||||
self.warningText = txt;
|
|
||||||
// Reload button is only visible on 5xx server error (right of ––––)
|
// Reload button is only visible on 5xx server error (right of ––––)
|
||||||
self.warningReload = [[[[NSView buttonIcon:NSImageNameRefreshTemplate size:16] placeIn:content x:35 yTop:21]
|
self.warningReload = [[[[NSView buttonIcon:NSImageNameRefreshTemplate size:16] placeIn:content x:35 yTop:21]
|
||||||
tooltip:NSLocalizedString(@"Retry download (⌘R)", nil)]
|
tooltip:NSLocalizedString(@"Retry download (⌘R)", nil)]
|
||||||
|
|||||||
@@ -50,9 +50,7 @@
|
|||||||
GrayLabel(NSLocalizedString(@"median:", nil)), [self createInlineButton:info[@"median"] callback:callback]];
|
GrayLabel(NSLocalizedString(@"median:", nil)), [self createInlineButton:info[@"median"] callback:callback]];
|
||||||
NSView *buttonsView = [self placeViewsHorizontally:arr];
|
NSView *buttonsView = [self placeViewsHorizontally:arr];
|
||||||
|
|
||||||
CGFloat w = NSWidth(buttonsView.frame);
|
CGFloat w = NSMaxWidth(dateView, buttonsView);
|
||||||
if (w < NSWidth(dateView.frame))
|
|
||||||
w = NSWidth(dateView.frame);
|
|
||||||
[self setFrameSize:NSMakeSize(w, NSHeight(buttonsView.frame) + PAD_M + NSHeight(dateView.frame))];
|
[self setFrameSize:NSMakeSize(w, NSHeight(buttonsView.frame) + PAD_M + NSHeight(dateView.frame))];
|
||||||
|
|
||||||
[dateView placeIn:self x:CENTER yTop:0];
|
[dateView placeIn:self x:CENTER yTop:0];
|
||||||
|
|||||||
@@ -29,5 +29,6 @@
|
|||||||
- (void)setUnreadCountRelative:(NSInteger)count;
|
- (void)setUnreadCountRelative:(NSInteger)count;
|
||||||
- (void)asyncReloadUnreadCount;
|
- (void)asyncReloadUnreadCount;
|
||||||
- (void)updateBarIcon;
|
- (void)updateBarIcon;
|
||||||
|
- (void)showWelcomeMessage;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#import "UserPrefs.h"
|
#import "UserPrefs.h"
|
||||||
#import "BarMenu.h"
|
#import "BarMenu.h"
|
||||||
#import "AppHook.h"
|
#import "AppHook.h"
|
||||||
|
#import "NSView+Ext.h"
|
||||||
|
|
||||||
@interface BarStatusItem()
|
@interface BarStatusItem()
|
||||||
@property (strong) BarMenu *barMenu;
|
@property (strong) BarMenu *barMenu;
|
||||||
@@ -45,8 +46,8 @@
|
|||||||
self.statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
|
self.statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
|
||||||
self.statusItem.highlightMode = YES;
|
self.statusItem.highlightMode = YES;
|
||||||
self.unreadCountTotal = 0;
|
self.unreadCountTotal = 0;
|
||||||
[self updateBarIcon];
|
self.statusItem.image = [NSImage imageNamed:RSSImageMenuBarIconActive];
|
||||||
[self asyncReloadUnreadCount];
|
self.statusItem.image.template = YES;
|
||||||
// Add empty menu (will be populated once opened)
|
// Add empty menu (will be populated once opened)
|
||||||
self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"];
|
self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainMenuWillOpen) name:NSMenuDidBeginTrackingNotification object:self.statusItem.menu];
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainMenuWillOpen) name:NSMenuDidBeginTrackingNotification object:self.statusItem.menu];
|
||||||
@@ -126,6 +127,26 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show popover with a brief notice that baRSS is running in the menu bar
|
||||||
|
- (void)showWelcomeMessage {
|
||||||
|
NSString *title = [NSString stringWithFormat:NSLocalizedString(@"Welcome to %@", nil), [UserPrefs appName]];
|
||||||
|
NSString *message = NSLocalizedString(@"There's no application window.\nEverything is up there.", nil);
|
||||||
|
NSTextField *head = [[NSView label:title] bold];
|
||||||
|
NSTextField *body = [[NSView label:message] small];
|
||||||
|
|
||||||
|
const CGFloat pad = 12;
|
||||||
|
CGFloat icon = NSHeight(head.frame) + PAD_S + NSHeight(body.frame);
|
||||||
|
CGFloat dx = pad + icon + PAD_L; // where text begins
|
||||||
|
|
||||||
|
NSPopover *pop = [NSView popover:NSMakeSize(dx + NSMaxWidth(head, body) + pad, icon + 2 * pad)];
|
||||||
|
NSView *content = pop.contentViewController.view;
|
||||||
|
|
||||||
|
[[NSView imageView:NSImageNameApplicationIcon size:icon] placeIn:content x:pad y:pad];
|
||||||
|
[head placeIn:content x:dx yTop:pad];
|
||||||
|
[body placeIn:content x:dx y:pad];
|
||||||
|
[pop showRelativeToRect:NSZeroRect ofView:self.statusItem.button preferredEdge:NSRectEdgeMaxY];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Main Menu Handling
|
#pragma mark - Main Menu Handling
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user