Cog/Application/AppController.m
Christopher Snowhill b889586708 Playback: Implement Selection Follows Playback
Option to make selection follow the playback, within the lag of the
output buffer, including if Always Stop After Current or Repeat One is
enabled. Allows easily queueing up a list of tracks in Always Stop mode,
then hitting the Play button again to play the next track. Enabled by
default, but always optional to disable.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 18:37:31 -08:00

866 lines
30 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 "FeedbackController.h"
@import Sentry;
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"];
}
static BOOL consentLastEnabled = NO;
- (void)awakeFromNib {
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"sentryConsented": @(NO),
@"sentryAskedConsent": @(NO) }];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.sentryConsented" 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.sentryConsented"]) {
BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"sentryConsented"];
if(enabled != consentLastEnabled) {
if(enabled) {
[SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
options.dsn = @"https://b5eda1c2390eb965a74dd735413b6392@cog-analytics.losno.co/3";
options.debug = YES; // Enabled debug when first installing is always helpful
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
options.tracesSampleRate = @1.0;
options.profilesSampleRate = @1.0;
// Adds IP for users.
// For more information, visit: https://docs.sentry.io/platforms/apple/data-management/data-collected/
options.sendDefaultPii = YES;
// And now to set up user feedback prompting
options.onCrashedLastRun = ^void(SentryEvent * _Nonnull event) {
// capture user feedback
FeedbackController *fbcon = [[FeedbackController alloc] init];
[fbcon performSelectorOnMainThread:@selector(showWindow:) withObject:nil waitUntilDone:YES];
if([fbcon waitForCompletion]) {
SentryUserFeedback *userFeedback = [[SentryUserFeedback alloc] initWithEventId:[event eventId]];
userFeedback.comments = [fbcon comments];
userFeedback.email = [fbcon email];
userFeedback.name = [fbcon name];
[SentrySDK captureUserFeedback:userFeedback];
}
};
}];
} else {
if([SentrySDK isEnabled]) {
[SentrySDK close];
}
}
consentLastEnabled = 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"];
[userDefaultsValuesDict setObject:@NO forKey:@"alwaysStopAfterCurrent"];
[userDefaultsValuesDict setObject:@YES forKey:@"selectionFollowsPlayback"];
// 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];
}
- (void)selectTrack:(id)sender {
PlaylistEntry *pe = (PlaylistEntry *)sender;
@try {
[playlistView selectRowIndexes:[NSIndexSet indexSetWithIndex:pe.index] byExtendingSelection:NO];
}
@catch(id anException) {
}
}
@end