2017-03-12 01:18:27 +00:00
|
|
|
/*
|
|
|
|
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
|
|
|
|
|
|
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
|
|
which should be included with this package. The terms are also available at
|
|
|
|
http://www.gnu.org/licenses/gpl-3.0.html
|
|
|
|
*/
|
|
|
|
|
|
|
|
#import "AppDelegate.h"
|
|
|
|
#import "ProgressController.h"
|
|
|
|
#import "HSPyUtil.h"
|
|
|
|
#import "Consts.h"
|
|
|
|
#import "Dialogs.h"
|
|
|
|
#import "Utils.h"
|
|
|
|
#import "ValueTransformers.h"
|
|
|
|
#import "DetailsPanelPicture.h"
|
|
|
|
|
|
|
|
@implementation AppDelegate
|
|
|
|
|
|
|
|
@synthesize recentResultsMenu;
|
|
|
|
@synthesize columnsMenu;
|
|
|
|
|
|
|
|
+ (NSDictionary *)defaultPreferences
|
|
|
|
{
|
|
|
|
NSMutableDictionary *d = [NSMutableDictionary dictionary];
|
|
|
|
[d setObject:i2n(1) forKey:@"scanTypeStandard"];
|
|
|
|
[d setObject:i2n(3) forKey:@"scanTypeMusic"];
|
|
|
|
[d setObject:i2n(0) forKey:@"scanTypePicture"];
|
|
|
|
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
|
|
|
|
[d setObject:i2n(30) forKey:@"smallFileThreshold"];
|
|
|
|
[d setObject:b2n(YES) forKey:@"wordWeighting"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
|
|
|
|
[d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"scanTagTrack"];
|
|
|
|
[d setObject:b2n(YES) forKey:@"scanTagArtist"];
|
|
|
|
[d setObject:b2n(YES) forKey:@"scanTagAlbum"];
|
|
|
|
[d setObject:b2n(YES) forKey:@"scanTagTitle"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"scanTagGenre"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"scanTagYear"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"matchScaled"];
|
|
|
|
[d setObject:i2n(1) forKey:@"recreatePathType"];
|
|
|
|
[d setObject:i2n(11) forKey:TableFontSize];
|
|
|
|
[d setObject:b2n(YES) forKey:@"mixFileKind"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"useRegexpFilter"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"ignoreHardlinkMatches"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"removeEmptyFolders"];
|
|
|
|
[d setObject:b2n(NO) forKey:@"DebugMode"];
|
|
|
|
[d setObject:@"" forKey:@"CustomCommand"];
|
|
|
|
[d setObject:[NSArray array] forKey:@"recentDirectories"];
|
|
|
|
[d setObject:[NSArray array] forKey:@"columnsOrder"];
|
|
|
|
[d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"];
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)initialize
|
|
|
|
{
|
|
|
|
HSVTAdd *vt = [[[HSVTAdd alloc] initWithValue:4] autorelease];
|
|
|
|
[NSValueTransformer setValueTransformer:vt forName:@"vtRowHeightOffset"];
|
|
|
|
NSDictionary *d = [self defaultPreferences];
|
|
|
|
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d];
|
|
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:d];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (void)awakeFromNib
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
model = [[PyDupeGuru alloc] init];
|
|
|
|
[model bindCallback:createCallback(@"DupeGuruView", self)];
|
|
|
|
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
|
|
|
|
[contentsIndexes addIndex:1];
|
|
|
|
[contentsIndexes addIndex:2];
|
|
|
|
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
|
|
|
|
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
|
|
|
|
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
|
|
|
|
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
|
|
|
|
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
|
|
|
|
i = [NSMutableIndexSet indexSetWithIndex:4];
|
|
|
|
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
|
|
|
|
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeMusicIsNotContent"];
|
|
|
|
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
|
|
|
|
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
|
|
|
|
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
|
|
|
|
[_recentResults setDelegate:self];
|
|
|
|
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
|
|
|
|
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
|
|
|
|
_problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
|
|
|
|
_deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
|
|
|
|
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
|
|
|
|
[_progressWindow setParentWindow:[_directoryPanel window]];
|
|
|
|
// Lazily loaded
|
|
|
|
_aboutBox = nil;
|
|
|
|
_preferencesPanel = nil;
|
|
|
|
_resultWindow = nil;
|
|
|
|
_detailsPanel = nil;
|
|
|
|
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Virtual */
|
|
|
|
|
|
|
|
- (PyDupeGuru *)model
|
|
|
|
{
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (DetailsPanel *)createDetailsPanel
|
|
|
|
{
|
|
|
|
NSInteger appMode = [self getAppMode];
|
|
|
|
if (appMode == AppModePicture) {
|
|
|
|
return [[DetailsPanelPicture alloc] initWithApp:model];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setScanOptions
|
|
|
|
{
|
|
|
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
|
|
|
NSString *scanTypeOptionName;
|
|
|
|
NSInteger appMode = [self getAppMode];
|
|
|
|
if (appMode == AppModePicture) {
|
|
|
|
scanTypeOptionName = @"scanTypePicture";
|
|
|
|
}
|
|
|
|
else if (appMode == AppModeMusic) {
|
|
|
|
scanTypeOptionName = @"scanTypeMusic";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
scanTypeOptionName = @"scanTypeStandard";
|
|
|
|
}
|
|
|
|
[model setScanType:n2i([ud objectForKey:scanTypeOptionName])];
|
|
|
|
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
|
|
|
|
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
|
|
|
|
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
|
|
|
|
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
|
|
|
|
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
|
2017-04-04 03:40:48 +00:00
|
|
|
NSInteger smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
|
|
|
|
NSInteger sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
|
2017-03-12 01:18:27 +00:00
|
|
|
[model setSizeThreshold:sizeThreshold];
|
|
|
|
[model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
|
|
|
|
[model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
|
|
|
|
[model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
|
|
|
|
[model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
|
|
|
|
[model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
|
|
|
|
[model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
|
|
|
|
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Public */
|
|
|
|
- (ResultWindow *)resultWindow
|
|
|
|
{
|
|
|
|
return _resultWindow;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (DirectoryPanel *)directoryPanel
|
|
|
|
{
|
|
|
|
return _directoryPanel;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (DetailsPanel *)detailsPanel
|
|
|
|
{
|
|
|
|
return _detailsPanel;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (HSRecentFiles *)recentResults
|
|
|
|
{
|
|
|
|
return _recentResults;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSInteger)getAppMode
|
|
|
|
{
|
|
|
|
return [model getAppMode];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setAppMode:(NSInteger)appMode
|
|
|
|
{
|
|
|
|
[model setAppMode:appMode];
|
|
|
|
if (_preferencesPanel != nil) {
|
|
|
|
[_preferencesPanel release];
|
|
|
|
_preferencesPanel = nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Actions */
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)clearPictureCache:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
|
|
|
|
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
|
|
|
|
return;
|
|
|
|
[model clearPictureCache];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)loadResults:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
NSOpenPanel *op = [NSOpenPanel openPanel];
|
|
|
|
[op setCanChooseFiles:YES];
|
|
|
|
[op setCanChooseDirectories:NO];
|
|
|
|
[op setCanCreateDirectories:NO];
|
|
|
|
[op setAllowsMultipleSelection:NO];
|
|
|
|
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
|
|
|
|
[op setTitle:NSLocalizedString(@"Select a results file to load", @"")];
|
2020-12-30 02:40:44 +00:00
|
|
|
if ([op runModal] == NSModalResponseOK) {
|
2017-03-12 01:18:27 +00:00
|
|
|
NSString *filename = [[[op URLs] objectAtIndex:0] path];
|
|
|
|
[model loadResultsFrom:filename];
|
|
|
|
[[self recentResults] addFile:filename];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)openWebsite:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
2017-04-04 03:40:48 +00:00
|
|
|
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.hardcoded.net/dupeguru/"]];
|
2017-03-12 01:18:27 +00:00
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)openHelp:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
NSBundle *b = [NSBundle mainBundle];
|
|
|
|
NSString *p = [b pathForResource:@"index" ofType:@"html" inDirectory:@"help"];
|
|
|
|
NSURL *u = [NSURL fileURLWithPath:p];
|
|
|
|
[[NSWorkspace sharedWorkspace] openURL:u];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)showAboutBox:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
if (_aboutBox == nil) {
|
|
|
|
_aboutBox = [[HSAboutBox alloc] initWithApp:model];
|
|
|
|
}
|
|
|
|
[[_aboutBox window] makeKeyAndOrderFront:nil];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)showDirectoryWindow:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
[[[self directoryPanel] window] makeKeyAndOrderFront:nil];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)showPreferencesPanel:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
if (_preferencesPanel == nil) {
|
2017-04-06 02:17:28 +00:00
|
|
|
NSString *nibName = @"Preferences";
|
2017-03-12 01:18:27 +00:00
|
|
|
NSInteger appMode = [model getAppMode];
|
|
|
|
if (appMode == AppModePicture) {
|
2017-04-06 02:17:28 +00:00
|
|
|
nibName = @"PreferencesPicture";
|
2017-03-12 01:18:27 +00:00
|
|
|
}
|
|
|
|
else if (appMode == AppModeMusic) {
|
2017-04-06 02:17:28 +00:00
|
|
|
nibName = @"PreferencesMusic";
|
2017-03-12 01:18:27 +00:00
|
|
|
}
|
2017-04-06 02:17:28 +00:00
|
|
|
_preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:nibName];
|
2017-03-12 01:18:27 +00:00
|
|
|
}
|
|
|
|
[_preferencesPanel showWindow:nil];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)showResultWindow:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)showIgnoreList:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
|
|
|
[model showIgnoreList];
|
|
|
|
}
|
|
|
|
|
2017-04-04 03:40:48 +00:00
|
|
|
- (IBAction)startScanning:(id)sender
|
2017-03-12 01:18:27 +00:00
|
|
|
{
|
2017-04-05 02:33:27 +00:00
|
|
|
[[self directoryPanel] startDuplicateScan:sender];
|
2017-03-12 01:18:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Delegate */
|
|
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
|
|
|
{
|
|
|
|
[model loadSession];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
|
|
|
{
|
|
|
|
if (![[[self directoryPanel] window] isVisible]) {
|
|
|
|
[[self directoryPanel] showWindow:NSApp];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
|
|
|
|
{
|
|
|
|
if ([model resultsAreModified]) {
|
|
|
|
NSString *msg = NSLocalizedString(@"You have unsaved results, do you really want to quit?", @"");
|
|
|
|
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) { // NO
|
|
|
|
return NSTerminateCancel;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return NSTerminateNow;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)applicationWillTerminate:(NSNotification *)aNotification
|
|
|
|
{
|
|
|
|
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
|
|
|
NSInteger sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"];
|
|
|
|
if (sc >= 10) {
|
|
|
|
sc = -1;
|
|
|
|
[model purgeIgnoreList];
|
|
|
|
}
|
|
|
|
sc++;
|
|
|
|
[model saveSession];
|
|
|
|
[ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"];
|
|
|
|
// NSApplication does not release nib instances objects, we must do it manually
|
|
|
|
// Well, it isn't needed because the memory is freed anyway (we are quitting the application
|
|
|
|
// But I need to release HSRecentFiles so it saves the user defaults
|
|
|
|
[_directoryPanel release];
|
|
|
|
[_recentResults release];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)recentFileClicked:(NSString *)path
|
|
|
|
{
|
|
|
|
[model loadResultsFrom:path];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* model --> view */
|
|
|
|
- (void)showMessage:(NSString *)msg
|
|
|
|
{
|
|
|
|
[Dialogs showMessage:msg];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)askYesNoWithPrompt:(NSString *)prompt
|
|
|
|
{
|
|
|
|
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)createResultsWindow
|
|
|
|
{
|
|
|
|
if (_resultWindow != nil) {
|
|
|
|
[_resultWindow release];
|
|
|
|
}
|
|
|
|
if (_detailsPanel != nil) {
|
|
|
|
[_detailsPanel release];
|
|
|
|
}
|
|
|
|
// Warning: creation order is important
|
|
|
|
// If the details panel is not created first and that there are some results in the model
|
|
|
|
// (happens if we load results), a dupe selection event triggers a details refresh in the
|
|
|
|
// core before we have the chance to initialize it, and then we crash.
|
|
|
|
_detailsPanel = [self createDetailsPanel];
|
|
|
|
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
|
|
|
|
}
|
|
|
|
- (void)showResultsWindow
|
|
|
|
{
|
|
|
|
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)showProblemDialog
|
|
|
|
{
|
|
|
|
[_problemDialog showWindow:self];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
|
|
|
|
{
|
|
|
|
NSOpenPanel *op = [NSOpenPanel openPanel];
|
|
|
|
[op setCanChooseFiles:NO];
|
|
|
|
[op setCanChooseDirectories:YES];
|
|
|
|
[op setCanCreateDirectories:YES];
|
|
|
|
[op setAllowsMultipleSelection:NO];
|
|
|
|
[op setTitle:prompt];
|
2020-12-30 02:40:44 +00:00
|
|
|
if ([op runModal] == NSModalResponseOK) {
|
2017-03-12 01:18:27 +00:00
|
|
|
return [[[op URLs] objectAtIndex:0] path];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)selectDestFileWithPrompt:(NSString *)prompt extension:(NSString *)extension
|
|
|
|
{
|
|
|
|
NSSavePanel *sp = [NSSavePanel savePanel];
|
|
|
|
[sp setCanCreateDirectories:YES];
|
|
|
|
[sp setAllowedFileTypes:[NSArray arrayWithObject:extension]];
|
|
|
|
[sp setTitle:prompt];
|
2020-12-30 02:40:44 +00:00
|
|
|
if ([sp runModal] == NSModalResponseOK) {
|
2017-03-12 01:18:27 +00:00
|
|
|
return [[sp URL] path];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|