diff --git a/Application/AppController.m b/Application/AppController.m index 3cb5d1284..556e9e08b 100644 --- a/Application/AppController.m +++ b/Application/AppController.m @@ -21,6 +21,8 @@ #import "ColorToValueTransformer.h" +#import "TotalTimeTransformer.h" + #import "Shortcuts.h" #import @@ -53,8 +55,11 @@ void *kAppControllerContext = &kAppControllerContext; NSValueTransformer *colorToValueTransformer = [[ColorToValueTransformer alloc] init]; [NSValueTransformer setValueTransformer:colorToValueTransformer forName:@"ColorToValueTransformer"]; -} + NSValueTransformer *totalTimeTransformer = [[TotalTimeTransformer alloc] init]; + [NSValueTransformer setValueTransformer:totalTimeTransformer + forName:@"TotalTimeTransformer"]; +} - (id)init { self = [super init]; if(self) { @@ -307,6 +312,12 @@ void *kAppControllerContext = &kAppControllerContext; miniWindow.title = title; mainWindow.title = title; } + } else if([keyPath isEqualToString:@"finished"]) { + NSProgress *progress = (NSProgress *)object; + if([progress isFinished]) { + playbackController.progressOverall = nil; + [NSApp terminate:nil]; + } } } @@ -324,6 +335,15 @@ void *kAppControllerContext = &kAppControllerContext; [expandedNodes removeObject:url]; } +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + if(playbackController.progressOverall) { + [playbackController.progressOverall addObserver:self forKeyPath:@"finished" options:0 context:kAppControllerContext]; + return NSTerminateLater; + } else { + return NSTerminateNow; + } +} + - (void)applicationWillTerminate:(NSNotification *)aNotification { CogStatus currentStatus = [playbackController playbackStatus]; NSInteger lastTrackPlaying = -1; diff --git a/Application/DockIconController.m b/Application/DockIconController.m index 21e1f2121..e52dd7cf1 100644 --- a/Application/DockIconController.m +++ b/Application/DockIconController.m @@ -16,14 +16,22 @@ static NSString *DockIconPlaybackStatusObservationContext = @"DockIconPlaybackSt - (void)startObserving { [playbackController addObserver:self forKeyPath:@"playbackStatus" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; - [playbackController addObserver:self forKeyPath:@"progressBarStatus" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; + [playbackController addObserver:self forKeyPath:@"progressOverall" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.colorfulDockIcons" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; } - (void)stopObserving { - [playbackController removeObserver:self forKeyPath:@"playbackStatus"]; - [playbackController removeObserver:self forKeyPath:@"progressBarStatus"]; - [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.colorfulDockIcons"]; + [playbackController removeObserver:self forKeyPath:@"playbackStatus" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; + [playbackController removeObserver:self forKeyPath:@"progressOverall" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; + [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.colorfulDockIcons" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; +} + +- (void)startObservingProgress:(NSProgress *)progress { + [progress addObserver:self forKeyPath:@"fractionCompleted" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; +} + +- (void)stopObservingProgress:(NSProgress *)progress { + [progress removeObserver:self forKeyPath:@"fractionCompleted" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)]; } static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) { @@ -151,12 +159,35 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) { NSInteger playbackStatus = [[change objectForKey:NSKeyValueChangeNewKey] integerValue]; [self refreshDockIcon:playbackStatus withProgress:-10]; - } else if([keyPath isEqualToString:@"progressBarStatus"]) { - double progressStatus = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue]; + } else if([keyPath isEqualToString:@"progressOverall"]) { + double progressStatus = [lastProgressStatus doubleValue]; + + 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]; + progressStatus = -1; + } + + if(progressNew) { + [self startObservingProgress:progressNew]; + progressStatus = progressNew.fractionCompleted * 100.0; + } [self refreshDockIcon:-1 withProgress:progressStatus]; } else if([keyPath isEqualToString:@"values.colorfulDockIcons"]) { [self refreshDockIcon:-1 withProgress:-10]; + } else if([keyPath isEqualToString:@"fractionCompleted"]) { + double progressStatus = [(NSProgress *)object fractionCompleted]; + [self refreshDockIcon:-1 withProgress:progressStatus * 100.0]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; diff --git a/Application/PlaybackController.h b/Application/PlaybackController.h index f76e3a139..12e0e3f5e 100644 --- a/Application/PlaybackController.h +++ b/Application/PlaybackController.h @@ -53,14 +53,16 @@ extern NSDictionary *makeRGInfo(PlaylistEntry *pe); BOOL fading; // progress bar display - double progressBarStatus; + NSProgress *progressOverall; + NSProgress *progressJob; AudioUnit _eq; } @property CogStatus playbackStatus; -@property double progressBarStatus; +@property NSProgress *progressOverall; +@property NSProgress *progressJob; - (IBAction)changeVolume:(id)sender; - (IBAction)volumeDown:(id)sender; diff --git a/Application/PlaybackController.m b/Application/PlaybackController.m index 1e0d3073b..fc822412d 100644 --- a/Application/PlaybackController.m +++ b/Application/PlaybackController.m @@ -30,7 +30,8 @@ NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation"; @synthesize playbackStatus; -@synthesize progressBarStatus; +@synthesize progressOverall; +@synthesize progressJob; + (NSSet *)keyPathsForValuesAffectingSeekable { return [NSSet setWithObjects:@"playlistController.currentEntry", @"playlistController.currentEntry.seekable", nil]; @@ -44,7 +45,8 @@ NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation"; seekable = NO; fading = NO; - progressBarStatus = -1; + progressOverall = nil; + progressJob = nil; audioPlayer = [[AudioPlayer alloc] init]; [audioPlayer setDelegate:self]; diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 077024c03..c7a340b43 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -25,17 +25,17 @@ - + - + - + - + @@ -651,9 +651,15 @@ - + - Total Duration: %{value1}@ + %{value1}@%{value2}@ + TotalTimeTransformer + + + + + %{value1}@%{value2}@ diff --git a/Cog.xcodeproj/project.pbxproj b/Cog.xcodeproj/project.pbxproj index 6f73e41d3..3eb9b4753 100644 --- a/Cog.xcodeproj/project.pbxproj +++ b/Cog.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ 83489C6B2782F78700BDCEA2 /* libvgmPlayer.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 83489C542782F2DF00BDCEA2 /* libvgmPlayer.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8349270C27B4EFFC0009AB2B /* duplicateItemsTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 8349270127B4EFFC0009AB2B /* duplicateItemsTemplate.pdf */; }; 8349270D27B4EFFC0009AB2B /* deadItemsTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 8349270B27B4EFFC0009AB2B /* deadItemsTemplate.pdf */; }; + 834B05EA2859C006000B7DC0 /* TotalTimeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 834B05E92859C006000B7DC0 /* TotalTimeTransformer.m */; }; 834D793F20E4EFEA00C4A5CC /* OpusPlugin.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 830B62B320E4EF89004A74B2 /* OpusPlugin.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 834D794020E4EFEF00C4A5CC /* VorbisPlugin.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8301F94520E4EEF70017B2DC /* VorbisPlugin.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 834F7F4320E4E4ED00228DAB /* AdPlug.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8303A30920E4E3D000951EF8 /* AdPlug.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1004,6 +1005,8 @@ 83489C4E2782F2DF00BDCEA2 /* libvgmPlayer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = libvgmPlayer.xcodeproj; path = Plugins/libvgmPlayer/libvgmPlayer.xcodeproj; sourceTree = ""; }; 8349270127B4EFFC0009AB2B /* duplicateItemsTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = duplicateItemsTemplate.pdf; path = Images/duplicateItemsTemplate.pdf; sourceTree = ""; }; 8349270B27B4EFFC0009AB2B /* deadItemsTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = deadItemsTemplate.pdf; path = Images/deadItemsTemplate.pdf; sourceTree = ""; }; + 834B05E82859C006000B7DC0 /* TotalTimeTransformer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TotalTimeTransformer.h; path = Transformers/TotalTimeTransformer.h; sourceTree = ""; }; + 834B05E92859C006000B7DC0 /* TotalTimeTransformer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = TotalTimeTransformer.m; path = Transformers/TotalTimeTransformer.m; sourceTree = ""; }; 8355D6B4180612F300D05687 /* NSData+MD5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+MD5.h"; sourceTree = ""; }; 8355D6B5180612F300D05687 /* NSData+MD5.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+MD5.m"; sourceTree = ""; }; 8355D6B7180613FB00D05687 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -1539,6 +1542,8 @@ 17E0D6140F520F87005B6FED /* StringToURLTransformer.h */, 17E0D6150F520F87005B6FED /* StringToURLTransformer.m */, 838F84FF25687C5C00C3E614 /* Cog-Bridging-Header.h */, + 834B05E82859C006000B7DC0 /* TotalTimeTransformer.h */, + 834B05E92859C006000B7DC0 /* TotalTimeTransformer.m */, ); name = Transformers; sourceTree = ""; @@ -2775,6 +2780,7 @@ 17249F0F0D82E17700F33392 /* ToggleQueueTitleTransformer.m in Sources */, 179D031E0E0CB2500064A77A /* ContainedNode.m in Sources */, 179D031F0E0CB2500064A77A /* ContainerNode.m in Sources */, + 834B05EA2859C006000B7DC0 /* TotalTimeTransformer.m in Sources */, 839DA7CF274A2D4C001B18E5 /* NSDictionary+Merge.m in Sources */, 179D03200E0CB2500064A77A /* DirectoryNode.m in Sources */, 179D03210E0CB2500064A77A /* FileIconCell.m in Sources */, diff --git a/Playlist/PlaylistController.h b/Playlist/PlaylistController.h index 5763c0eeb..383d8e0dc 100644 --- a/Playlist/PlaylistController.h +++ b/Playlist/PlaylistController.h @@ -46,16 +46,20 @@ typedef NS_ENUM(NSInteger, URLOrigin) { NSMutableArray *queueList; NSString *totalTime; + NSString *currentStatus; PlaylistEntry *currentEntry; PlaylistEntry *nextEntryAfterDeleted; NSUndoManager *undoManager; + + BOOL observersRegistered; } @property(nonatomic, retain) PlaylistEntry *_Nullable currentEntry; @property(retain) NSString *_Nullable totalTime; +@property(retain) NSString *_Nullable currentStatus; // Private Methods - (void)updateTotalTime; diff --git a/Playlist/PlaylistController.m b/Playlist/PlaylistController.m index 5b6111fd5..08c0de4d5 100644 --- a/Playlist/PlaylistController.m +++ b/Playlist/PlaylistController.m @@ -28,9 +28,12 @@ @synthesize currentEntry; @synthesize totalTime; +@synthesize currentStatus; static NSArray *cellIdentifiers = nil; +static void *playlistControllerContext = &playlistControllerContext; + + (void)initialize { cellIdentifiers = @[@"index", @"status", @"title", @"albumartist", @"artist", @"album", @"length", @"year", @"genre", @"track", @"path", @@ -117,22 +120,93 @@ static NSArray *cellIdentifiers = nil; [self addObserver:self forKeyPath:@"arrangedObjects" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) - context:nil]; - [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fontSize" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:nil]; + 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([keyPath isEqualToString:@"arrangedObjects"]) { - [self updatePlaylistIndexes]; + 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]; - [self.tableView reloadData]; - } else if([keyPath isEqualToString:@"values.fontSize"]) { - [self updateRowSize]; } } @@ -144,13 +218,31 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc } } -- (void)setProgressBarStatus:(double)status { +- (void)beginProgress:(NSString *)localizedDescription { dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ - [self->playbackController setProgressBarStatus:status]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.001]]; + self->playbackController.progressOverall = [NSProgress progressWithTotalUnitCount:100000]; + self->playbackController.progressOverall.localizedDescription = localizedDescription; }); } +- (void)completeProgress { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + [self->playbackController.progressOverall setCompletedUnitCount:100000]; + self->playbackController.progressOverall = nil; + }); +} + +- (void)setProgressStatus:(double)status { + if(status >= 0) { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + NSUInteger jobCount = (NSUInteger)ceil(1000.0 * status); + [self->playbackController.progressOverall setCompletedUnitCount:jobCount]; + }); + } else { + [self completeProgress]; + } +} + - (void)updatePlaylistIndexes { NSArray *arranged = [self arrangedObjects]; NSUInteger n = [arranged count]; @@ -163,10 +255,12 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc } } if(updated) { + [self beginProgress:NSLocalizedString(@"ProgressActionUpdatingIndexes", @"updating playlist indexes")]; [[SQLiteStore sharedStore] syncPlaylistEntries:arranged progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; + [self completeProgress]; } } @@ -461,12 +555,16 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc [super moveObjectsFromIndex:fromIndex toArrangedObjectIndexes:indexSet]; + [self beginProgress:NSLocalizedString(@"ProgressActionMovingEntries", @"moving playlist entries")]; + [[SQLiteStore sharedStore] playlistMoveObjectsFromIndex:fromIndex toArrangedObjectIndexes:indexSet progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; + [self completeProgress]; + [playbackController playlistDidChange:self]; } @@ -481,12 +579,16 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc [super moveObjectsInArrangedObjectsFromIndexes:indexSet toIndex:insertIndex]; + [self beginProgress:NSLocalizedString(@"ProgressActionMovingEntries", @"moving playlist entries")]; + [[SQLiteStore sharedStore] playlistMoveObjectsInArrangedObjectsFromIndexes:indexSet toIndex:insertIndex progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; + [self completeProgress]; + [playbackController playlistDidChange:self]; } @@ -669,14 +771,18 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc pe.deleted = NO; } + [self beginProgress:NSLocalizedString(@"ProgressActionInsertingEntries", @"inserting playlist entries")]; + [[SQLiteStore sharedStore] playlistInsertTracks:objects atObjectIndexes:indexes progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; [super insertObjects:objects atArrangedObjectIndexes:indexes]; + [self completeProgress]; + if([self shuffle] != ShuffleOff) [self resetShuffleList]; } @@ -696,14 +802,18 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc pe.trashURL = nil; } + [self beginProgress:NSLocalizedString(@"ProgressActionUntrashingEntries", @"restoring playlist entries from the trash")]; + [[SQLiteStore sharedStore] playlistInsertTracks:objects atObjectIndexes:indexes progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; [super insertObjects:objects atArrangedObjectIndexes:indexes]; + [self completeProgress]; + if([self shuffle] != ShuffleOff) [self resetShuffleList]; } @@ -759,15 +869,19 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc currentEntry.index = -i - 1; } + [self beginProgress:NSLocalizedString(@"ProgressActionRemovingEntries", @"removing playlist entries")]; + [[SQLiteStore sharedStore] playlistRemoveTracksAtIndexes:unarrangedIndexes progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; [super removeObjectsAtArrangedObjectIndexes:indexes]; if([self shuffle] != ShuffleOff) [self resetShuffleList]; + [self completeProgress]; + [playbackController playlistDidChange:self]; } @@ -799,9 +913,11 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc } } + [self beginProgress:NSLocalizedString(@"ProgressActionTrashingEntries", @"moving playlist entries to the trash")]; + [[SQLiteStore sharedStore] playlistRemoveTracksAtIndexes:unarrangedIndexes progressCall:^(double progress) { - [self setProgressBarStatus:progress]; + [self setProgressStatus:progress]; }]; [super removeObjectsAtArrangedObjectIndexes:indexes]; @@ -818,6 +934,8 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc pe.trashURL = removed; } } + + [self completeProgress]; } - (void)setSortDescriptors:(NSArray *)sortDescriptors { diff --git a/Playlist/PlaylistLoader.h b/Playlist/PlaylistLoader.h index d02ec0824..53e941f31 100644 --- a/Playlist/PlaylistLoader.h +++ b/Playlist/PlaylistLoader.h @@ -26,6 +26,8 @@ typedef enum { IBOutlet PlaybackController *playbackController; NSOperationQueue *queue; + + BOOL metadataLoadInProgress; } - (void)initDefaults; diff --git a/Playlist/PlaylistLoader.m b/Playlist/PlaylistLoader.m index aae3e940a..e42a262da 100644 --- a/Playlist/PlaylistLoader.m +++ b/Playlist/PlaylistLoader.m @@ -261,12 +261,62 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc } } -- (void)setProgressBarStatus:(double)status { +- (void)beginProgress:(NSString *)localizedDescription { dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ - [self->playbackController setProgressBarStatus:status]; + self->playbackController.progressOverall = [NSProgress progressWithTotalUnitCount:100000]; + self->playbackController.progressOverall.localizedDescription = localizedDescription; }); } +- (void)completeProgress { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + if(self->playbackController.progressJob) { + [self->playbackController.progressJob setCompletedUnitCount:100000]; + self->playbackController.progressJob = nil; + } + [self->playbackController.progressOverall setCompletedUnitCount:100000]; + self->playbackController.progressOverall = nil; + }); +} + +- (void)beginProgressJob:(NSString *)localizedDescription percentOfTotal:(double)percent { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + NSUInteger jobCount = (NSUInteger)ceil(1000.0 * percent); + self->playbackController.progressJob = [NSProgress progressWithTotalUnitCount:100000]; + self->playbackController.progressJob.localizedDescription = localizedDescription; + [self->playbackController.progressOverall addChild:self->playbackController.progressJob withPendingUnitCount:jobCount]; + }); +} + +- (void)completeProgressJob { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + [self->playbackController.progressJob setCompletedUnitCount:100000]; + self->playbackController.progressJob = nil; + }); +} + +- (void)setProgressStatus:(double)status { + if(status >= 0) { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + NSUInteger jobCount = (NSUInteger)ceil(1000.0 * status); + [self->playbackController.progressOverall setCompletedUnitCount:jobCount]; + }); + } else { + [self completeProgress]; + } +} + +- (void)setProgressJobStatus:(double)status { + if(status >= 0) { + dispatch_sync_reentrant(dispatch_get_main_queue(), ^{ + NSUInteger jobCount = (NSUInteger)ceil(1000.0 * status); + [self->playbackController.progressJob setCompletedUnitCount:jobCount]; + }); + } else { + [self completeProgressJob]; + } +} + - (NSArray *)insertURLs:(NSArray *)urls atIndex:(NSInteger)index sort:(BOOL)sort { NSMutableSet *uniqueURLs = [NSMutableSet set]; @@ -276,19 +326,23 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc NSMutableArray *validURLs = [NSMutableArray array]; NSDictionary *xmlData = nil; - double progress = 0.0; + double progress; if(!urls) { - [self setProgressBarStatus:-1]; + [self completeProgress]; return @[]; } + [self beginProgress:NSLocalizedString(@"ProgressActionLoader", @"playlist loader inserting files")]; + + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderListingFiles", @"collecting files") percentOfTotal:20.0]; + if(index < 0) index = 0; - [self setProgressBarStatus:progress]; + progress = 0.0; - double progressstep = [urls count] ? 20.0 / (double)([urls count]) : 0; + double progressstep = [urls count] ? 100.0 / (double)([urls count]) : 0; NSURL *url; for(url in urls) { @@ -309,12 +363,12 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc progress += progressstep; - [self setProgressBarStatus:progress]; + [self setProgressJobStatus:progress]; } - progress = 20.0; + [self completeProgressJob]; - [self setProgressBarStatus:progress]; + progress = 0.0; DLog(@"Expanded urls: %@", expandedURLs); @@ -326,7 +380,9 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc sortedURLs = expandedURLs; } - progressstep = [sortedURLs count] ? 20.0 / (double)([sortedURLs count]) : 0; + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderFilteringContainerFiles", @"handling container file types") percentOfTotal:20.0]; + + progressstep = [sortedURLs count] ? 100.0 / (double)([sortedURLs count]) : 0; for(url in sortedURLs) { // Container vs non-container url @@ -349,17 +405,23 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc } progress += progressstep; - [self setProgressBarStatus:progress]; + [self setProgressJobStatus:progress]; } - progress = 40.0; - [self setProgressBarStatus:progress]; + progress = 0.0; + [self completeProgressJob]; + + if([fileURLs count] > 0) { + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderFilteringFiles", @"eliminating unsupported file types") percentOfTotal:20.0]; + } else { + [self setProgressStatus:60.0]; + } DLog(@"File urls: %@", fileURLs); DLog(@"Contained urls: %@", containedURLs); - progressstep = [fileURLs count] ? 20.0 / (double)([fileURLs count]) : 0; + progressstep = [fileURLs count] ? 100.0 / (double)([fileURLs count]) : 0; for(url in fileURLs) { progress += progressstep; @@ -379,16 +441,24 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc [uniqueURLs addObject:url]; } - [self setProgressBarStatus:progress]; + [self setProgressJobStatus:progress]; } - progress = 60.0; + progress = 0.0; - [self setProgressBarStatus:progress]; + if([fileURLs count] > 0) { + [self completeProgressJob]; + } + + if([containedURLs count] > 0) { + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderFilteringContainedFiles", @"eliminating unsupported file types from containers") percentOfTotal:20.0]; + } else { + [self setProgressStatus:80.0]; + } DLog(@"Valid urls: %@", validURLs); - progressstep = [containedURLs count] ? 20.0 / (double)([containedURLs count]) : 0; + progressstep = [containedURLs count] ? 100.0 / (double)([containedURLs count]) : 0; for(url in containedURLs) { progress += progressstep; @@ -402,12 +472,13 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc [validURLs addObject:url]; - [self setProgressBarStatus:progress]; + [self setProgressJobStatus:progress]; } - progress = 80.0; - - [self setProgressBarStatus:progress]; + progress = 0.0; + if([containedURLs count] > 0) { + [self completeProgressJob]; + } // Create actual entries int count = (int)[validURLs count]; @@ -415,11 +486,13 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc // no valid URLs, or they use an unsupported URL scheme if(!count) { - [self setProgressBarStatus:-1]; + [self completeProgress]; return @[]; } - progressstep = 20.0 / (double)(count); + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderAddingEntries", @"creating and adding playlist entries") percentOfTotal:20.0]; + + progressstep = 100.0 / (double)(count); NSInteger i = 0; NSMutableArray *entries = [NSMutableArray arrayWithCapacity:count]; @@ -436,7 +509,7 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc ++i; progress += progressstep; - [self setProgressBarStatus:progress]; + [self setProgressJobStatus:progress]; } NSInteger j = index + i; @@ -455,8 +528,7 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc } } - progress = 100.0; - [self setProgressBarStatus:progress]; + [self completeProgress]; NSIndexSet *is = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, [entries count])]; @@ -486,19 +558,21 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc NSMutableArray *arrayRest = [entries mutableCopy]; [arrayRest removeObjectAtIndex:0]; - progress = 0.0; - [self setProgressBarStatus:progress]; + metadataLoadInProgress = YES; + + [self beginProgress:NSLocalizedString(@"ProgressActionLoadingMetadata", @"loading metadata for tracks")]; + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoadingMetadata", @"processing files") percentOfTotal:50.0]; [self performSelectorOnMainThread:@selector(syncLoadInfoForEntries:) withObject:arrayFirst waitUntilDone:YES]; progressstep = 100.0 / (double)([entries count]); - progress += progressstep; - [self setProgressBarStatus:progress]; + progress = progressstep; + [self setProgressJobStatus:progress]; if([arrayRest count]) [self performSelectorInBackground:@selector(loadInfoForEntries:) withObject:arrayRest]; else - [self setProgressBarStatus:-1]; + [self completeProgress]; return entries; } } @@ -510,17 +584,20 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc SQLiteStore *store = [SQLiteStore sharedStore]; - __block double progress = [playbackController progressBarStatus]; + __block double progress = 0.0; - if(progress < 0 || progress >= 100) - progress = 0; + double progressstep; - double progressRemaining = 100.0 - progress; + if(metadataLoadInProgress && [entries count]) { + progressstep = 100.0 / (double)([entries count] + 1); + progress = progressstep; + } else if([entries count]) { + [self beginProgress:NSLocalizedString(@"ProgressActionLoadingMetadata", @"loading metadata for tracks")]; + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoadingMetadata", @"processing files") percentOfTotal:50.0]; - // 50% for properties reading, 50% for applying them to the main thread - const double progressstep = [entries count] ? (progressRemaining / 2.0) / [entries count] : 0; - - progressRemaining = progress + (progressRemaining / 2.0); + progressstep = 100.0 / (double)([entries count]); + progress = 0.0; + } i = 0; j = 0; @@ -537,6 +614,8 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc if(!i) { [playlistController performSelectorOnMainThread:@selector(updateTotalTime) withObject:nil waitUntilDone:NO]; + [self completeProgress]; + metadataLoadInProgress = NO; return; } @@ -555,11 +634,13 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc NSBlockOperation *op = [[NSBlockOperation alloc] init]; [op addExecutionBlock:^{ - [weakLock lock]; - progress += progressstep; - [weakLock unlock]; - - if(weakPe.deleted) return; + if(weakPe.deleted) { + [weakLock lock]; + progress += progressstep; + [self setProgressJobStatus:progress]; + [weakLock unlock]; + return; + } DLog(@"Loading metadata for %@", weakPe.URL); @@ -575,7 +656,8 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc entryInfo = [weakDataStore coalesceEntryInfo:entryInfo]; [weakArray addObject:weakPe]; [weakArray addObject:entryInfo]; - [self setProgressBarStatus:progress]; + progress += progressstep; + [self setProgressJobStatus:progress]; [weakLock unlock]; }]; @@ -585,8 +667,12 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc [queue waitUntilAllOperationsAreFinished]; - progress = progressRemaining; - [self setProgressBarStatus:progress]; + progress = 0.0; + [self completeProgressJob]; + + [self beginProgressJob:NSLocalizedString(@"ProgressSubActionMetadataApply", @"applying info to playlist storage") percentOfTotal:50.0]; + + progressstep = 200.0 / (double)([outArray count]); for(i = 0, j = [outArray count]; i < j; i += 2) { __block PlaylistEntry *weakPe = [outArray objectAtIndex:i]; @@ -597,7 +683,7 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc [store trackUpdate:weakPe]; } progress += progressstep; - [self setProgressBarStatus:progress]; + [self setProgressJobStatus:progress]; }); } @@ -612,8 +698,10 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc }); } - [self setProgressBarStatus:-1]; + [self completeProgress]; + metadataLoadInProgress = NO; } + // To be called on main thread only - (void)syncLoadInfoForEntries:(NSArray *)entries { NSMutableIndexSet *update_indexes = [[NSMutableIndexSet alloc] init]; diff --git a/Transformers/TotalTimeTransformer.h b/Transformers/TotalTimeTransformer.h new file mode 100644 index 000000000..8b4f2198e --- /dev/null +++ b/Transformers/TotalTimeTransformer.h @@ -0,0 +1,16 @@ +// +// TotalTimeTransformer.h +// Cog +// +// Created by Christopher Snowhill on 6/15/22. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TotalTimeTransformer : NSValueTransformer + +@end + +NS_ASSUME_NONNULL_END diff --git a/Transformers/TotalTimeTransformer.m b/Transformers/TotalTimeTransformer.m new file mode 100644 index 000000000..a7e566f62 --- /dev/null +++ b/Transformers/TotalTimeTransformer.m @@ -0,0 +1,32 @@ +// +// TotalTimeTransformer.m +// Cog +// +// Created by Christopher Snowhill on 6/15/22. +// + +#import "TotalTimeTransformer.h" + +@implementation TotalTimeTransformer + ++ (Class)transformedValueClass { + return [NSString class]; +} ++ (BOOL)allowsReverseTransformation { + return NO; +} + +- (id)transformedValue:(id)value { + if(value == nil) return @""; + + if([value isKindOfClass:[NSString class]]) { + NSString *string = (NSString *)value; + if([string length] > 0) { + return [@"Total Duration: " stringByAppendingString:string]; + } + } + + return @""; +} + +@end diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index b84ceaa35..ffca95a12 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -45,3 +45,21 @@ "CogTitle" = "Cog"; "PreferencesTitle" = "Preferences"; + +"ProgressActionUpdatingIndexes" = "updating playlist indexes"; +"ProgressActionMovingEntries" = "moving playlist entries"; +"ProgressActionInsertingEntries" = "inserting playlist entries"; +"ProgressActionUntrashingEntries" = "restoring playlist entries from the trash"; +"ProgressActionRemovingEntries" = "removing playlist entries"; +"ProgressActionTrashingEntries" = "moving playlist entries to the trash"; + +"ProgressActionLoader" = "playlist loader inserting files"; +"ProgressSubActionLoaderListingFiles" = "collecting files"; +"ProgressSubActionLoaderFilteringContainerFiles" = "handling container file types"; +"ProgressSubActionLoaderFilteringFiles" = "eliminating unsupported file types"; +"ProgressSubActionLoaderFilteringContainedFiles" = "eliminating unsupported file types from containers"; +"ProgressSubActionLoaderAddingEntries" = "creating and adding playlist entries"; + +"ProgressActionLoadingMetadata" = "loading metadata for tracks"; +"ProgressSubActionLoadingMetadata" = "processing files"; +"ProgressSubActionMetadataApply" = "applying info to playlist storage"; diff --git a/es.lproj/Localizable.strings b/es.lproj/Localizable.strings index b84ceaa35..ffca95a12 100644 --- a/es.lproj/Localizable.strings +++ b/es.lproj/Localizable.strings @@ -45,3 +45,21 @@ "CogTitle" = "Cog"; "PreferencesTitle" = "Preferences"; + +"ProgressActionUpdatingIndexes" = "updating playlist indexes"; +"ProgressActionMovingEntries" = "moving playlist entries"; +"ProgressActionInsertingEntries" = "inserting playlist entries"; +"ProgressActionUntrashingEntries" = "restoring playlist entries from the trash"; +"ProgressActionRemovingEntries" = "removing playlist entries"; +"ProgressActionTrashingEntries" = "moving playlist entries to the trash"; + +"ProgressActionLoader" = "playlist loader inserting files"; +"ProgressSubActionLoaderListingFiles" = "collecting files"; +"ProgressSubActionLoaderFilteringContainerFiles" = "handling container file types"; +"ProgressSubActionLoaderFilteringFiles" = "eliminating unsupported file types"; +"ProgressSubActionLoaderFilteringContainedFiles" = "eliminating unsupported file types from containers"; +"ProgressSubActionLoaderAddingEntries" = "creating and adding playlist entries"; + +"ProgressActionLoadingMetadata" = "loading metadata for tracks"; +"ProgressSubActionLoadingMetadata" = "processing files"; +"ProgressSubActionMetadataApply" = "applying info to playlist storage";