// // PlaylistController.m // Cog // // Created by Vincent Spader on 3/18/05. // Copyright 2005 Vincent Spader All rights reserved. // #import "PlaylistController.h" #import "PlaybackController.h" #import "PlaylistEntry.h" #import "PlaylistLoader.h" #import "RepeatTransformers.h" #import "Shuffle.h" #import "ShuffleTransformers.h" #import "SpotlightWindowController.h" #import "StatusImageTransformer.h" #import "ToggleQueueTitleTransformer.h" #import "NSString+CogSort.h" #import "Logging.h" #import "Cog-Swift.h" #import "AppController.h" #import "SandboxBroker.h" #define UNDO_STACK_LIMIT 0 extern BOOL kAppControllerShuttingDown; NSPersistentContainer *kPersistentContainer = nil; @implementation PlaylistController @synthesize currentEntry; @synthesize totalTime; @synthesize currentStatus; static NSArray *cellIdentifiers = nil; NSMutableDictionary *kArtworkDictionary = nil; static void *playlistControllerContext = &playlistControllerContext; + (void)initialize { cellIdentifiers = @[@"index", @"status", @"title", @"albumartist", @"artist", @"album", @"length", @"year", @"genre", @"track", @"path", @"filename", @"codec", @"rating", @"samplerate", @"bitspersample"]; NSValueTransformer *repeatNoneTransformer = [[RepeatModeTransformer alloc] initWithMode:RepeatModeNoRepeat]; [NSValueTransformer setValueTransformer:repeatNoneTransformer forName:@"RepeatNoneTransformer"]; NSValueTransformer *repeatOneTransformer = [[RepeatModeTransformer alloc] initWithMode:RepeatModeRepeatOne]; [NSValueTransformer setValueTransformer:repeatOneTransformer forName:@"RepeatOneTransformer"]; NSValueTransformer *repeatAlbumTransformer = [[RepeatModeTransformer alloc] initWithMode:RepeatModeRepeatAlbum]; [NSValueTransformer setValueTransformer:repeatAlbumTransformer forName:@"RepeatAlbumTransformer"]; NSValueTransformer *repeatAllTransformer = [[RepeatModeTransformer alloc] initWithMode:RepeatModeRepeatAll]; [NSValueTransformer setValueTransformer:repeatAllTransformer forName:@"RepeatAllTransformer"]; NSValueTransformer *repeatModeImageTransformer = [[RepeatModeImageTransformer alloc] init]; [NSValueTransformer setValueTransformer:repeatModeImageTransformer forName:@"RepeatModeImageTransformer"]; NSValueTransformer *shuffleOffTransformer = [[ShuffleModeTransformer alloc] initWithMode:ShuffleOff]; [NSValueTransformer setValueTransformer:shuffleOffTransformer forName:@"ShuffleOffTransformer"]; NSValueTransformer *shuffleAlbumsTransformer = [[ShuffleModeTransformer alloc] initWithMode:ShuffleAlbums]; [NSValueTransformer setValueTransformer:shuffleAlbumsTransformer forName:@"ShuffleAlbumsTransformer"]; NSValueTransformer *shuffleAllTransformer = [[ShuffleModeTransformer alloc] initWithMode:ShuffleAll]; [NSValueTransformer setValueTransformer:shuffleAllTransformer forName:@"ShuffleAllTransformer"]; NSValueTransformer *shuffleImageTransformer = [[ShuffleImageTransformer alloc] init]; [NSValueTransformer setValueTransformer:shuffleImageTransformer forName:@"ShuffleImageTransformer"]; NSValueTransformer *statusImageTransformer = [[StatusImageTransformer alloc] init]; [NSValueTransformer setValueTransformer:statusImageTransformer forName:@"StatusImageTransformer"]; NSValueTransformer *toggleQueueTitleTransformer = [[ToggleQueueTitleTransformer alloc] init]; [NSValueTransformer setValueTransformer:toggleQueueTitleTransformer forName:@"ToggleQueueTitleTransformer"]; } - (void)initDefaults { NSDictionary *defaultsDictionary = @{ @"repeat": @(RepeatModeNoRepeat), @"shuffle": @(ShuffleOff) }; [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary]; } - (id)initWithCoder:(NSCoder *)decoder { self = [super initWithCoder:decoder]; if(!self) return nil; shuffleList = [[NSMutableArray alloc] init]; queueList = [[NSMutableArray alloc] init]; undoManager = [[NSUndoManager alloc] init]; [undoManager setLevelsOfUndo:UNDO_STACK_LIMIT]; [self initDefaults]; _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"DataModel"]; [self.persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *description, NSError *error) { if(error != nil) { ALog(@"Failed to load Core Data stack: %@", error); abort(); } }]; kPersistentContainer = self.persistentContainer; self.persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; _persistentArtStorage = [[NSMutableDictionary alloc] init]; kArtworkDictionary = self.persistentArtStorage; return self; } + (NSPersistentContainer *)sharedPersistentContainer { return kPersistentContainer; } - (void)awakeFromNib { [super awakeFromNib]; statusImageTransformer = [NSValueTransformer valueTransformerForName:@"StatusImageTransformer"]; numberHertzToStringTransformer = [NSValueTransformer valueTransformerForName:@"NumberHertzToStringTransformer"]; NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES]; [self.tableView setSortDescriptors:@[sortDescriptor]]; [self addObserver:self forKeyPath:@"arrangedObjects" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:playlistControllerContext]; [playbackController addObserver:self forKeyPath:@"progressOverall" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld) context:playlistControllerContext]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fontSize" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:playlistControllerContext]; observersRegistered = YES; [self.tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO]; } - (void)deinit { if(observersRegistered) { [self removeObserver:self forKeyPath:@"arrangedObjects" context:playlistControllerContext]; [playbackController removeObserver:self forKeyPath:@"progressOverall" context:playlistControllerContext]; [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.fontSize" context:playlistControllerContext]; } } - (void)startObservingProgress:(NSProgress *)progress { [progress addObserver:self forKeyPath:@"localizedDescription" options:0 context:playlistControllerContext]; [progress addObserver:self forKeyPath:@"fractionCompleted" options:0 context:playlistControllerContext]; } - (void)stopObservingProgress:(NSProgress *)progress { [progress removeObserver:self forKeyPath:@"localizedDescription" context:playlistControllerContext]; [progress removeObserver:self forKeyPath:@"fractionCompleted" context:playlistControllerContext]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if(context == playlistControllerContext) { if([keyPath isEqualToString:@"arrangedObjects"]) { [self updatePlaylistIndexes]; [self updateTotalTime]; [self.tableView reloadData]; } else if([keyPath isEqualToString:@"values.fontSize"]) { [self updateRowSize]; } else if([keyPath isEqualToString:@"progressOverall"]) { id objNew = [change objectForKey:NSKeyValueChangeNewKey]; id objOld = [change objectForKey:NSKeyValueChangeOldKey]; NSProgress *progressNew = nil, *progressOld = nil; if(objNew && [objNew isKindOfClass:[NSProgress class]]) progressNew = (NSProgress *)objNew; if(objOld && [objOld isKindOfClass:[NSProgress class]]) progressOld = (NSProgress *)objOld; if(progressOld) { [self stopObservingProgress:progressOld]; } if(progressNew) { [self startObservingProgress:progressNew]; } [self updateProgressText]; } else if([keyPath isEqualToString:@"localizedDescription"] || [keyPath isEqualToString:@"fractionCompleted"]) { [self updateProgressText]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)updateProgressText { NSString *description = nil; if(playbackController.progressOverall) { if(playbackController.progressJob) { description = [NSString stringWithFormat:@"%@ - %@", playbackController.progressOverall.localizedDescription, playbackController.progressJob.localizedDescription]; } else { description = playbackController.progressOverall.localizedDescription; } } if(description) { [self setTotalTime:nil]; description = [description stringByAppendingFormat:@" - %.2f%% complete", playbackController.progressOverall.fractionCompleted * 100.0]; [self setCurrentStatus:description]; } else { [self setCurrentStatus:nil]; [self updateTotalTime]; } } - (void)commitPersistentStore { [self.persistentContainer.viewContext performBlockAndWait:^{ NSError *error = nil; [self.persistentContainer.viewContext save:&error]; if(error) { ALog(@"Error committing playlist storage: %@", [error localizedDescription]); } }]; } - (void)updatePlayCountForTrack:(PlaylistEntry *)pe { if(pe.countAdded) return; pe.countAdded = YES; __block PlayCount *pc = pe.playCountItem; if(pc) { [self.persistentContainer.viewContext performBlockAndWait:^{ pc.count += 1; pc.lastPlayed = [NSDate date]; }]; } else { [self.persistentContainer.viewContext performBlockAndWait:^{ pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext]; pc.count = 1; pc.firstSeen = pc.lastPlayed = [NSDate date]; pc.album = pe.album; pc.artist = pe.artist; pc.title = pe.title; pc.filename = pe.filenameFragment; }]; } [self commitPersistentStore]; } - (void)firstSawTrack:(PlaylistEntry *)pe { __block PlayCount *pc = pe.playCountItem; if(!pc) { [self.persistentContainer.viewContext performBlockAndWait:^{ pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext]; pc.count = 0; pc.firstSeen = [NSDate date]; pc.album = pe.album; pc.artist = pe.artist; pc.title = pe.title; pc.filename = pe.filenameFragment; }]; } } - (void)ratingUpdatedWithEntry:(PlaylistEntry *)pe rating:(CGFloat)rating { if(pe && !pe.deLeted) { __block PlayCount *pc = pe.playCountItem; if(!pc) { [self.persistentContainer.viewContext performBlockAndWait:^{ pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext]; pc.count = 0; pc.firstSeen = [NSDate date]; pc.album = pe.album; pc.artist = pe.artist; pc.title = pe.title; pc.filename = pe.filenameFragment; }]; } [self.persistentContainer.viewContext performBlockAndWait:^{ pc.rating = rating; }]; [self commitPersistentStore]; } } - (void)resetPlayCountForTrack:(PlaylistEntry *)pe { PlayCount *pc = pe.playCountItem; if(pc) { [self.persistentContainer.viewContext performBlockAndWait:^{ pc.count = 0; }]; } } - (void)removeRatingForTrack:(PlaylistEntry *)pe { PlayCount *pc = pe.playCountItem; if(pc) { [self.persistentContainer.viewContext performBlockAndWait:^{ pc.rating = 0; }]; } } - (void)updatePlaylistIndexes { NSArray *arranged = [self arrangedObjects]; NSUInteger n = [arranged count]; BOOL updated = NO; for(NSUInteger i = 0; i < n; i++) { PlaylistEntry *pe = arranged[i]; if(pe.index != i) { // Make sure we don't get into some kind of crazy observing loop... pe.index = i; updated = YES; } } if(updated) { [self commitPersistentStore]; } } - (void)updateTotalTime { double tt = 0; ldiv_t hoursAndMinutes; ldiv_t daysAndHours; ldiv_t weeksAndDays; for(PlaylistEntry *pe in [self arrangedObjects]) { if(!isnan([pe.length doubleValue])) tt += [pe.length doubleValue]; } long sec = (long)(tt); hoursAndMinutes = ldiv(sec / 60, 60); NSString *week = NSLocalizedString(@"%1d week(s)", @"week for total"); NSString *day = NSLocalizedString(@"%1d day(s)", @"day for total"); NSString *hour = NSLocalizedString(@"%1d hour(s)", @"hour for total"); NSString *min = NSLocalizedString(@"%1d minute(s)", @"minute for total"); NSString *second = NSLocalizedString(@"%1d second(s)", @"second for total"); if(hoursAndMinutes.quot >= 24) { daysAndHours = ldiv(hoursAndMinutes.quot, 24); if(daysAndHours.quot >= 7) { weeksAndDays = ldiv(daysAndHours.quot, 7); NSString *weekCount = [NSString localizedStringWithFormat:week, weeksAndDays.quot]; NSString *dayCount = [NSString localizedStringWithFormat:day, weeksAndDays.rem]; NSString *hourCount = [NSString localizedStringWithFormat:hour, daysAndHours.rem]; NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem]; NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60]; [self setTotalTime:[NSString stringWithFormat: NSLocalizedString(@"wdhms", "weeks, days, hours, minutes and seconds"), weekCount, dayCount, hourCount, minuteCount, secCount]]; } else { NSString *dayCount = [NSString localizedStringWithFormat:day, daysAndHours.quot]; NSString *hourCount = [NSString localizedStringWithFormat:hour, daysAndHours.rem]; NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem]; NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60]; [self setTotalTime:[NSString stringWithFormat: NSLocalizedString(@"dhms", "days, hours, minutes and seconds"), dayCount, hourCount, minuteCount, secCount]]; } } else { if(hoursAndMinutes.quot > 0) { NSString *hourCount = [NSString localizedStringWithFormat:hour, hoursAndMinutes.quot]; NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem]; NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60]; [self setTotalTime:[NSString stringWithFormat: NSLocalizedString(@"hms", "hours, minutes and seconds"), hourCount, minuteCount, secCount]]; } else { if(hoursAndMinutes.rem > 0) { NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem]; NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60]; [self setTotalTime:[NSString stringWithFormat: NSLocalizedString(@"ms", "minutes and seconds"), minuteCount, secCount]]; } else { NSString *secCount = [NSString localizedStringWithFormat:second, sec]; [self setTotalTime:secCount]; } } } } - (NSView *_Nullable)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *_Nullable)tableColumn row:(NSInteger)row { NSImage *cellImage = nil; NSString *cellText = @""; NSString *cellIdentifier = @""; NSTextAlignment cellTextAlignment = NSTextAlignmentLeft; PlaylistEntry *pe = [[self arrangedObjects] objectAtIndex:row]; float fontSize = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] floatForKey:@"fontSize"]; BOOL cellRating = NO; if(pe) { cellIdentifier = [tableColumn identifier]; NSUInteger index = [cellIdentifiers indexOfObject:cellIdentifier]; switch(index) { case 0: cellText = [NSString stringWithFormat:@"%lld", pe.index + 1]; cellTextAlignment = NSTextAlignmentRight; break; case 1: cellImage = [statusImageTransformer transformedValue:pe.status]; break; case 2: if([pe title]) cellText = pe.title; break; case 3: if([pe albumartist]) cellText = pe.albumartist; break; case 4: if([pe artist]) cellText = pe.artist; break; case 5: if([pe album]) cellText = pe.album; break; case 6: cellText = pe.lengthText; cellTextAlignment = NSTextAlignmentRight; break; case 7: if([pe year]) cellText = pe.yearText; cellTextAlignment = NSTextAlignmentRight; break; case 8: if([pe genre]) cellText = pe.genre; break; case 9: if([pe track]) cellText = pe.trackText; cellTextAlignment = NSTextAlignmentRight; break; case 10: if([pe path]) cellText = pe.path; break; case 11: if([pe filename]) cellText = pe.filename; break; case 12: if([pe codec]) cellText = pe.codec; break; case 13: { NSString *filledStar = @"★"; NSString *emptyStar = @"☆"; NSUInteger rating = (NSUInteger)ceil(pe.rating); if(rating < 0) rating = 0; else if(rating > 5) rating = 5; cellText = [@"" stringByPaddingToLength:rating withString:filledStar startingAtIndex:0]; cellText = [cellText stringByPaddingToLength:5 withString:emptyStar startingAtIndex:0]; cellRating = YES; break; } case 14: cellText = [numberHertzToStringTransformer transformedValue:@(pe.sampleRate)]; cellTextAlignment = NSTextAlignmentRight; break; case 15: cellText = [NSString stringWithFormat:@"%u", pe.bitsPerSample]; cellTextAlignment = NSTextAlignmentRight; break; } } NSString *cellTextTruncated = cellText; if([cellTextTruncated length] > 1024) { cellTextTruncated = [cellTextTruncated substringToIndex:1023]; cellTextTruncated = [cellTextTruncated stringByAppendingString:@"…"]; } NSView *view = [tableView makeViewWithIdentifier:cellIdentifier owner:nil]; if(view && [view isKindOfClass:[NSTableCellView class]]) { NSTableCellView *cellView = (NSTableCellView *)view; NSRect frameRect = cellView.frame; frameRect.origin.y = 1; frameRect.size.height = tableView.rowHeight; cellView.frame = frameRect; if(cellView.textField) { cellView.textField.allowsDefaultTighteningForTruncation = YES; NSFont *font = [NSFont monospacedDigitSystemFontOfSize:fontSize weight:NSFontWeightRegular]; cellView.textField.font = font; cellView.textField.stringValue = cellTextTruncated ?: @""; cellView.textField.alignment = cellTextAlignment; if(cellView.textField.intrinsicContentSize.width > cellView.textField.frame.size.width - 4) cellView.textField.toolTip = cellTextTruncated ?: @""; else cellView.textField.toolTip = [pe statusMessage]; NSRect cellFrameRect = cellView.textField.frame; cellFrameRect.origin.y = 1; cellFrameRect.size.height = frameRect.size.height; cellView.textField.frame = cellFrameRect; } if(cellView.imageView) { cellView.imageView.image = cellImage; cellView.imageView.toolTip = [pe statusMessage]; NSRect cellFrameRect = cellView.imageView.frame; cellFrameRect.size.height = frameRect.size.height * 14.0 / 18.0; cellFrameRect.origin.y = (frameRect.size.height - cellFrameRect.size.height) * 0.5; cellView.imageView.frame = cellFrameRect; } cellView.rowSizeStyle = NSTableViewRowSizeStyleCustom; } return view; } - (void)tableView:(NSTableView *)view didClickRow:(NSInteger)clickedRow column:(NSInteger)clickedColumn atPoint:(NSPoint)cellPoint { NSTableColumn *column = [view tableColumns][clickedColumn]; NSString *cellIdentifier = [column identifier]; NSUInteger index = [cellIdentifiers indexOfObject:cellIdentifier]; if(index == 13) { NSInteger rating = ((CGFloat)ceil(cellPoint.x * 5.0 / 64.0)); if(rating < 1) rating = 1; else if(rating > 5) rating = 5; PlaylistEntry *pe = [[self arrangedObjects] objectAtIndex:clickedRow]; [self ratingUpdatedWithEntry:pe rating:rating]; NSIndexSet *refreshRow = [NSIndexSet indexSetWithIndex:clickedRow]; NSIndexSet *refreshColumn = [NSIndexSet indexSetWithIndex:clickedColumn]; [self.tableView reloadDataForRowIndexes:refreshRow columnIndexes:refreshColumn]; } } - (void)updateRowSize { [self.tableView reloadData]; if(currentEntry != nil) [self.tableView scrollRowToVisible:currentEntry.index]; } - (void)updateNextAfterDeleted:(PlaylistEntry *)lastEntry withDeleteIndexes:(NSIndexSet *)indexes { __block PlaylistEntry *pe = nil; NSArray *allObjects = [self arrangedObjects]; [indexes enumerateRangesUsingBlock:^(NSRange range, BOOL *_Nonnull stop) { if(range.location <= lastEntry.index && range.location + range.length > lastEntry.index) { NSUInteger index = range.location + range.length; if(index < [allObjects count]) pe = [allObjects objectAtIndex:index]; else pe = nil; } else if(pe && range.location <= [pe index] && range.location + range.length > [pe index]) { NSUInteger index = range.location + range.length; if(index < [allObjects count]) pe = [allObjects objectAtIndex:index]; else pe = nil; } else if(pe && range.location > [pe index]) { *stop = YES; } }]; nextEntryAfterDeleted = pe; } - (void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn { if([self shuffle] != ShuffleOff) [self resetShuffleList]; NSUInteger index = [cellIdentifiers indexOfObject:[tableColumn identifier]]; NSSortDescriptor *sortDescriptor;/* = [tableColumn sortDescriptorPrototype];*/ NSArray *sortDescriptors;/* = sortDescriptor ? @[sortDescriptor] : @[];*/ BOOL ascending; NSImage *indicatorImage = [tableView indicatorImageInTableColumn:tableColumn]; NSImage *sortAscending = [NSImage imageNamed:@"NSAscendingSortIndicator"]; NSImage *sortDescending = [NSImage imageNamed:@"NSDescendingSortIndicator"]; if(indicatorImage == sortAscending) { [tableView setIndicatorImage:sortDescending inTableColumn:tableColumn]; ascending = NO; } else { [tableView setIndicatorImage:sortAscending inTableColumn:tableColumn]; ascending = YES; } switch(index) { default: case 0: sortDescriptors = @[]; break; case 1: case 2: case 3: case 4: case 5: case 8: case 10: case 11: case 12: sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[tableColumn identifier] ascending:ascending selector:@selector(caseInsensitiveCompare:)]; sortDescriptors = @[sortDescriptor]; break; case 6: case 7: sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[tableColumn identifier] ascending:ascending selector:@selector(compareTrackNumbers:)]; sortDescriptors = @[sortDescriptor]; break; case 9: { // Unfortunately, this makes the column header bug out. No way around it. sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"albumartist" ascending:ascending selector:@selector(caseInsensitiveCompare:)], [[NSSortDescriptor alloc] initWithKey:@"album" ascending:ascending selector:@selector(caseInsensitiveCompare:)], [[NSSortDescriptor alloc] initWithKey:@"disc" // Yes, this, even though it's not actually a column ascending:ascending selector:@selector(compareTrackNumbers:)], [[NSSortDescriptor alloc] initWithKey:@"track" ascending:ascending selector:@selector(compareTrackNumbers:)] ]; } } [self setSortDescriptors:sortDescriptors]; } // This action is only needed to revert the one that follows it - (void)moveObjectsFromIndex:(NSUInteger)fromIndex toArrangedObjectIndexes:(NSIndexSet *)indexSet { [[[self undoManager] prepareWithInvocationTarget:self] moveObjectsInArrangedObjectsFromIndexes:indexSet toIndex:fromIndex]; NSString *actionName = [NSString stringWithFormat:@"Reordering %lu entries", (unsigned long)[indexSet count]]; [[self undoManager] setActionName:actionName]; [super moveObjectsFromIndex:fromIndex toArrangedObjectIndexes:indexSet]; [playbackController playlistDidChange:self]; } - (void)moveObjectsInArrangedObjectsFromIndexes:(NSIndexSet *)indexSet toIndex:(NSUInteger)insertIndex { [[[self undoManager] prepareWithInvocationTarget:self] moveObjectsFromIndex:insertIndex toArrangedObjectIndexes:indexSet]; NSString *actionName = [NSString stringWithFormat:@"Reordering %lu entries", (unsigned long)[indexSet count]]; [[self undoManager] setActionName:actionName]; [super moveObjectsInArrangedObjectsFromIndexes:indexSet toIndex:insertIndex]; [playbackController playlistDidChange:self]; } - (id)tableView:(NSTableView *)tableView pasteboardWriterForRow:(NSInteger)row { NSPasteboardItem *item = (NSPasteboardItem *)[super tableView:tableView pasteboardWriterForRow:row]; if(!item) { item = [[NSPasteboardItem alloc] init]; } PlaylistEntry *song = [[self arrangedObjects] objectAtIndex:row]; if(song.url != nil) { [item setData:[song.url dataRepresentation] forType:NSPasteboardTypeFileURL]; } return item; } - (BOOL)tableView:(NSTableView *)tv acceptDrop:(id)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)op { // Check if DNDArrayController handles it. if([super tableView:tv acceptDrop:info row:row dropOperation:op]) return YES; if(row < 0) row = 0; // Determine the type of object that was dropped NSArray *supportedTypes = @[CogUrlsPboardType, NSPasteboardTypeFileURL, iTunesDropType]; NSPasteboard *pboard = [info draggingPasteboard]; NSString *bestType = [pboard availableTypeFromArray:supportedTypes]; NSMutableArray *acceptedURLs = [[NSMutableArray alloc] init]; // Get files from an file drawer drop if([bestType isEqualToString:CogUrlsPboardType]) { NSError *error; NSData *data = [pboard dataForType:CogUrlsPboardType]; NSArray *urls; if(@available(macOS 11.0, *)) { urls = [NSKeyedUnarchiver unarchivedArrayOfObjectsOfClass:[NSURL class] fromData:data error:&error]; } else { NSSet *allowed = [NSSet setWithArray:@[[NSArray class], [NSURL class]]]; urls = [NSKeyedUnarchiver unarchivedObjectOfClasses:allowed fromData:data error:&error]; } if(!urls) { DLog(@"%@", error); } else { DLog(@"URLS: %@", urls); } //[playlistLoader insertURLs: urls atIndex:row sort:YES]; [acceptedURLs addObjectsFromArray:urls]; } // Get files from a normal file drop (such as from Finder) if([bestType isEqualToString:NSPasteboardTypeFileURL]) { NSArray *classes = @[[NSURL class]]; NSDictionary *options = @{}; NSArray *files = [pboard readObjectsForClasses:classes options:options]; //[playlistLoader insertURLs:urls atIndex:row sort:YES]; [acceptedURLs addObjectsFromArray:files]; } // Get files from an iTunes drop if([bestType isEqualToString:iTunesDropType]) { NSDictionary *iTunesDict = [pboard propertyListForType:iTunesDropType]; NSDictionary *tracks = [iTunesDict valueForKey:@"Tracks"]; // Convert the iTunes URLs to URLs....MWAHAHAH! NSMutableArray *urls = [[NSMutableArray alloc] init]; for(NSDictionary *trackInfo in [tracks allValues]) { [urls addObject:[NSURL URLWithString:[trackInfo valueForKey:@"Location"]]]; } //[playlistLoader insertURLs:urls atIndex:row sort:YES]; [acceptedURLs addObjectsFromArray:urls]; } if([acceptedURLs count]) { if(![[self content] count]) { row = 0; } NSDictionary *loadEntriesData = @{ @"entries": acceptedURLs, @"index": @(row), @"sort": @(YES), @"origin": @(URLOriginInternal) }; [self performSelectorInBackground:@selector(insertURLsInBackground:) withObject:loadEntriesData]; } return YES; } - (NSUndoManager *)undoManager { return undoManager; } - (NSIndexSet *)disarrangeIndexes:(NSIndexSet *)indexes { if([[self arrangedObjects] count] <= [indexes lastIndex]) return indexes; NSMutableIndexSet *disarrangedIndexes = [[NSMutableIndexSet alloc] init]; [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) { [disarrangedIndexes addIndex:[[self content] indexOfObject:[[self arrangedObjects] objectAtIndex:idx]]]; }]; return disarrangedIndexes; } - (NSArray *)disarrangeObjects:(NSArray *)objects { NSMutableArray *disarrangedObjects = [[NSMutableArray alloc] init]; for(PlaylistEntry *pe in [self content]) { if([objects containsObject:pe]) [disarrangedObjects addObject:pe]; } return disarrangedObjects; } - (NSIndexSet *)rearrangeIndexes:(NSIndexSet *)indexes { if([[self content] count] <= [indexes lastIndex]) return indexes; NSMutableIndexSet *rearrangedIndexes = [[NSMutableIndexSet alloc] init]; [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) { [rearrangedIndexes addIndex:[[self arrangedObjects] indexOfObject:[[self content] objectAtIndex:idx]]]; }]; return rearrangedIndexes; } - (void)insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes { [self insertObjects:objects atArrangedObjectIndexes:indexes]; [self rearrangeObjects]; } - (void)untrashObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes { [self untrashObjects:objects atArrangedObjectIndexes:indexes]; [self rearrangeObjects]; } - (void)insertObjectsUnsynced:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes { [super insertObjects:objects atArrangedObjectIndexes:indexes]; [self rearrangeObjects]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; } - (void)insertObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes { // Special case, somehow someone is undoing while a search filter is in place __block NSUInteger index = NSNotFound; [indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { if(range.location < index) { index = range.location; } }]; NSUInteger count = [[self content] count]; if(index > count) { // Ah, oops, bodge fix __block NSMutableIndexSet *replacementIndexes = [[NSMutableIndexSet alloc] init]; [indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { [replacementIndexes addIndexesInRange:NSMakeRange(range.location - index + count, range.length)]; }]; indexes = replacementIndexes; } [[[self undoManager] prepareWithInvocationTarget:self] removeObjectsAtIndexes:[self disarrangeIndexes:indexes]]; NSString *actionName = [NSString stringWithFormat:@"Adding %lu entries", (unsigned long)[objects count]]; [[self undoManager] setActionName:actionName]; for(PlaylistEntry *pe in objects) { pe.deLeted = NO; } // further bodge fix @try { [super insertObjects:objects atArrangedObjectIndexes:indexes]; } @catch(id anException) { indexes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(count, [objects count])]; [super insertObjects:objects atArrangedObjectIndexes:indexes]; } [self commitPersistentStore]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; } - (void)untrashObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes { [[[self undoManager] prepareWithInvocationTarget:self] trashObjectsAtIndexes:[self disarrangeIndexes:indexes]]; NSString *actionName = [NSString stringWithFormat:@"Restoring %lu entries from trash", (unsigned long)[objects count]]; [[self undoManager] setActionName:actionName]; for(PlaylistEntry *pe in objects) { if(pe.deLeted && pe.trashUrl) { NSError *error = nil; [[NSFileManager defaultManager] moveItemAtURL:pe.trashUrl toURL:pe.url error:&error]; } pe.deLeted = NO; pe.trashUrl = nil; } [super insertObjects:objects atArrangedObjectIndexes:indexes]; [self commitPersistentStore]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; } - (void)removeObjectsAtIndexes:(NSIndexSet *)indexes { [self removeObjectsAtArrangedObjectIndexes:[self rearrangeIndexes:indexes]]; } - (void)trashObjectsAtIndexes:(NSIndexSet *)indexes { [self trashObjectsAtArrangedObjectIndexes:[self rearrangeIndexes:indexes]]; } - (void)removeObjectsAtArrangedObjectIndexes:(NSIndexSet *)indexes { NSArray *objects = [[self arrangedObjects] objectsAtIndexes:indexes]; [[[self undoManager] prepareWithInvocationTarget:self] insertObjects:[self disarrangeObjects:objects] atIndexes:[self disarrangeIndexes:indexes]]; NSString *actionName = [NSString stringWithFormat:@"Removing %lu entries", (unsigned long)[indexes count]]; [[self undoManager] setActionName:actionName]; DLog(@"Removing indexes: %@", indexes); DLog(@"Current index: %lli", currentEntry.index); NSMutableIndexSet *unarrangedIndexes = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *pe in objects) { [unarrangedIndexes addIndex:[pe index]]; pe.deLeted = YES; } if([indexes containsIndex:currentEntry.index]) { [self updateNextAfterDeleted:currentEntry withDeleteIndexes:indexes]; } else if(nextEntryAfterDeleted && [indexes containsIndex:nextEntryAfterDeleted.index]) { [self updateNextAfterDeleted:nextEntryAfterDeleted withDeleteIndexes:indexes]; } if(currentEntry.index >= 0 && [unarrangedIndexes containsIndex:currentEntry.index]) { currentEntry.index = -currentEntry.index - 1; DLog(@"Current removed: %lli", currentEntry.index); } if(currentEntry.index < 0) // Need to update the negative index { NSInteger i = -currentEntry.index - 1; DLog(@"I is %li", i); NSInteger j; for(j = i - 1; j >= 0; j--) { if([unarrangedIndexes containsIndex:j]) { DLog(@"Removing 1"); i--; } } currentEntry.index = -i - 1; } [super removeObjectsAtArrangedObjectIndexes:indexes]; [self commitPersistentStore]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; [playbackController playlistDidChange:self]; } - (void)trashObjectsAtArrangedObjectIndexes:(NSIndexSet *)indexes { NSArray *objects = [[self arrangedObjects] objectsAtIndexes:indexes]; [[[self undoManager] prepareWithInvocationTarget:self] untrashObjects:[self disarrangeObjects:objects] atIndexes:[self disarrangeIndexes:indexes]]; NSString *actionName = [NSString stringWithFormat:@"Trashing %lu entries", (unsigned long)[indexes count]]; [[self undoManager] setActionName:actionName]; DLog(@"Trashing indexes: %@", indexes); DLog(@"Current index: %lli", currentEntry.index); NSMutableIndexSet *unarrangedIndexes = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *pe in objects) { [unarrangedIndexes addIndex:[pe index]]; pe.deLeted = YES; } if([indexes containsIndex:currentEntry.index]) { [self updateNextAfterDeleted:currentEntry withDeleteIndexes:indexes]; if(nextEntryAfterDeleted) { [playbackController playEntry:nextEntryAfterDeleted]; nextEntryAfterDeleted = nil; } else { [playbackController stop:nil]; } } [super removeObjectsAtArrangedObjectIndexes:indexes]; [self commitPersistentStore]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; [playbackController playlistDidChange:self]; for(PlaylistEntry *pe in objects) { if([pe.url isFileURL]) { NSURL *removed = nil; NSError *error = nil; [[NSFileManager defaultManager] trashItemAtURL:pe.url resultingItemURL:&removed error:&error]; pe.trashUrl = removed; } } } - (void)setSortDescriptors:(NSArray *)sortDescriptors { DLog(@"Current: %@, setting: %@", [self sortDescriptors], sortDescriptors); // Cheap hack so the index column isn't sorted if([sortDescriptors count] != 0) { if([[((NSSortDescriptor *)(sortDescriptors[0])) key] caseInsensitiveCompare:@"index"] == NSOrderedSame) { // Remove the sort descriptors [super setSortDescriptors:@[]]; [self rearrangeObjects]; return; } } [super setSortDescriptors:sortDescriptors]; [self rearrangeObjects]; [playbackController playlistDidChange:self]; } - (IBAction)randomizeList:(id)sender { [self setSortDescriptors:@[]]; NSArray *unrandomized = [self content]; [[[self undoManager] prepareWithInvocationTarget:self] unrandomizeList:unrandomized]; [self setContent:[Shuffle shuffleList:[self content]]]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; [[self undoManager] setActionName:NSLocalizedString(@"PlaylistRandomizationAction", @"")]; } - (void)unrandomizeList:(NSArray *)entries { [[[self undoManager] prepareWithInvocationTarget:self] randomizeList:self]; [self setContent:entries]; } - (IBAction)toggleShuffle:(id)sender { ShuffleMode shuffle = [self shuffle]; if(shuffle == ShuffleOff) { [self setShuffle:ShuffleAlbums]; } else if(shuffle == ShuffleAlbums) { [self setShuffle:ShuffleAll]; } else if(shuffle == ShuffleAll) { [self setShuffle:ShuffleOff]; } } - (IBAction)toggleRepeat:(id)sender { RepeatMode repeat = [self repeat]; if(repeat == RepeatModeNoRepeat) { [self setRepeat:RepeatModeRepeatOne]; } else if(repeat == RepeatModeRepeatOne) { [self setRepeat:RepeatModeRepeatAlbum]; } else if(repeat == RepeatModeRepeatAlbum) { [self setRepeat:RepeatModeRepeatAll]; } else if(repeat == RepeatModeRepeatAll) { [self setRepeat:RepeatModeNoRepeat]; } } - (PlaylistEntry *)entryAtIndex:(NSInteger)i { if([[self arrangedObjects] count] == 0) return nil; RepeatMode repeat = [self repeat]; if(i < 0 || i >= [[self arrangedObjects] count]) { if(repeat != RepeatModeRepeatAll) return nil; while(i < 0) i += [[self arrangedObjects] count]; if(i >= [[self arrangedObjects] count]) i %= [[self arrangedObjects] count]; } return [[self arrangedObjects] objectAtIndex:i]; } - (IBAction)remove:(id)sender { // It's a kind of magic. // Plain old NSArrayController's remove: isn't working properly for some reason. // The method is definitely called but (overridden) removeObjectsAtArrangedObjectIndexes: isn't // called and no entries are removed. Putting explicit call to // removeObjectsAtArrangedObjectIndexes: here for now. // TODO: figure it out NSIndexSet *selected = [self selectionIndexes]; if([selected count] > 0) { [self removeObjectsAtArrangedObjectIndexes:selected]; } } - (IBAction)trash:(id)sender { // Someone asked for this, so they're getting it. // Trash the selection, and advance playback to the next untrashed file if necessary. NSIndexSet *selected = [self selectionIndexes]; if([selected count] > 0) { [self trashObjectsAtArrangedObjectIndexes:selected]; } } - (IBAction)removeDuplicates:(id)sender { NSMutableArray *originals = [[NSMutableArray alloc] init]; NSMutableArray *duplicates = [[NSMutableArray alloc] init]; for(PlaylistEntry *pe in [self content]) { if([originals containsObject:pe.url]) [duplicates addObject:pe]; else [originals addObject:pe.url]; } if([duplicates count] > 0) { NSArray *arrangedContent = [self arrangedObjects]; NSMutableIndexSet *duplicatesIndex = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *pe in duplicates) { [duplicatesIndex addIndex:[arrangedContent indexOfObject:pe]]; } [self removeObjectsAtArrangedObjectIndexes:duplicatesIndex]; } } - (IBAction)removeDeadItems:(id)sender { NSMutableArray *deadItems = [[NSMutableArray alloc] init]; for(PlaylistEntry *pe in [self content]) { NSURL *url = pe.url; if([url isFileURL]) if(![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) [deadItems addObject:pe]; } if([deadItems count] > 0) { NSArray *arrangedContent = [self arrangedObjects]; NSMutableIndexSet *deadItemsIndex = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *pe in deadItems) { [deadItemsIndex addIndex:[arrangedContent indexOfObject:pe]]; } [self removeObjectsAtArrangedObjectIndexes:deadItemsIndex]; } } - (PlaylistEntry *)shuffledEntryAtIndex:(NSInteger)i { RepeatMode repeat = [self repeat]; while(i < 0) { if(repeat == RepeatModeRepeatAll) { [self addShuffledListToFront]; // change i appropriately i += [[self arrangedObjects] count]; } else { return nil; } } while(i >= [shuffleList count]) { if(repeat == RepeatModeRepeatAll) { [self addShuffledListToBack]; } else { return nil; } } return shuffleList[i]; } - (PlaylistEntry *)getNextEntry:(PlaylistEntry *)pe { return [self getNextEntry:pe ignoreRepeatOne:NO]; } - (PlaylistEntry *)getNextEntry:(PlaylistEntry *)pe ignoreRepeatOne:(BOOL)ignoreRepeatOne { if(!ignoreRepeatOne && [self repeat] == RepeatModeRepeatOne) { return pe; } if([queueList count] > 0) { pe = queueList[0]; [queueList removeObjectAtIndex:0]; pe.queued = NO; [pe setQueuePosition:-1]; int i; for(i = 0; i < [queueList count]; i++) { PlaylistEntry *queueItem = queueList[i]; [queueItem setQueuePosition:i]; } [self commitPersistentStore]; return pe; } if([self shuffle] != ShuffleOff) { return [self shuffledEntryAtIndex:(pe.shuffleIndex + 1)]; } else { NSInteger i; if(pe.deLeted) // Was a current entry, now removed. { if(nextEntryAfterDeleted) i = nextEntryAfterDeleted.index; else i = 0; nextEntryAfterDeleted = nil; } else { i = pe.index + 1; } if([self repeat] == RepeatModeRepeatAlbum) { PlaylistEntry *next = [self entryAtIndex:i]; if((i > [[self arrangedObjects] count] - 1) || ([[next album] caseInsensitiveCompare:[pe album]]) || ([next album] == nil)) { NSArray *filtered = [self filterPlaylistOnAlbum:[pe album]]; if([pe album] == nil || !filtered || [filtered count] < 1) i--; else i = [(PlaylistEntry *)filtered[0] index]; } } return [self entryAtIndex:i]; } } - (NSArray *)filterPlaylistOnAlbum:(NSString *)album { NSPredicate *predicate; if([album length] > 0) predicate = [NSPredicate predicateWithFormat:@"album == %@", album]; else predicate = [NSPredicate predicateWithFormat:@"album == nil || album == %@", @""]; return [[self arrangedObjects] filteredArrayUsingPredicate:predicate]; } - (PlaylistEntry *)getPrevEntry:(PlaylistEntry *)pe { return [self getPrevEntry:pe ignoreRepeatOne:NO]; } - (PlaylistEntry *)getPrevEntry:(PlaylistEntry *)pe ignoreRepeatOne:(BOOL)ignoreRepeatOne { if(!ignoreRepeatOne && [self repeat] == RepeatModeRepeatOne) { return pe; } if([self shuffle] != ShuffleOff) { return [self shuffledEntryAtIndex:(pe.shuffleIndex - 1)]; } else { NSInteger i; if(pe.index < 0) // Was a current entry, now removed. { i = -pe.index - 2; } else { i = pe.index - 1; } return [self entryAtIndex:i]; } } - (BOOL)next { PlaylistEntry *pe; pe = [self getNextEntry:[self currentEntry] ignoreRepeatOne:YES]; if(pe == nil) return NO; [self setCurrentEntry:pe]; return YES; } - (BOOL)prev { PlaylistEntry *pe; pe = [self getPrevEntry:[self currentEntry] ignoreRepeatOne:YES]; if(pe == nil) return NO; [self setCurrentEntry:pe]; return YES; } - (NSArray *)shuffleAlbums { NSArray *newList = [self arrangedObjects]; NSMutableArray *temp = [[NSMutableArray alloc] init]; NSSortDescriptor *sortDescriptorTrack = [[NSSortDescriptor alloc] initWithKey:@"track" ascending:YES]; NSSortDescriptor *sortDescriptorDisc = [[NSSortDescriptor alloc] initWithKey:@"disc" ascending:YES]; NSArray *albums = [newList valueForKey:@"album"]; albums = [[NSSet setWithArray:albums] allObjects]; NSArray *tempList = [Shuffle shuffleList:albums]; temp = [[NSMutableArray alloc] init]; BOOL blankAdded = NO; for(NSString *album in tempList) { NSString *theAlbum = album; if((id)album == [NSNull null]) { if(blankAdded) continue; else theAlbum = @""; blankAdded = YES; } else if([album isEqualToString:@""]) { if(blankAdded) continue; blankAdded = YES; } NSArray *albumContent = [self filterPlaylistOnAlbum:theAlbum]; NSArray *sortedContent = [albumContent sortedArrayUsingDescriptors:@[sortDescriptorDisc, sortDescriptorTrack]]; if(sortedContent && [sortedContent count]) [temp addObjectsFromArray:sortedContent]; } return temp; } - (void)readQueueFromDataStore { NSPredicate *hasUrlPredicate = [NSPredicate predicateWithFormat:@"urlString != nil && urlString != %@", @""]; NSPredicate *deletedPredicate = [NSPredicate predicateWithFormat:@"deLeted == NO || deLeted == nil"]; NSPredicate *queuedPredicate = [NSPredicate predicateWithFormat:@"queued == YES"]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"queuePosition" ascending:YES]; NSCompoundPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[deletedPredicate, hasUrlPredicate, queuedPredicate]]; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"]; request.predicate = predicate; request.sortDescriptors = @[sortDescriptor]; [self.persistentContainer.viewContext performBlockAndWait:^{ NSError *error = nil; NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error]; if(results && [results count] > 0) { [queueList removeAllObjects]; NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [results count])]; [queueList insertObjects:results atIndexes:indexSet]; } }]; } - (void)readShuffleListFromDataStore { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"deLeted == NO || deLeted == nil || urlString == nil"]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"shuffleIndex" ascending:YES]; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"]; request.predicate = predicate; request.sortDescriptors = @[sortDescriptor]; [self.persistentContainer.viewContext performBlockAndWait:^{ NSError *error = nil; NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error]; if(results && [results count] > 0) { [shuffleList removeAllObjects]; NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [results count])]; [shuffleList insertObjects:results atIndexes:indexSet]; } }]; } - (void)addShuffledListToFront { NSArray *newList; NSIndexSet *indexSet; if([self shuffle] == ShuffleAlbums) { newList = [self shuffleAlbums]; } else { newList = [Shuffle shuffleList:[self arrangedObjects]]; } indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [newList count])]; [shuffleList insertObjects:newList atIndexes:indexSet]; int i; for(i = 0; i < [shuffleList count]; i++) { [shuffleList[i] setShuffleIndex:i]; } [self commitPersistentStore]; } - (void)addShuffledListToBack { NSArray *newList; NSIndexSet *indexSet; if([self shuffle] == ShuffleAlbums) { newList = [self shuffleAlbums]; } else { newList = [Shuffle shuffleList:[self arrangedObjects]]; } indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange([shuffleList count], [newList count])]; [shuffleList insertObjects:newList atIndexes:indexSet]; unsigned long i; for(i = ([shuffleList count] - [newList count]); i < [shuffleList count]; i++) { [shuffleList[i] setShuffleIndex:(int)i]; } [self commitPersistentStore]; } - (void)resetShuffleList { [shuffleList removeAllObjects]; [self addShuffledListToFront]; if(currentEntry && currentEntry.index >= 0) { if([self shuffle] == ShuffleAlbums) { NSString *currentAlbum = currentEntry.album; if(!currentAlbum) currentAlbum = @""; NSArray *wholeAlbum = [self filterPlaylistOnAlbum:currentAlbum]; // First prune the shuffle list of the currently playing album long i, j; for(i = 0; i < [shuffleList count];) { if([wholeAlbum containsObject:shuffleList[i]]) { [shuffleList removeObjectAtIndex:i]; } else { ++i; } } // Then insert the playing album at the start NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [wholeAlbum count])]; [shuffleList insertObjects:wholeAlbum atIndexes:indexSet]; // Oops, gotta reset the shuffle indexes for(i = 0, j = [shuffleList count]; i < j; ++i) { [shuffleList[i] setShuffleIndex:(int)i]; } [self commitPersistentStore]; } else { [shuffleList insertObject:currentEntry atIndex:0]; [currentEntry setShuffleIndex:0]; // Need to rejigger so the current entry is at the start now... long i, j; BOOL found = NO; for(i = 1, j = [shuffleList count]; i < j && !found; i++) { if(shuffleList[i] == currentEntry) { found = YES; [shuffleList removeObjectAtIndex:i]; } else { [shuffleList[i] setShuffleIndex:(int)i]; } } [self commitPersistentStore]; } } } - (void)setCurrentEntry:(PlaylistEntry *)pe { if(pe == currentEntry || kAppControllerShuttingDown) return; if(currentEntry) { currentEntry.current = NO; currentEntry.stopAfter = NO; currentEntry.currentPosition = 0.0; currentEntry.countAdded = NO; } if(pe) { pe.current = YES; } [self commitPersistentStore]; NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; if(currentEntry != nil && !currentEntry.deLeted && currentEntry.index < NSNotFound) [refreshSet addIndex:currentEntry.index]; if(pe != nil && !pe.deLeted && pe.index < NSNotFound) [refreshSet addIndex:pe.index]; // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; if(pe != nil) [self.tableView scrollRowToVisible:pe.index]; currentEntry = pe; } - (void)setShuffle:(ShuffleMode)s { [[NSUserDefaults standardUserDefaults] setInteger:s forKey:@"shuffle"]; if(s != ShuffleOff) [self resetShuffleList]; [playbackController playlistDidChange:self]; } - (ShuffleMode)shuffle { return (ShuffleMode)[[NSUserDefaults standardUserDefaults] integerForKey:@"shuffle"]; } - (void)setRepeat:(RepeatMode)r { [[NSUserDefaults standardUserDefaults] setInteger:r forKey:@"repeat"]; [playbackController playlistDidChange:self]; } - (RepeatMode)repeat { return (RepeatMode)[[NSUserDefaults standardUserDefaults] integerForKey:@"repeat"]; } - (IBAction)clear:(id)sender { [self setFilterPredicate:nil]; [self removeObjectsAtArrangedObjectIndexes: [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [[self arrangedObjects] count])]]; } - (IBAction)clearFilterPredicate:(id)sender { [self setFilterPredicate:nil]; } - (void)setFilterPredicate:(NSPredicate *)filterPredicate { [super setFilterPredicate:filterPredicate]; } - (IBAction)showEntryInFinder:(id)sender { if([[self selectedObjects] count] == 0) return; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; NSURL *url = [[self selectedObjects][0] url]; if([url isFileURL]) [ws selectFile:[url path] inFileViewerRootedAtPath:[url path]]; } /* - (IBAction)showTagEditor:(id)sender { // call the editor & pass the url if ([self selectionIndex] < 0) return; NSURL *url = [[[self selectedObjects] objectAtIndex:0] URL]; if ([url isFileURL]) [TagEditorController openTagEditor:url sender:sender]; } */ - (IBAction)searchByArtist:(id)sender; { PlaylistEntry *entry = [[self arrangedObjects] objectAtIndex:[self selectionIndex]]; [spotlightWindowController searchForArtist:[entry artist]]; } - (IBAction)searchByAlbum:(id)sender; { PlaylistEntry *entry = [[self arrangedObjects] objectAtIndex:[self selectionIndex]]; [spotlightWindowController searchForAlbum:[entry album]]; } - (NSMutableArray *)queueList { return queueList; } - (IBAction)emptyQueueList:(id)sender { [self emptyQueueListUnsynced]; } - (void)emptyQueueListUnsynced { NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *queueItem in queueList) { queueItem.queued = NO; [queueItem setQueuePosition:-1]; [refreshSet addIndex:[queueItem index]]; } [queueList removeAllObjects]; [self commitPersistentStore]; // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } - (IBAction)toggleQueued:(id)sender { NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *queueItem in [self selectedObjects]) { if(queueItem.queued) { [queueList removeObject:queueItem]; queueItem.queued = NO; queueItem.queuePosition = -1; } else { queueItem.queued = YES; queueItem.queuePosition = (int)[queueList count]; [queueList addObject:queueItem]; } [refreshSet addIndex:[queueItem index]]; DLog(@"TOGGLE QUEUED: %i", queueItem.queued); } for(PlaylistEntry *queueItem in queueList) { if(![[self selectedObjects] containsObject:queueItem]) [refreshSet addIndex:[queueItem index]]; } int i = 0; for(PlaylistEntry *cur in queueList) { cur.queuePosition = i++; } [self commitPersistentStore]; // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } - (IBAction)removeFromQueue:(id)sender { NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *queueItem in [self selectedObjects]) { queueItem.queued = NO; queueItem.queuePosition = -1; [queueList removeObject:queueItem]; [refreshSet addIndex:[queueItem index]]; } for(PlaylistEntry *queueItem in queueList) { [refreshSet addIndex:[queueItem index]]; } int i = 0; for(PlaylistEntry *cur in queueList) { cur.queuePosition = i++; } [self commitPersistentStore]; // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } - (IBAction)addToQueue:(id)sender { NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *queueItem in [self selectedObjects]) { queueItem.queued = YES; queueItem.queuePosition = (int)[queueList count]; [queueList addObject:queueItem]; } for(PlaylistEntry *queueItem in queueList) { [refreshSet addIndex:[queueItem index]]; } int i = 0; for(PlaylistEntry *cur in queueList) { cur.queuePosition = i++; } [self commitPersistentStore]; // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } - (IBAction)stopAfterCurrent:(id)sender { currentEntry.stopAfter = !currentEntry.stopAfter; [self commitPersistentStore]; NSIndexSet *refreshSet = [NSIndexSet indexSetWithIndex:[currentEntry index]]; // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } - (IBAction)stopAfterSelection:(id)sender { NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *pe in [self selectedObjects]) { pe.stopAfter = !pe.stopAfter; [refreshSet addIndex:pe.index]; } [self commitPersistentStore]; // Refresh entire row of all affected items to update tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL action = [menuItem action]; if(action == @selector(removeFromQueue:)) { for(PlaylistEntry *q in [self selectedObjects]) if(q.queuePosition >= 0) return YES; return NO; } if(action == @selector(emptyQueueList:) && ([queueList count] < 1)) return NO; if(action == @selector(stopAfterCurrent:) && currentEntry.stopAfter) return NO; // if nothing is selected, gray out these if([[self selectedObjects] count] < 1) { if(action == @selector(remove:)) return NO; if(action == @selector(addToQueue:)) return NO; if(action == @selector(toggleQueued:)) return NO; if(action == @selector(searchByArtist:)) return NO; if(action == @selector(searchByAlbum:)) return NO; if(action == @selector(stopAfterSelection:)) return NO; } return YES; } // Asynchronous event inlets: - (void)addURLsInBackground:(NSDictionary *)input { NSUInteger row = [[self content] count]; NSMutableDictionary *_input = [input mutableCopy]; [_input setObject:@(row) forKey:@"index"]; [self insertURLsInBackground:_input]; } static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_block_t block) { if(dispatch_queue_get_label(queue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { block(); } else { dispatch_sync(queue, block); } } - (void)insertURLsInBackground:(NSDictionary *)input { NSArray *entries = [input objectForKey:@"entries"]; NSUInteger row = [[input objectForKey:@"index"] integerValue]; BOOL sort = [[input objectForKey:@"sort"] boolValue]; URLOrigin origin = (URLOrigin)[[input objectForKey:@"origin"] integerValue]; NSUInteger countNow = [[self content] count]; dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ [self willInsertURLs:entries origin:origin]; }); if(countNow && row && ![[self content] count]) { row = 0; } NSArray *urlsAccepted = [playlistLoader insertURLs:entries atIndex:row sort:sort]; dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ [self didInsertURLs:urlsAccepted origin:origin]; }); dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ if([self shuffle] != ShuffleOff) [self resetShuffleList]; }); } // Event inlets: - (void)willInsertURLs:(NSArray *)urls origin:(URLOrigin)origin { if(![urls count]) return; CGEventRef event = CGEventCreate(NULL /*default event source*/); CGEventFlags mods = CGEventGetFlags(event); CFRelease(event); BOOL modifierPressed = ((mods & kCGEventFlagMaskCommand) != 0) & ((mods & kCGEventFlagMaskControl) != 0); modifierPressed |= ((mods & kCGEventFlagMaskShift) != 0); NSString *behavior = [[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesBehavior"]; if(modifierPressed) { behavior = [[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesAlteredBehavior"]; } BOOL shouldClear = modifierPressed; // By default, internal sources should not clear the playlist if(origin == URLOriginExternal) { // For external insertions, we look at the preference // possible settings are "clearAndPlay", "enqueue", "enqueueAndPlay" shouldClear = [behavior isEqualToString:@"clearAndPlay"]; } if(shouldClear) { [self clear:self]; } } - (void)didInsertURLs:(NSArray *)urls origin:(URLOrigin)origin { if(![urls count]) return; NSArray *nsurls = [urls valueForKey:@"url"]; if(![[SandboxBroker sharedSandboxBroker] areAllPathsSafe:nsurls]) { [appController showPathSuggester]; } CGEventRef event = CGEventCreate(NULL); CGEventFlags mods = CGEventGetFlags(event); CFRelease(event); BOOL modifierPressed = ((mods & kCGEventFlagMaskCommand) != 0) & ((mods & kCGEventFlagMaskControl) != 0); modifierPressed |= ((mods & kCGEventFlagMaskShift) != 0); NSString *behavior = [[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesBehavior"]; if(modifierPressed) { behavior = [[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesAlteredBehavior"]; } BOOL shouldPlay = modifierPressed; // The default is NO for internal insertions if(origin == URLOriginExternal) { // For external insertions, we look at the preference shouldPlay = [behavior isEqualToString:@"clearAndPlay"] || [behavior isEqualToString:@"enqueueAndPlay"]; ; } // Auto start playback if(shouldPlay && [[self content] count] > 0) { [playbackController playEntry:urls[0]]; } } - (IBAction)reloadTags:(id)sender { NSArray *selectedobjects = [self selectedObjects]; if([selectedobjects count]) { for(PlaylistEntry *pe in selectedobjects) { pe.metadataLoaded = NO; } [playlistLoader performSelectorInBackground:@selector(loadInfoForEntries:) withObject:selectedobjects]; } } - (IBAction)resetPlaycounts:(id)sender { NSArray *selectedobjects = [self selectedObjects]; if([selectedobjects count]) { for(PlaylistEntry *pe in selectedobjects) { [self resetPlayCountForTrack:pe]; } [self commitPersistentStore]; } } - (IBAction)removeRatings:(id)sender { NSArray *selectedobjects = [self selectedObjects]; if([selectedobjects count]) { for(PlaylistEntry *pe in selectedobjects) { [self removeRatingForTrack:pe]; } [self commitPersistentStore]; NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init]; for(PlaylistEntry *pe in selectedobjects) { if(pe.index >= 0 && pe.index < NSNotFound) { [refreshSet addIndex:pe.index]; } } // Refresh entire row to refresh tooltips unsigned long columns = [[self.tableView tableColumns] count]; [self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]]; } } - (BOOL)pathSuggesterEmpty { __block BOOL rval; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"]; [self.persistentContainer.viewContext performBlockAndWait:^{ NSError *error = nil; NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error]; if(!results || [results count] < 1) rval = YES; else rval = NO; }]; return rval; } @end