cog/Playlist/PlaylistController.m

1965 lines
61 KiB
Objective-C

//
// 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;
NSLock *kPersistentContainerLock = nil;
NSPersistentContainer *kPersistentContainer = nil;
@implementation PlaylistController
@synthesize currentEntry;
@synthesize totalTime;
@synthesize currentStatus;
static NSArray *cellIdentifiers = nil;
NSMutableDictionary<NSString *, AlbumArtwork *> *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;
_persistentContainerLock = [[NSLock alloc] init];
kPersistentContainerLock = self.persistentContainerLock;
self.persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
_persistentArtStorage = [[NSMutableDictionary alloc] init];
kArtworkDictionary = self.persistentArtStorage;
return self;
}
+ (NSPersistentContainer *)sharedPersistentContainer {
return kPersistentContainer;
}
+ (NSLock *)sharedPersistentContainerLock {
return kPersistentContainerLock;
}
- (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 {
NSError *error = nil;
[self.persistentContainerLock lock];
[self.persistentContainer.viewContext save:&error];
[self.persistentContainerLock unlock];
if(error) {
ALog(@"Error committing playlist storage: %@", [error localizedDescription]);
}
}
- (void)updatePlayCountForTrack:(PlaylistEntry *)pe {
if(pe.countAdded) return;
pe.countAdded = YES;
PlayCount *pc = pe.playCountItem;
if(pc) {
[self.persistentContainerLock lock];
pc.count += 1;
pc.lastPlayed = [NSDate date];
[self.persistentContainerLock unlock];
} else {
[self.persistentContainerLock lock];
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.persistentContainerLock unlock];
}
[self commitPersistentStore];
}
- (void)firstSawTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(!pc) {
[self.persistentContainerLock lock];
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.persistentContainerLock unlock];
}
}
- (void)ratingUpdatedWithEntry:(PlaylistEntry *)pe rating:(CGFloat)rating {
if(pe && !pe.deLeted) {
PlayCount *pc = pe.playCountItem;
if(!pc) {
[self.persistentContainerLock lock];
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.persistentContainerLock unlock];
}
[self.persistentContainerLock lock];
pc.rating = rating;
[self.persistentContainerLock unlock];
[self commitPersistentStore];
}
}
- (void)resetPlayCountForTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(pc) {
pc.count = 0;
}
}
- (void)removeRatingForTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(pc) {
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<NSPasteboardWriting>)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<NSDraggingInfo>)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<Class> *classes = @[[NSURL class]];
NSDictionary *options = @{};
NSArray<NSURL *> *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];
NSError *error = nil;
[self.persistentContainerLock lock];
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[self.persistentContainerLock unlock];
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];
NSError *error = nil;
[self.persistentContainerLock lock];
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results) {
results = [results copy];
}
[self.persistentContainerLock unlock];
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 {
BOOL rval;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
NSError *error = nil;
[self.persistentContainerLock lock];
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(!results || [results count] < 1) rval = YES;
else rval = NO;
[self.persistentContainerLock unlock];
return rval;
}
@end