And disable it by default in new installations, otherwise leave the setting alone. The disablement setting is shared with the engine setting, so the default should not really change anything, except for new installs. Also, the time/pitch shifting dialog disables itself and displays an obvious notice button, which opens the Rubber Band settings. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
821 lines
29 KiB
Objective-C
821 lines
29 KiB
Objective-C
#import "AppController.h"
|
|
#import "Cog-Swift.h"
|
|
#import "FileTreeController.h"
|
|
#import "FileTreeOutlineView.h"
|
|
#import "FileTreeViewController.h"
|
|
#import "FontSizetoLineHeightTransformer.h"
|
|
#import "OpenURLPanel.h"
|
|
#import "PathNode.h"
|
|
#import "PlaybackController.h"
|
|
#import "PlaylistController.h"
|
|
#import "PlaylistEntry.h"
|
|
#import "PlaylistLoader.h"
|
|
#import "PlaylistView.h"
|
|
#import "RubberbandEngineTransformer.h"
|
|
#import "SQLiteStore.h"
|
|
#import "SandboxBroker.h"
|
|
#import "SpotlightWindowController.h"
|
|
#import "StringToURLTransformer.h"
|
|
#import <CogAudio/Status.h>
|
|
|
|
#import "DualWindow.h"
|
|
#import "Logging.h"
|
|
#import "MiniModeMenuTitleTransformer.h"
|
|
|
|
#import "ColorToValueTransformer.h"
|
|
|
|
#import "TotalTimeTransformer.h"
|
|
|
|
#import "Shortcuts.h"
|
|
#import <MASShortcut/Shortcut.h>
|
|
|
|
#import "PreferencesController.h"
|
|
|
|
@import Firebase;
|
|
|
|
void *kAppControllerContext = &kAppControllerContext;
|
|
|
|
BOOL kAppControllerShuttingDown = NO;
|
|
|
|
static AppController *kAppController = nil;
|
|
|
|
@implementation AppController {
|
|
BOOL _isFullToolbarStyle;
|
|
}
|
|
|
|
@synthesize mainWindow;
|
|
@synthesize miniWindow;
|
|
|
|
+ (void)initialize {
|
|
// Register transformers
|
|
NSValueTransformer *stringToURLTransformer = [[StringToURLTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:stringToURLTransformer
|
|
forName:@"StringToURLTransformer"];
|
|
|
|
NSValueTransformer *fontSizetoLineHeightTransformer =
|
|
[[FontSizetoLineHeightTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:fontSizetoLineHeightTransformer
|
|
forName:@"FontSizetoLineHeightTransformer"];
|
|
|
|
NSValueTransformer *miniModeMenuTitleTransformer = [[MiniModeMenuTitleTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:miniModeMenuTitleTransformer
|
|
forName:@"MiniModeMenuTitleTransformer"];
|
|
|
|
NSValueTransformer *colorToValueTransformer = [[ColorToValueTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:colorToValueTransformer
|
|
forName:@"ColorToValueTransformer"];
|
|
|
|
NSValueTransformer *totalTimeTransformer = [[TotalTimeTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:totalTimeTransformer
|
|
forName:@"TotalTimeTransformer"];
|
|
|
|
NSValueTransformer *numberHertzToStringTransformer = [[NumberHertzToStringTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:numberHertzToStringTransformer
|
|
forName:@"NumberHertzToStringTransformer"];
|
|
|
|
NSValueTransformer *rubberbandEngineEnabledTransformer = [[RubberbandEngineEnabledTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:rubberbandEngineEnabledTransformer
|
|
forName:@"RubberbandEngineEnabledTransformer"];
|
|
|
|
NSValueTransformer *rubberbandEngineHiddenTransformer = [[RubberbandEngineHiddenTransformer alloc] init];
|
|
[NSValueTransformer setValueTransformer:rubberbandEngineHiddenTransformer
|
|
forName:@"RubberbandEngineHiddenTransformer"];
|
|
}
|
|
- (id)init {
|
|
self = [super init];
|
|
if(self) {
|
|
[self initDefaults];
|
|
|
|
queue = [[NSOperationQueue alloc] init];
|
|
|
|
kAppController = self;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (IBAction)openFiles:(id)sender {
|
|
NSOpenPanel *p;
|
|
|
|
p = [NSOpenPanel openPanel];
|
|
|
|
[p setAllowedFileTypes:[playlistLoader acceptableFileTypes]];
|
|
[p setCanChooseDirectories:YES];
|
|
[p setAllowsMultipleSelection:YES];
|
|
[p setResolvesAliases:YES];
|
|
|
|
[p beginSheetModalForWindow:mainWindow
|
|
completionHandler:^(NSInteger result) {
|
|
if(result == NSModalResponseOK) {
|
|
NSDictionary *loadEntryData = @{@"entries": [p URLs],
|
|
@"sort": @(YES),
|
|
@"origin": @(URLOriginExternal)};
|
|
[self->playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntryData];
|
|
} else {
|
|
[p close];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (IBAction)savePlaylist:(id)sender {
|
|
NSSavePanel *p;
|
|
|
|
p = [NSSavePanel savePanel];
|
|
|
|
/* Yes, this is deprecated. Yes, this is required to give the dialog
|
|
* a default set of filename extensions to save, including adding an
|
|
* extension if the user does not supply one. */
|
|
[p setAllowedFileTypes:@[@"m3u", @"pls"]];
|
|
|
|
[p beginSheetModalForWindow:mainWindow
|
|
completionHandler:^(NSInteger result) {
|
|
if(result == NSModalResponseOK) {
|
|
[self->playlistLoader save:[[p URL] path]];
|
|
} else {
|
|
[p close];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (IBAction)openURL:(id)sender {
|
|
OpenURLPanel *p;
|
|
|
|
p = [OpenURLPanel openURLPanel];
|
|
|
|
[p beginSheetWithWindow:mainWindow delegate:self didEndSelector:@selector(openURLPanelDidEnd:returnCode:contextInfo:) contextInfo:nil];
|
|
}
|
|
|
|
- (void)openURLPanelDidEnd:(OpenURLPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo {
|
|
if(returnCode == NSModalResponseOK) {
|
|
NSDictionary *loadEntriesData = @{ @"entries": @[[panel url]],
|
|
@"sort": @(NO),
|
|
@"origin": @(URLOriginExternal) };
|
|
[playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntriesData];
|
|
}
|
|
}
|
|
|
|
- (IBAction)delEntries:(id)sender {
|
|
[playlistController remove:self];
|
|
}
|
|
|
|
- (PlaylistEntry *)currentEntry {
|
|
return [playlistController currentEntry];
|
|
}
|
|
|
|
- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key {
|
|
return [key isEqualToString:@"currentEntry"];
|
|
}
|
|
|
|
- (void)awakeFromNib {
|
|
#if DEBUG
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @(NO) }];
|
|
#else
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @(YES) }];
|
|
#endif
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"crashlyticsConsented": @(NO),
|
|
@"crashlyticsAskedConsent": @(NO) }];
|
|
|
|
[FIRApp configure];
|
|
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.crashlyticsConsented" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kAppControllerContext];
|
|
|
|
[[totalTimeField cell] setBackgroundStyle:NSBackgroundStyleRaised];
|
|
|
|
[self.infoButton setToolTip:NSLocalizedString(@"InfoButtonTooltip", @"")];
|
|
[self.infoButtonMini setToolTip:NSLocalizedString(@"InfoButtonTooltip", @"")];
|
|
[shuffleButton setToolTip:NSLocalizedString(@"ShuffleButtonTooltip", @"")];
|
|
[repeatButton setToolTip:NSLocalizedString(@"RepeatButtonTooltip", @"")];
|
|
[randomizeButton setToolTip:NSLocalizedString(@"RandomizeButtonTooltip", @"")];
|
|
[fileButton setToolTip:NSLocalizedString(@"FileButtonTooltip", @"")];
|
|
|
|
[self registerHotKeys];
|
|
|
|
(void)[spotlightWindowController init];
|
|
|
|
[[playlistController undoManager] disableUndoRegistration];
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
|
NSString *basePath = [[paths firstObject] stringByAppendingPathComponent:@"Cog"];
|
|
|
|
NSString *dbFilename = @"Default.sqlite";
|
|
|
|
NSString *oldFilename = @"Default.m3u";
|
|
NSString *newFilename = @"Default.xml";
|
|
|
|
BOOL dataStorePresent = [playlistLoader addDataStore];
|
|
|
|
if(!dataStorePresent) {
|
|
if([[NSFileManager defaultManager] fileExistsAtPath:[basePath stringByAppendingPathComponent:dbFilename]]) {
|
|
[playlistLoader addDatabase];
|
|
} else if([[NSFileManager defaultManager] fileExistsAtPath:[basePath stringByAppendingPathComponent:newFilename]]) {
|
|
[playlistLoader addURL:[NSURL fileURLWithPath:[basePath stringByAppendingPathComponent:newFilename]]];
|
|
} else {
|
|
[playlistLoader addURL:[NSURL fileURLWithPath:[basePath stringByAppendingPathComponent:oldFilename]]];
|
|
}
|
|
}
|
|
|
|
SandboxBroker *sandboxBroker = [SandboxBroker sharedSandboxBroker];
|
|
if(!sandboxBroker) {
|
|
ALog(@"Sandbox broker init failed.");
|
|
}
|
|
[SandboxBroker cleanupFolderAccess];
|
|
|
|
[[playlistController undoManager] enableUndoRegistration];
|
|
|
|
int lastStatus = (int)[[NSUserDefaults standardUserDefaults] integerForKey:@"lastPlaybackStatus"];
|
|
|
|
if(lastStatus != CogStatusStopped) {
|
|
NSPredicate *hasUrlPredicate = [NSPredicate predicateWithFormat:@"urlString != nil && urlString != %@", @""];
|
|
NSPredicate *deletedPredicate = [NSPredicate predicateWithFormat:@"deLeted == NO || deLeted == nil"];
|
|
NSPredicate *currentPredicate = [NSPredicate predicateWithFormat:@"current == YES"];
|
|
|
|
NSCompoundPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[deletedPredicate, hasUrlPredicate, currentPredicate]];
|
|
|
|
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"];
|
|
request.predicate = predicate;
|
|
|
|
NSError *error = nil;
|
|
NSArray *results = [playlistController.persistentContainer.viewContext executeFetchRequest:request error:&error];
|
|
|
|
if(results && [results count] > 0) {
|
|
PlaylistEntry *pe = results[0];
|
|
if([[NSUserDefaults standardUserDefaults] boolForKey:@"resumePlaybackOnStartup"]) {
|
|
[playbackController playEntryAtIndex:pe.index startPaused:(lastStatus == CogStatusPaused) andSeekTo:@(pe.currentPosition)];
|
|
} else {
|
|
pe.current = NO;
|
|
pe.stopAfter = NO;
|
|
pe.currentPosition = 0.0;
|
|
pe.countAdded = NO;
|
|
[playlistController commitPersistentStore];
|
|
}
|
|
// Bug fix
|
|
if([results count] > 1) {
|
|
for(size_t i = 1; i < [results count]; ++i) {
|
|
PlaylistEntry *pe = results[i];
|
|
[pe setCurrent:NO];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore mini mode
|
|
[self setMiniMode:[[NSUserDefaults standardUserDefaults] boolForKey:@"miniMode"]];
|
|
|
|
[self setToolbarStyle:[[NSUserDefaults standardUserDefaults] boolForKey:@"toolbarStyleFull"]];
|
|
|
|
[self setFloatingMiniWindow:[[NSUserDefaults standardUserDefaults]
|
|
boolForKey:@"floatingMiniWindow"]];
|
|
|
|
// We need file tree view to restore its state here
|
|
// so attempt to access file tree view controller's root view
|
|
// to force it to read nib and create file tree view for us
|
|
//
|
|
// TODO: there probably is a more elegant way to do all this
|
|
// but i'm too stupid/tired to figure it out now
|
|
[fileTreeViewController view];
|
|
|
|
FileTreeOutlineView *outlineView = [fileTreeViewController outlineView];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nodeExpanded:) name:NSOutlineViewItemDidExpandNotification object:outlineView];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nodeCollapsed:) name:NSOutlineViewItemDidCollapseNotification object:outlineView];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDockMenu:) name:CogPlaybackDidBeginNotificiation object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDockMenu:) name:CogPlaybackDidStopNotificiation object:nil];
|
|
|
|
[self updateDockMenu:nil];
|
|
|
|
NSArray *expandedNodesArray = [[NSUserDefaults standardUserDefaults] valueForKey:@"fileTreeViewExpandedNodes"];
|
|
|
|
if(expandedNodesArray) {
|
|
expandedNodes = [[NSMutableSet alloc] initWithArray:expandedNodesArray];
|
|
} else {
|
|
expandedNodes = [[NSMutableSet alloc] init];
|
|
}
|
|
|
|
DLog(@"Nodes to expand: %@", [expandedNodes description]);
|
|
|
|
DLog(@"Num of rows: %ld", [outlineView numberOfRows]);
|
|
|
|
if(!outlineView) {
|
|
DLog(@"outlineView is NULL!");
|
|
}
|
|
|
|
[outlineView reloadData];
|
|
|
|
for(NSInteger i = 0; i < [outlineView numberOfRows]; i++) {
|
|
PathNode *pn = [outlineView itemAtRow:i];
|
|
NSString *str = [[pn URL] absoluteString];
|
|
|
|
if([expandedNodes containsObject:str]) {
|
|
[outlineView expandItem:pn];
|
|
}
|
|
}
|
|
|
|
[self addObserver:self
|
|
forKeyPath:@"playlistController.currentEntry"
|
|
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
|
|
context:kAppControllerContext];
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
ofObject:(id)object
|
|
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
|
|
context:(void *)context {
|
|
if(context != kAppControllerContext) {
|
|
return;
|
|
}
|
|
|
|
if([keyPath isEqualToString:@"values.crashlyticsConsented"]) {
|
|
BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"crashlyticsConsented"];
|
|
[[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:enabled];
|
|
[FIRAnalytics setAnalyticsCollectionEnabled:enabled];
|
|
} else if([keyPath isEqualToString:@"playlistController.currentEntry"]) {
|
|
PlaylistEntry *entry = playlistController.currentEntry;
|
|
NSString *appTitle = NSLocalizedString(@"CogTitle", @"");
|
|
if(!entry) {
|
|
miniWindow.title = appTitle;
|
|
mainWindow.title = appTitle;
|
|
if(@available(macOS 11.0, *)) {
|
|
miniWindow.subtitle = @"";
|
|
mainWindow.subtitle = @"";
|
|
}
|
|
|
|
self.infoButton.imageScaling = NSImageScaleNone;
|
|
self.infoButton.image = [NSImage imageNamed:@"infoTemplate"];
|
|
self.infoButtonMini.imageScaling = NSImageScaleNone;
|
|
self.infoButtonMini.image = [NSImage imageNamed:@"infoTemplate"];
|
|
}
|
|
|
|
if(entry.albumArt) {
|
|
self.infoButton.imageScaling = NSImageScaleProportionallyUpOrDown;
|
|
self.infoButton.image = playlistController.currentEntry.albumArt;
|
|
self.infoButtonMini.imageScaling = NSImageScaleProportionallyUpOrDown;
|
|
self.infoButtonMini.image = playlistController.currentEntry.albumArt;
|
|
} else {
|
|
self.infoButton.imageScaling = NSImageScaleNone;
|
|
self.infoButton.image = [NSImage imageNamed:@"infoTemplate"];
|
|
self.infoButtonMini.imageScaling = NSImageScaleNone;
|
|
self.infoButtonMini.image = [NSImage imageNamed:@"infoTemplate"];
|
|
}
|
|
|
|
if(@available(macOS 11.0, *)) {
|
|
NSString *title = appTitle;
|
|
if(entry.title) {
|
|
title = entry.title;
|
|
}
|
|
miniWindow.title = title;
|
|
mainWindow.title = title;
|
|
|
|
NSString *subtitle = @"";
|
|
NSMutableArray<NSString *> *subtitleItems = [NSMutableArray array];
|
|
if(entry.album && ![entry.album isEqualToString:@""]) {
|
|
[subtitleItems addObject:entry.album];
|
|
}
|
|
if(entry.artist && ![entry.artist isEqualToString:@""]) {
|
|
[subtitleItems addObject:entry.artist];
|
|
}
|
|
|
|
if([subtitleItems count]) {
|
|
subtitle = [subtitleItems componentsJoinedByString:@" - "];
|
|
}
|
|
|
|
miniWindow.subtitle = subtitle;
|
|
mainWindow.subtitle = subtitle;
|
|
} else {
|
|
NSString *title = appTitle;
|
|
if(entry.display) {
|
|
title = entry.display;
|
|
}
|
|
miniWindow.title = title;
|
|
mainWindow.title = title;
|
|
}
|
|
} else if([keyPath isEqualToString:@"finished"]) {
|
|
NSProgress *progress = (NSProgress *)object;
|
|
if([progress isFinished]) {
|
|
playbackController.progressOverall = nil;
|
|
[NSApp terminate:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)nodeExpanded:(NSNotification *)notification {
|
|
PathNode *node = [[notification userInfo] objectForKey:@"NSObject"];
|
|
NSString *url = [[node URL] absoluteString];
|
|
|
|
[expandedNodes addObject:url];
|
|
}
|
|
|
|
- (void)nodeCollapsed:(NSNotification *)notification {
|
|
PathNode *node = [[notification userInfo] objectForKey:@"NSObject"];
|
|
NSString *url = [[node URL] absoluteString];
|
|
|
|
[expandedNodes removeObject:url];
|
|
}
|
|
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
|
|
if(playbackController.progressOverall) {
|
|
[playbackController.progressOverall addObserver:self forKeyPath:@"finished" options:0 context:kAppControllerContext];
|
|
return NSTerminateLater;
|
|
} else {
|
|
return NSTerminateNow;
|
|
}
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
|
kAppControllerShuttingDown = YES;
|
|
|
|
CogStatus currentStatus = [playbackController playbackStatus];
|
|
|
|
if(currentStatus == CogStatusStopping)
|
|
currentStatus = CogStatusStopped;
|
|
|
|
[playbackController stop:self];
|
|
|
|
[[NSUserDefaults standardUserDefaults] setInteger:currentStatus forKey:@"lastPlaybackStatus"];
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
|
NSString *folder = [[paths firstObject] stringByAppendingPathComponent:@"Cog"];
|
|
|
|
if([fileManager fileExistsAtPath:folder] == NO) {
|
|
[fileManager createDirectoryAtPath:folder withIntermediateDirectories:NO attributes:nil error:nil];
|
|
}
|
|
|
|
[playlistController clearFilterPredicate:self];
|
|
|
|
NSMutableDictionary<NSString *, AlbumArtwork *> *artLeftovers = [playlistController.persistentArtStorage mutableCopy];
|
|
|
|
NSManagedObjectContext *moc = playlistController.persistentContainer.viewContext;
|
|
|
|
for(PlaylistEntry *pe in playlistController.arrangedObjects) {
|
|
if(pe.deLeted) {
|
|
[moc deleteObject:pe];
|
|
continue;
|
|
}
|
|
if([artLeftovers objectForKey:pe.artHash]) {
|
|
[artLeftovers removeObjectForKey:pe.artHash];
|
|
}
|
|
}
|
|
|
|
for(NSString *key in artLeftovers) {
|
|
[moc deleteObject:[artLeftovers objectForKey:key]];
|
|
}
|
|
|
|
[playlistController commitPersistentStore];
|
|
|
|
if([SQLiteStore databaseStarted]) {
|
|
[[SQLiteStore sharedStore] shutdown];
|
|
}
|
|
|
|
NSError *error;
|
|
NSString *fileName = @"Default.sqlite";
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:[folder stringByAppendingPathComponent:fileName] error:&error];
|
|
|
|
fileName = @"Default.xml";
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:[folder stringByAppendingPathComponent:fileName] error:&error];
|
|
|
|
fileName = @"Default.m3u";
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:[folder stringByAppendingPathComponent:fileName] error:&error];
|
|
|
|
DLog(@"Shutting down sandbox broker");
|
|
[[SandboxBroker sharedSandboxBroker] shutdown];
|
|
|
|
DLog(@"Saving expanded nodes: %@", [expandedNodes description]);
|
|
|
|
[[NSUserDefaults standardUserDefaults] setValue:[expandedNodes allObjects] forKey:@"fileTreeViewExpandedNodes"];
|
|
// Workaround window not restoring it's size and position.
|
|
[miniWindow setContentSize:NSMakeSize(miniWindow.frame.size.width, 1)];
|
|
[miniWindow saveFrameUsingName:@"Mini Window"];
|
|
}
|
|
|
|
- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
|
|
if(flag == NO)
|
|
[mainWindow makeKeyAndOrderFront:self]; // TODO: do we really need this? We never close the main window.
|
|
|
|
for(NSWindow *win in [NSApp windows]) // Maximizing all windows
|
|
if([win isMiniaturized])
|
|
[win deminiaturize:self];
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
|
|
NSArray *urls = @[[NSURL fileURLWithPath:filename]];
|
|
NSDictionary *loadEntriesData = @{ @"entries": urls,
|
|
@"sort": @(NO),
|
|
@"origin": @(URLOriginExternal) };
|
|
[playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntriesData];
|
|
return YES;
|
|
}
|
|
|
|
- (void)application:(NSApplication *)theApplication openFiles:(NSArray *)filenames {
|
|
// Need to convert to urls
|
|
NSMutableArray *urls = [NSMutableArray array];
|
|
|
|
for(NSString *filename in filenames) {
|
|
NSURL *url = nil;
|
|
if([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
|
|
url = [NSURL fileURLWithPath:filename];
|
|
} else {
|
|
if([filename hasPrefix:@"/http/::"] ||
|
|
[filename hasPrefix:@"/https/::"]) {
|
|
// Stupid Carbon bodge for AppleScript
|
|
NSString *method = nil;
|
|
NSString *server = nil;
|
|
NSString *path = nil;
|
|
|
|
NSScanner *objScanner = [NSScanner scannerWithString:filename];
|
|
|
|
if(![objScanner scanString:@"/" intoString:nil] ||
|
|
![objScanner scanUpToString:@"/" intoString:&method] ||
|
|
![objScanner scanString:@"/::" intoString:nil] ||
|
|
![objScanner scanUpToString:@":" intoString:&server] ||
|
|
![objScanner scanString:@":" intoString:nil]) {
|
|
continue;
|
|
}
|
|
[objScanner scanUpToCharactersFromSet:[NSCharacterSet illegalCharacterSet] intoString:&path];
|
|
// Colons in server were converted to shashes, convert back
|
|
NSString *convertedServer = [server stringByReplacingOccurrencesOfString:@"/" withString:@":"];
|
|
// Slashes in path were converted to colons, convert back
|
|
NSString *convertedPath = [path stringByReplacingOccurrencesOfString:@":" withString:@"/"];
|
|
url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@/%@", method, convertedServer, convertedPath]];
|
|
}
|
|
}
|
|
if(url) {
|
|
[urls addObject:url];
|
|
}
|
|
}
|
|
|
|
NSDictionary *loadEntriesData = @{ @"entries": urls,
|
|
@"sort": @(YES),
|
|
@"origin": @(URLOriginExternal) };
|
|
|
|
[playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntriesData];
|
|
|
|
[theApplication replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
|
|
}
|
|
|
|
- (IBAction)privacyPolicy:(id)sender {
|
|
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:NSLocalizedString(@"PrivacyPolicyURL", @"Privacy policy URL from Iubenda.")]];
|
|
}
|
|
|
|
- (IBAction)feedback:(id)sender {
|
|
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
|
|
|
NSArray<NSURLQueryItem *> *query = @[
|
|
[NSURLQueryItem queryItemWithName:@"labels"
|
|
value:@"bug"],
|
|
[NSURLQueryItem queryItemWithName:@"template"
|
|
value:@"bug_report.md"],
|
|
[NSURLQueryItem queryItemWithName:@"title"
|
|
value:[NSString stringWithFormat:@"[Cog %@] ", version]]
|
|
];
|
|
NSURLComponents *components =
|
|
[NSURLComponents componentsWithString:@"https://github.com/losnoco/Cog/issues/new"];
|
|
|
|
components.queryItems = query;
|
|
|
|
[[NSWorkspace sharedWorkspace] openURL:components.URL];
|
|
}
|
|
|
|
- (void)initDefaults {
|
|
NSMutableDictionary *userDefaultsValuesDict = [NSMutableDictionary dictionary];
|
|
|
|
// Font defaults
|
|
float fFontSize = [NSFont systemFontSizeForControlSize:NSControlSizeRegular];
|
|
NSNumber *fontSize = @(fFontSize);
|
|
[userDefaultsValuesDict setObject:fontSize forKey:@"fontSize"];
|
|
|
|
NSString *feedURLdefault = @"https://cogcdn.cog.losno.co/mercury.xml";
|
|
[userDefaultsValuesDict setObject:feedURLdefault forKey:@"SUFeedURL"];
|
|
|
|
[userDefaultsValuesDict setObject:@"enqueueAndPlay" forKey:@"openingFilesBehavior"];
|
|
[userDefaultsValuesDict setObject:@"enqueue" forKey:@"openingFilesAlteredBehavior"];
|
|
|
|
[userDefaultsValuesDict setObject:@"albumGainWithPeak" forKey:@"volumeScaling"];
|
|
|
|
[userDefaultsValuesDict setObject:@"cubic" forKey:@"resampling"];
|
|
|
|
[userDefaultsValuesDict setObject:@(CogStatusStopped) forKey:@"lastPlaybackStatus"];
|
|
|
|
[userDefaultsValuesDict setObject:@"dls appl" forKey:@"midiPlugin"];
|
|
|
|
[userDefaultsValuesDict setObject:@"default" forKey:@"midi.flavor"];
|
|
|
|
[userDefaultsValuesDict setObject:@(NO) forKey:@"resumePlaybackOnStartup"];
|
|
|
|
[userDefaultsValuesDict setObject:@(NO) forKey:@"quitOnNaturalStop"];
|
|
|
|
[userDefaultsValuesDict setObject:@(NO) forKey:@"spectrumFreqMode"];
|
|
[userDefaultsValuesDict setObject:@(YES) forKey:@"spectrumProjectionMode"];
|
|
|
|
NSValueTransformer *colorToValueTransformer = [NSValueTransformer valueTransformerForName:@"ColorToValueTransformer"];
|
|
|
|
NSData *barColor = [colorToValueTransformer reverseTransformedValue:[NSColor colorWithSRGBRed:1.0 green:0.5 blue:0 alpha:1.0]];
|
|
NSData *dotColor = [colorToValueTransformer reverseTransformedValue:[NSColor systemRedColor]];
|
|
|
|
[userDefaultsValuesDict setObject:@(YES) forKey:@"spectrumSceneKit"];
|
|
[userDefaultsValuesDict setObject:barColor forKey:@"spectrumBarColor"];
|
|
[userDefaultsValuesDict setObject:dotColor forKey:@"spectrumDotColor"];
|
|
|
|
[userDefaultsValuesDict setObject:@(150.0) forKey:@"synthDefaultSeconds"];
|
|
[userDefaultsValuesDict setObject:@(8.0) forKey:@"synthDefaultFadeSeconds"];
|
|
[userDefaultsValuesDict setObject:@(2) forKey:@"synthDefaultLoopCount"];
|
|
[userDefaultsValuesDict setObject:@(44100) forKey:@"synthSampleRate"];
|
|
|
|
// Register and sync defaults
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:userDefaultsValuesDict];
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
|
|
// And if the existing feed URL is broken due to my ineptitude with the above defaults, fix it
|
|
NSSet<NSString *> *brokenFeedURLs = [NSSet setWithObjects:
|
|
@"https://kode54.net/cog/stable.xml",
|
|
@"https://kode54.net/cog/mercury.xml"
|
|
@"https://www.kode54.net/cog/mercury.xml",
|
|
@"https://f.losno.co/cog/mercury.xml",
|
|
nil];
|
|
NSString *feedURL = [[NSUserDefaults standardUserDefaults] stringForKey:@"SUFeedURL"];
|
|
if([brokenFeedURLs containsObject:feedURL]) {
|
|
[[NSUserDefaults standardUserDefaults] setValue:feedURLdefault forKey:@"SUFeedURL"];
|
|
}
|
|
|
|
NSString *oldMidiPlugin = [[NSUserDefaults standardUserDefaults] stringForKey:@"midi.plugin"];
|
|
if(oldMidiPlugin) {
|
|
[[NSUserDefaults standardUserDefaults] setValue:oldMidiPlugin forKey:@"midiPlugin"];
|
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"midi.plugin"];
|
|
}
|
|
|
|
// if([[[NSUserDefaults standardUserDefaults] stringForKey:@"midiPlugin"] isEqualToString:@"BASSMIDI"]) {
|
|
// [[NSUserDefaults standardUserDefaults] setValue:@"FluidSynth" forKey:@"midiPlugin"];
|
|
// }
|
|
if([[[NSUserDefaults standardUserDefaults] stringForKey:@"midiPlugin"] isEqualToString:@"FluidSynth"]) {
|
|
[[NSUserDefaults standardUserDefaults] setValue:@"BASSMIDI" forKey:@"midiPlugin"];
|
|
}
|
|
}
|
|
|
|
/* Unassign previous handler first, so dealloc can unregister it from the global map before the new instances are assigned */
|
|
- (void)registerHotKeys {
|
|
MASShortcutBinder *binder = [MASShortcutBinder sharedBinder];
|
|
[binder bindShortcutWithDefaultsKey:CogPlayShortcutKey
|
|
toAction:^{
|
|
[self clickPlay];
|
|
}];
|
|
|
|
[binder bindShortcutWithDefaultsKey:CogNextShortcutKey
|
|
toAction:^{
|
|
[self clickNext];
|
|
}];
|
|
|
|
[binder bindShortcutWithDefaultsKey:CogPrevShortcutKey
|
|
toAction:^{
|
|
[self clickPrev];
|
|
}];
|
|
|
|
[binder bindShortcutWithDefaultsKey:CogSpamShortcutKey
|
|
toAction:^{
|
|
[self clickSpam];
|
|
}];
|
|
|
|
[binder bindShortcutWithDefaultsKey:CogFadeShortcutKey
|
|
toAction:^{
|
|
[self clickFade];
|
|
}];
|
|
}
|
|
|
|
- (void)clickPlay {
|
|
[playbackController playPauseResume:self];
|
|
}
|
|
|
|
- (void)clickPause {
|
|
[playbackController pause:self];
|
|
}
|
|
|
|
- (void)clickStop {
|
|
[playbackController stop:self];
|
|
}
|
|
|
|
- (void)clickPrev {
|
|
[playbackController prev:nil];
|
|
}
|
|
|
|
- (void)clickNext {
|
|
[playbackController next:nil];
|
|
}
|
|
|
|
- (void)clickSpam {
|
|
[playbackController spam:nil];
|
|
}
|
|
|
|
- (void)clickFade {
|
|
[playbackController fade:nil];
|
|
}
|
|
|
|
- (void)clickSeek:(NSTimeInterval)position {
|
|
[playbackController seek:self toTime:position];
|
|
}
|
|
|
|
- (void)changeFontSize:(float)size {
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
float fCurrentSize = [defaults floatForKey:@"fontSize"];
|
|
NSNumber *newSize = @(fCurrentSize + size);
|
|
[defaults setObject:newSize forKey:@"fontSize"];
|
|
}
|
|
|
|
- (IBAction)increaseFontSize:(id)sender {
|
|
[self changeFontSize:1];
|
|
}
|
|
|
|
- (IBAction)decreaseFontSize:(id)sender {
|
|
[self changeFontSize:-1];
|
|
}
|
|
|
|
- (IBAction)toggleMiniMode:(id)sender {
|
|
[self setMiniMode:(!miniMode)];
|
|
}
|
|
|
|
- (BOOL)miniMode {
|
|
return miniMode;
|
|
}
|
|
|
|
- (void)setMiniMode:(BOOL)newMiniMode {
|
|
miniMode = newMiniMode;
|
|
[[NSUserDefaults standardUserDefaults] setBool:miniMode forKey:@"miniMode"];
|
|
|
|
NSWindow *windowToShow = miniMode ? miniWindow : mainWindow;
|
|
NSWindow *windowToHide = miniMode ? mainWindow : miniWindow;
|
|
[windowToHide close];
|
|
[windowToShow makeKeyAndOrderFront:self];
|
|
}
|
|
|
|
- (IBAction)toggleToolbarStyle:(id)sender {
|
|
[self setToolbarStyle:!_isFullToolbarStyle];
|
|
}
|
|
|
|
- (void)setToolbarStyle:(BOOL)full {
|
|
_isFullToolbarStyle = full;
|
|
[[NSUserDefaults standardUserDefaults] setBool:full forKey:@"toolbarStyleFull"];
|
|
DLog("Changed toolbar style: %@", (full ? @"full" : @"compact"));
|
|
|
|
if(@available(macOS 11.0, *)) {
|
|
NSWindowToolbarStyle style =
|
|
full ? NSWindowToolbarStyleExpanded : NSWindowToolbarStyleUnified;
|
|
mainWindow.toolbarStyle = style;
|
|
miniWindow.toolbarStyle = style;
|
|
} else {
|
|
NSWindowTitleVisibility titleVisibility = full ? NSWindowTitleVisible : NSWindowTitleHidden;
|
|
mainWindow.titleVisibility = titleVisibility;
|
|
miniWindow.titleVisibility = titleVisibility;
|
|
}
|
|
|
|
// Fix empty area after changing toolbar style in mini window as it has no content view
|
|
[miniWindow setContentSize:NSMakeSize(miniWindow.frame.size.width, 0)];
|
|
}
|
|
|
|
- (void)setFloatingMiniWindow:(BOOL)floatingMiniWindow {
|
|
_floatingMiniWindow = floatingMiniWindow;
|
|
[[NSUserDefaults standardUserDefaults] setBool:floatingMiniWindow forKey:@"floatingMiniWindow"];
|
|
NSWindowLevel level = floatingMiniWindow ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
|
[miniWindow setLevel:level];
|
|
}
|
|
|
|
- (void)updateDockMenu:(NSNotification *)notification {
|
|
PlaylistEntry *pe = [playlistController currentEntry];
|
|
|
|
BOOL hideItem = NO;
|
|
|
|
if([[notification name] isEqualToString:CogPlaybackDidStopNotificiation] || !pe || ![pe artist] || [[pe artist] isEqualToString:@""])
|
|
hideItem = YES;
|
|
|
|
if(hideItem && [dockMenu indexOfItem:currentArtistItem] == 0) {
|
|
[dockMenu removeItem:currentArtistItem];
|
|
} else if(!hideItem && [dockMenu indexOfItem:currentArtistItem] < 0) {
|
|
[dockMenu insertItem:currentArtistItem atIndex:0];
|
|
}
|
|
}
|
|
|
|
- (BOOL)pathSuggesterEmpty {
|
|
return [playlistController pathSuggesterEmpty];
|
|
}
|
|
|
|
+ (BOOL)globalPathSuggesterEmpty {
|
|
return [kAppController pathSuggesterEmpty];
|
|
}
|
|
|
|
- (void)showPathSuggester {
|
|
[preferencesController showPathSuggester:self];
|
|
}
|
|
|
|
+ (void)globalShowPathSuggester {
|
|
[kAppController showPathSuggester];
|
|
}
|
|
|
|
- (void)showRubberbandSettings:(id)sender {
|
|
[preferencesController showRubberbandSettings:sender];
|
|
}
|
|
|
|
+ (void)globalShowRubberbandSettings {
|
|
[kAppController showRubberbandSettings:kAppController];
|
|
}
|
|
|
|
@end
|