diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index c2c6db303..7ce039fd6 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -449,6 +449,7 @@ ED20B87C28589274005FA6BE /* common_defs.h in Headers */ = {isa = PBXBuildFile; fileRef = ED20B87B28589274005FA6BE /* common_defs.h */; }; ED20B87F285892C5005FA6BE /* crc32_multipliers.h in Headers */ = {isa = PBXBuildFile; fileRef = ED20B87D285892C5005FA6BE /* crc32_multipliers.h */; }; ED20B880285892C5005FA6BE /* crc32_tables.h in Headers */ = {isa = PBXBuildFile; fileRef = ED20B87E285892C5005FA6BE /* crc32_tables.h */; }; + ED86936F2ADAE34D00342B1A /* DefaultAppHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED86936E2ADAE34D00342B1A /* DefaultAppHelper.mm */; }; ED8A163F2735A8AA000D61F9 /* peer-mgr-active-requests.h in Headers */ = {isa = PBXBuildFile; fileRef = ED8A163B2735A8AA000D61F9 /* peer-mgr-active-requests.h */; }; ED8A16402735A8AA000D61F9 /* peer-mgr-active-requests.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED8A163C2735A8AA000D61F9 /* peer-mgr-active-requests.cc */; }; ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */ = {isa = PBXBuildFile; fileRef = ED8A163D2735A8AA000D61F9 /* peer-mgr-wishlist.h */; }; @@ -1236,6 +1237,8 @@ ED20B87B28589274005FA6BE /* common_defs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = common_defs.h; sourceTree = ""; }; ED20B87D285892C5005FA6BE /* crc32_multipliers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = crc32_multipliers.h; path = lib/crc32_multipliers.h; sourceTree = ""; }; ED20B87E285892C5005FA6BE /* crc32_tables.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = crc32_tables.h; path = lib/crc32_tables.h; sourceTree = ""; }; + ED86936D2ADAE34D00342B1A /* DefaultAppHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DefaultAppHelper.h; sourceTree = ""; }; + ED86936E2ADAE34D00342B1A /* DefaultAppHelper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DefaultAppHelper.mm; sourceTree = ""; }; ED8A163B2735A8AA000D61F9 /* peer-mgr-active-requests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "peer-mgr-active-requests.h"; sourceTree = ""; }; ED8A163C2735A8AA000D61F9 /* peer-mgr-active-requests.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "peer-mgr-active-requests.cc"; sourceTree = ""; }; ED8A163D2735A8AA000D61F9 /* peer-mgr-wishlist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "peer-mgr-wishlist.h"; sourceTree = ""; }; @@ -1537,6 +1540,8 @@ A222E9860E6B21D9009FB003 /* BlocklistDownloaderViewController.mm */, A222EA790E6C32C4009FB003 /* BlocklistScheduler.h */, A222EA7A0E6C32C4009FB003 /* BlocklistScheduler.mm */, + ED86936D2ADAE34D00342B1A /* DefaultAppHelper.h */, + ED86936E2ADAE34D00342B1A /* DefaultAppHelper.mm */, A2AB883916A399A6008FAD50 /* VDKQueue */, ); name = Sources; @@ -3078,6 +3083,7 @@ buildActionMask = 2147483647; files = ( C86BCD9928228A9600F45599 /* SparkleProxy.mm in Sources */, + ED86936F2ADAE34D00342B1A /* DefaultAppHelper.mm in Sources */, A225A4C0187E369C00CDE823 /* ShareToolbarItem.mm in Sources */, A2D77453154CC72B00A62B93 /* WebSeedTableView.mm in Sources */, 8D11072D0486CEB800E47090 /* main.mm in Sources */, diff --git a/macosx/CMakeLists.txt b/macosx/CMakeLists.txt index a855d2161..f1f85f7b1 100644 --- a/macosx/CMakeLists.txt +++ b/macosx/CMakeLists.txt @@ -62,6 +62,8 @@ target_sources(${TR_NAME}-mac Controller.mm CreatorWindowController.h CreatorWindowController.mm + DefaultAppHelper.h + DefaultAppHelper.mm DragOverlayView.h DragOverlayView.mm DragOverlayWindow.h diff --git a/macosx/DefaultAppHelper.h b/macosx/DefaultAppHelper.h new file mode 100644 index 000000000..99c11acc8 --- /dev/null +++ b/macosx/DefaultAppHelper.h @@ -0,0 +1,15 @@ +// This file Copyright © 2007-2023 Transmission authors and contributors. +// It may be used under the MIT (SPDX: MIT) license. +// License text can be found in the licenses/ folder. + +#import + +@interface DefaultAppHelper : NSObject + +- (BOOL)isDefaultForTorrentFiles; +- (void)setDefaultForTorrentFiles:(void (^_Nullable)())completionHandler; + +- (BOOL)isDefaultForMagnetURLs; +- (void)setDefaultForMagnetURLs:(void (^_Nullable)())completionHandler; + +@end diff --git a/macosx/DefaultAppHelper.mm b/macosx/DefaultAppHelper.mm new file mode 100644 index 000000000..7653ecf64 --- /dev/null +++ b/macosx/DefaultAppHelper.mm @@ -0,0 +1,183 @@ +// This file Copyright © 2007-2023 Transmission authors and contributors. +// It may be used under the MIT (SPDX: MIT) license. +// License text can be found in the licenses/ folder. + +#import "DefaultAppHelper.h" + +#import +#import + +static NSString* const kMagnetURLScheme = @"magnet"; +static NSString* const kTorrentFileType = @"org.bittorrent.torrent"; + +UTType* GetTorrentFileType(void) API_AVAILABLE(macos(11.0)) +{ + static UTType* result = nil; + + static dispatch_once_t once; + dispatch_once(&once, ^{ + result = [UTType exportedTypeWithIdentifier:kTorrentFileType conformingToType:UTTypeData]; + }); + + return result; +} + +@interface DefaultAppHelper () + +@property(nonatomic, readonly) NSString* bundleIdentifier; + +@end + +@implementation DefaultAppHelper + +- (instancetype)init +{ + if (self = [super init]) + { + _bundleIdentifier = NSBundle.mainBundle.bundleIdentifier; + } + return self; +} + +- (BOOL)isDefaultForTorrentFiles +{ + if (@available(macOS 12, *)) + { + UTType* fileType = GetTorrentFileType(); + NSURL* appUrl = [NSWorkspace.sharedWorkspace URLForApplicationToOpenContentType:fileType]; + if (!appUrl) + { + return NO; + } + + NSString* bundleId = [NSBundle bundleWithURL:appUrl].bundleIdentifier; + + if ([self.bundleIdentifier isEqualToString:bundleId]) + { + return YES; + } + } + else + { + NSString* bundleId = (__bridge NSString*)LSCopyDefaultRoleHandlerForContentType((__bridge CFStringRef)kTorrentFileType, kLSRolesViewer); + if (!bundleId) + { + return NO; + } + + if ([self.bundleIdentifier isEqualToString:bundleId]) + { + return YES; + } + } + + return NO; +} + +- (void)setDefaultForTorrentFiles:(void (^_Nullable)())completionHandler +{ + if (@available(macOS 12, *)) + { + UTType* fileType = GetTorrentFileType(); + NSURL* appUrl = [NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:self.bundleIdentifier]; + [NSWorkspace.sharedWorkspace setDefaultApplicationAtURL:appUrl toOpenContentType:fileType completionHandler:^(NSError* error) { + if (error) + { + NSLog(@"Failed setting default torrent file handler: %@", error.localizedDescription); + } + if (completionHandler != nil) + { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(); + }); + } + }]; + } + else + { + OSStatus const result = LSSetDefaultRoleHandlerForContentType( + (__bridge CFStringRef)kTorrentFileType, + kLSRolesViewer, + (__bridge CFStringRef)self.bundleIdentifier); + if (result != noErr) + { + NSLog(@"Failed setting default torrent file handler"); + } + if (completionHandler != nil) + { + completionHandler(); + } + } +} + +- (BOOL)isDefaultForMagnetURLs +{ + if (@available(macOS 12, *)) + { + NSURL* schemeUrl = [NSURL URLWithString:[kMagnetURLScheme stringByAppendingString:@":"]]; + NSURL* appUrl = [NSWorkspace.sharedWorkspace URLForApplicationToOpenURL:schemeUrl]; + if (!appUrl) + { + return NO; + } + + NSString* bundleId = [NSBundle bundleWithURL:appUrl].bundleIdentifier; + + if ([self.bundleIdentifier isEqualToString:bundleId]) + { + return YES; + } + } + else + { + NSString* bundleId = (__bridge NSString*)LSCopyDefaultHandlerForURLScheme((__bridge CFStringRef)kMagnetURLScheme); + if (!bundleId) + { + return NO; + } + + if ([self.bundleIdentifier isEqualToString:bundleId]) + { + return YES; + } + } + + return NO; +} + +- (void)setDefaultForMagnetURLs:(void (^_Nullable)())completionHandler +{ + if (@available(macOS 12, *)) + { + NSURL* appUrl = [NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:self.bundleIdentifier]; + [NSWorkspace.sharedWorkspace setDefaultApplicationAtURL:appUrl toOpenURLsWithScheme:kMagnetURLScheme + completionHandler:^(NSError* error) { + if (error) + { + NSLog(@"Failed setting default magnet link handler: %@", error.localizedDescription); + } + if (completionHandler != nil) + { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(); + }); + } + }]; + } + else + { + OSStatus const result = LSSetDefaultHandlerForURLScheme( + (__bridge CFStringRef)kMagnetURLScheme, + (__bridge CFStringRef)self.bundleIdentifier); + if (result != noErr) + { + NSLog(@"Failed setting default magnet link handler"); + } + if (completionHandler != nil) + { + completionHandler(); + } + } +} + +@end diff --git a/macosx/PrefsController.mm b/macosx/PrefsController.mm index b310f0bbb..db5b030dd 100644 --- a/macosx/PrefsController.mm +++ b/macosx/PrefsController.mm @@ -11,6 +11,7 @@ #import "BlocklistDownloaderViewController.h" #import "BlocklistScheduler.h" #import "Controller.h" +#import "DefaultAppHelper.h" #import "PortChecker.h" #import "BonjourController.h" #import "NSImageAdditions.h" @@ -103,6 +104,7 @@ static NSString* const kWebUIURLFormat = @"http://localhost:%ld/"; @property(nonatomic, readonly) NSMutableArray* fRPCWhitelistArray; @property(nonatomic) IBOutlet NSSegmentedControl* fRPCAddRemoveControl; @property(nonatomic, copy) NSString* fRPCPassword; +@property(nonatomic, readonly) DefaultAppHelper* fDefaultAppHelper; @end @@ -177,6 +179,8 @@ static NSString* const kWebUIURLFormat = @"http://localhost:%ld/"; } [self setAutoUpdateToBeta:nil]; + + _fDefaultAppHelper = [[DefaultAppHelper alloc] init]; } return self; @@ -215,7 +219,7 @@ static NSString* const kWebUIURLFormat = @"http://localhost:%ld/"; [self setPrefView:nil]; - [self updateDefaultsStatus]; + [self updateDefaultsStates]; //set special-handling of magnet link add window checkbox [self updateShowAddMagnetWindowField]; @@ -859,55 +863,27 @@ static NSString* const kWebUIURLFormat = @"http://localhost:%ld/"; - (IBAction)setDefaultForMagnets:(id)sender { - NSString* bundleID = NSBundle.mainBundle.bundleIdentifier; - OSStatus const result = LSSetDefaultHandlerForURLScheme((CFStringRef) @"magnet", (__bridge CFStringRef)bundleID); - if (result != noErr) - { - NSLog(@"Failed setting default magnet link handler"); - } - [self updateDefaultsStatus]; + PrefsController* __weak weakSelf = self; + [self.fDefaultAppHelper setDefaultForMagnetURLs:^{ + [weakSelf updateDefaultsStates]; + }]; } - (IBAction)setDefaultForTorrentFiles:(id)sender { - NSString* bundleID = NSBundle.mainBundle.bundleIdentifier; - OSStatus const result = LSSetDefaultRoleHandlerForContentType((CFStringRef) @"org.bittorrent.torrent", kLSRolesViewer, (__bridge CFStringRef)bundleID); - if (result != noErr) - { - NSLog(@"Failed setting default torrent file handler"); - } - [self updateDefaultsStatus]; + PrefsController* __weak weakSelf = self; + [self.fDefaultAppHelper setDefaultForTorrentFiles:^{ + [weakSelf updateDefaultsStates]; + }]; } -- (void)updateDefaultsStatus +- (void)updateDefaultsStates { - BOOL isDefaultForMagnetSet = NO; - BOOL isDefaultForTorrentSet = NO; + BOOL const isDefaultForMagnetURLs = [self.fDefaultAppHelper isDefaultForMagnetURLs]; + self.fSetDefaultForMagnetButton.enabled = !isDefaultForMagnetURLs; - NSString* bundleID = NSBundle.mainBundle.bundleIdentifier; - - NSString* const defaultBundleIdForMagnet = (__bridge NSString*)LSCopyDefaultHandlerForURLScheme((CFStringRef) @"magnet"); - if (defaultBundleIdForMagnet) - { - if ([bundleID isEqualToString:defaultBundleIdForMagnet]) - { - isDefaultForMagnetSet = YES; - } - } - - NSString* const defaultBundleIdForTorrent = (__bridge NSString*)LSCopyDefaultRoleHandlerForContentType( - (CFStringRef) @"org.bittorrent.torrent", - kLSRolesViewer); - if (defaultBundleIdForTorrent) - { - if ([bundleID isEqualToString:defaultBundleIdForTorrent]) - { - isDefaultForTorrentSet = YES; - } - } - - self.fSetDefaultForMagnetButton.enabled = !isDefaultForMagnetSet; - self.fSetDefaultForTorrentButton.enabled = !isDefaultForTorrentSet; + BOOL const isDefaultForTorrentFiles = [self.fDefaultAppHelper isDefaultForTorrentFiles]; + self.fSetDefaultForTorrentButton.enabled = !isDefaultForTorrentFiles; } - (void)setQueue:(id)sender