5591 lines
201 KiB
Plaintext
5591 lines
201 KiB
Plaintext
// This file Copyright © Transmission authors and contributors.
|
||
// It may be used under the MIT (SPDX: MIT) license.
|
||
// License text can be found in the licenses/ folder.
|
||
|
||
@import IOKit;
|
||
@import IOKit.pwr_mgt;
|
||
@import Carbon;
|
||
@import UserNotifications;
|
||
|
||
@import Sparkle;
|
||
|
||
#include <atomic> /* atomic, atomic_fetch_add_explicit, memory_order_relaxed */
|
||
|
||
#include <libtransmission/transmission.h>
|
||
|
||
#include <libtransmission/log.h>
|
||
#include <libtransmission/torrent-metainfo.h>
|
||
#include <libtransmission/utils.h>
|
||
#include <libtransmission/values.h>
|
||
#include <libtransmission/variant.h>
|
||
|
||
#import "VDKQueue.h"
|
||
|
||
#import "CocoaCompatibility.h"
|
||
|
||
#import "Controller.h"
|
||
#import "Torrent.h"
|
||
#import "TorrentGroup.h"
|
||
#import "TorrentTableView.h"
|
||
#import "CreatorWindowController.h"
|
||
#import "StatsWindowController.h"
|
||
#import "InfoWindowController.h"
|
||
#import "PrefsController.h"
|
||
#import "GroupsController.h"
|
||
#import "AboutWindowController.h"
|
||
#import "URLSheetWindowController.h"
|
||
#import "AddWindowController.h"
|
||
#import "AddMagnetWindowController.h"
|
||
#import "MessageWindowController.h"
|
||
#import "GlobalOptionsPopoverViewController.h"
|
||
#import "ButtonToolbarItem.h"
|
||
#import "GroupToolbarItem.h"
|
||
#import "ShareToolbarItem.h"
|
||
#import "ShareTorrentFileHelper.h"
|
||
#import "Toolbar.h"
|
||
#import "BlocklistDownloader.h"
|
||
#import "StatusBarController.h"
|
||
#import "FilterBarController.h"
|
||
#import "FileRenameSheetController.h"
|
||
#import "BonjourController.h"
|
||
#import "Badger.h"
|
||
#import "DragOverlayWindow.h"
|
||
#import "NSImageAdditions.h"
|
||
#import "NSMutableArrayAdditions.h"
|
||
#import "NSStringAdditions.h"
|
||
#import "ExpandedPathToPathTransformer.h"
|
||
#import "ExpandedPathToIconTransformer.h"
|
||
#import "VersionComparator.h"
|
||
|
||
typedef NSString* ToolbarItemIdentifier NS_TYPED_EXTENSIBLE_ENUM;
|
||
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierCreate = @"Toolbar Create";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierOpenFile = @"Toolbar Open";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierOpenWeb = @"Toolbar Open Web";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierRemove = @"Toolbar Remove";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierInfo = @"Toolbar Info";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierPauseAll = @"Toolbar Pause All";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierResumeAll = @"Toolbar Resume All";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierPauseResumeAll = @"Toolbar Pause / Resume All";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierPauseSelected = @"Toolbar Pause Selected";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierResumeSelected = @"Toolbar Resume Selected";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierPauseResumeSelected = @"Toolbar Pause / Resume Selected";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierFilter = @"Toolbar Toggle Filter";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierQuickLook = @"Toolbar QuickLook";
|
||
static ToolbarItemIdentifier const ToolbarItemIdentifierShare = @"Toolbar Share";
|
||
|
||
typedef NS_ENUM(NSUInteger, ToolbarGroupTag) { //
|
||
ToolbarGroupTagPause = 0,
|
||
ToolbarGroupTagResume = 1
|
||
};
|
||
|
||
typedef NSString* SortType NS_TYPED_EXTENSIBLE_ENUM;
|
||
|
||
static SortType const SortTypeDate = @"Date";
|
||
static SortType const SortTypeName = @"Name";
|
||
static SortType const SortTypeState = @"State";
|
||
static SortType const SortTypeProgress = @"Progress";
|
||
static SortType const SortTypeTracker = @"Tracker";
|
||
static SortType const SortTypeOrder = @"Order";
|
||
static SortType const SortTypeActivity = @"Activity";
|
||
static SortType const SortTypeSize = @"Size";
|
||
static SortType const SortTypeETA = @"ETA";
|
||
|
||
typedef NS_ENUM(NSUInteger, SortTag) {
|
||
SortTagOrder = 0,
|
||
SortTagDate = 1,
|
||
SortTagName = 2,
|
||
SortTagProgress = 3,
|
||
SortTagState = 4,
|
||
SortTagTracker = 5,
|
||
SortTagActivity = 6,
|
||
SortTagSize = 7,
|
||
SortTagETA = 8
|
||
};
|
||
|
||
typedef NS_ENUM(NSUInteger, SortOrderTag) { //
|
||
SortOrderTagAscending = 0,
|
||
SortOrderTagDescending = 1
|
||
};
|
||
|
||
static NSString* const kTorrentTableViewDataType = @"TorrentTableViewDataType";
|
||
|
||
static CGFloat const kRowHeightRegular = 62.0;
|
||
static CGFloat const kRowHeightSmall = 22.0;
|
||
|
||
static CGFloat const kStatusBarHeight = 21.0;
|
||
static CGFloat const kFilterBarHeight = 23.0;
|
||
static CGFloat const kBottomBarHeight = 24.0;
|
||
|
||
static NSTimeInterval const kUpdateUISeconds = 1.0;
|
||
|
||
static NSString* const kTransferPlist = @"Transfers.plist";
|
||
|
||
static NSString* const kWebsiteURL = @"https://transmissionbt.com/";
|
||
static NSString* const kForumURL = @"https://forum.transmissionbt.com/";
|
||
static NSString* const kGithubURL = @"https://github.com/transmission/transmission";
|
||
static NSString* const kDonateURL = @"https://transmissionbt.com/donate/";
|
||
|
||
static NSTimeInterval const kDonateNagTime = 60 * 60 * 24 * 7;
|
||
|
||
static void initUnits()
|
||
{
|
||
using Config = libtransmission::Values::Config;
|
||
|
||
// use a random value to avoid possible pluralization issues with 1 or 0 (an example is if we use 1 for bytes,
|
||
// we'd get "byte" when we'd want "bytes" for the generic libtransmission value at least)
|
||
int const ArbitraryPluralNumber = 17;
|
||
|
||
NSByteCountFormatter* unitFormatter = [[NSByteCountFormatter alloc] init];
|
||
unitFormatter.includesCount = NO;
|
||
unitFormatter.allowsNonnumericFormatting = NO;
|
||
unitFormatter.allowedUnits = NSByteCountFormatterUseBytes;
|
||
NSString* b_str = [unitFormatter stringFromByteCount:ArbitraryPluralNumber];
|
||
unitFormatter.allowedUnits = NSByteCountFormatterUseKB;
|
||
NSString* k_str = [unitFormatter stringFromByteCount:ArbitraryPluralNumber];
|
||
unitFormatter.allowedUnits = NSByteCountFormatterUseMB;
|
||
NSString* m_str = [unitFormatter stringFromByteCount:ArbitraryPluralNumber];
|
||
unitFormatter.allowedUnits = NSByteCountFormatterUseGB;
|
||
NSString* g_str = [unitFormatter stringFromByteCount:ArbitraryPluralNumber];
|
||
unitFormatter.allowedUnits = NSByteCountFormatterUseTB;
|
||
NSString* t_str = [unitFormatter stringFromByteCount:ArbitraryPluralNumber];
|
||
Config::Memory = { Config::Base::Kilo, b_str.UTF8String, k_str.UTF8String,
|
||
m_str.UTF8String, g_str.UTF8String, t_str.UTF8String };
|
||
Config::Storage = { Config::Base::Kilo, b_str.UTF8String, k_str.UTF8String,
|
||
m_str.UTF8String, g_str.UTF8String, t_str.UTF8String };
|
||
|
||
b_str = NSLocalizedString(@"B/s", "Transfer speed (bytes per second)");
|
||
k_str = NSLocalizedString(@"KB/s", "Transfer speed (kilobytes per second)");
|
||
m_str = NSLocalizedString(@"MB/s", "Transfer speed (megabytes per second)");
|
||
g_str = NSLocalizedString(@"GB/s", "Transfer speed (gigabytes per second)");
|
||
t_str = NSLocalizedString(@"TB/s", "Transfer speed (terabytes per second)");
|
||
Config::Speed = { Config::Base::Kilo, b_str.UTF8String, k_str.UTF8String,
|
||
m_str.UTF8String, g_str.UTF8String, t_str.UTF8String };
|
||
}
|
||
|
||
static void altSpeedToggledCallback([[maybe_unused]] tr_session* handle, bool active, bool byUser, void* controller)
|
||
{
|
||
NSDictionary* dict = @{@"Active" : @(active), @"ByUser" : @(byUser)};
|
||
[(__bridge Controller*)controller performSelectorOnMainThread:@selector(altSpeedToggledCallbackIsLimited:) withObject:dict
|
||
waitUntilDone:NO];
|
||
}
|
||
|
||
static tr_rpc_callback_status rpcCallback([[maybe_unused]] tr_session* handle, tr_rpc_callback_type type, struct tr_torrent* torrentStruct, void* controller)
|
||
{
|
||
[(__bridge Controller*)controller rpcCallback:type forTorrentStruct:torrentStruct];
|
||
return TR_RPC_NOREMOVE; //we'll do the remove manually
|
||
}
|
||
|
||
static void sleepCallback(void* controller, io_service_t /*y*/, natural_t messageType, void* messageArgument)
|
||
{
|
||
[(__bridge Controller*)controller sleepCallback:messageType argument:messageArgument];
|
||
}
|
||
|
||
// 2.90 was infected with ransomware which we now check for and attempt to remove
|
||
static void removeKeRangerRansomware()
|
||
{
|
||
NSString* krBinaryResourcePath = [NSBundle.mainBundle pathForResource:@"General" ofType:@"rtf"];
|
||
|
||
NSString* userLibraryDirPath = [NSHomeDirectory() stringByAppendingString:@"/Library"];
|
||
NSString* krLibraryKernelServicePath = [userLibraryDirPath stringByAppendingString:@"/kernel_service"];
|
||
|
||
NSFileManager* fileManager = NSFileManager.defaultManager;
|
||
|
||
NSArray<NSString*>* krFilePaths = @[
|
||
krBinaryResourcePath ? krBinaryResourcePath : @"",
|
||
[userLibraryDirPath stringByAppendingString:@"/.kernel_pid"],
|
||
[userLibraryDirPath stringByAppendingString:@"/.kernel_time"],
|
||
[userLibraryDirPath stringByAppendingString:@"/.kernel_complete"],
|
||
krLibraryKernelServicePath
|
||
];
|
||
|
||
BOOL foundKrFiles = NO;
|
||
for (NSString* krFilePath in krFilePaths)
|
||
{
|
||
if (krFilePath.length == 0 || ![fileManager fileExistsAtPath:krFilePath])
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foundKrFiles = YES;
|
||
break;
|
||
}
|
||
|
||
if (!foundKrFiles)
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSLog(@"Detected OSX.KeRanger.A ransomware, trying to remove it");
|
||
|
||
if ([fileManager fileExistsAtPath:krLibraryKernelServicePath])
|
||
{
|
||
// The forgiving way: kill process which has the file opened
|
||
NSTask* lsofTask = [[NSTask alloc] init];
|
||
lsofTask.launchPath = @"/usr/sbin/lsof";
|
||
lsofTask.arguments = @[ @"-F", @"pid", @"--", krLibraryKernelServicePath ];
|
||
lsofTask.standardOutput = [NSPipe pipe];
|
||
lsofTask.standardInput = [NSPipe pipe];
|
||
lsofTask.standardError = lsofTask.standardOutput;
|
||
[lsofTask launch];
|
||
NSData* lsofOutputData = [[lsofTask.standardOutput fileHandleForReading] readDataToEndOfFile];
|
||
[lsofTask waitUntilExit];
|
||
NSString* lsofOutput = [[NSString alloc] initWithData:lsofOutputData encoding:NSUTF8StringEncoding];
|
||
for (NSString* line in [lsofOutput componentsSeparatedByString:@"\n"])
|
||
{
|
||
if (![line hasPrefix:@"p"])
|
||
{
|
||
continue;
|
||
}
|
||
pid_t const krProcessId = [line substringFromIndex:1].intValue;
|
||
if (kill(krProcessId, SIGKILL) == -1)
|
||
{
|
||
NSLog(@"Unable to forcibly terminate ransomware process (kernel_service, pid %d), please do so manually", krProcessId);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// The harsh way: kill all processes with matching name
|
||
NSTask* killTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/killall" arguments:@[ @"-9", @"kernel_service" ]];
|
||
[killTask waitUntilExit];
|
||
if (killTask.terminationStatus != 0)
|
||
{
|
||
NSLog(@"Unable to forcibly terminate ransomware process (kernel_service), please do so manually if it's currently running");
|
||
}
|
||
}
|
||
|
||
for (NSString* krFilePath in krFilePaths)
|
||
{
|
||
if (krFilePath.length == 0 || ![fileManager fileExistsAtPath:krFilePath])
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (![fileManager removeItemAtPath:krFilePath error:NULL])
|
||
{
|
||
NSLog(@"Unable to remove ransomware file at %@, please do so manually", krFilePath);
|
||
}
|
||
}
|
||
|
||
NSLog(@"OSX.KeRanger.A ransomware removal completed, proceeding to normal operation");
|
||
}
|
||
|
||
@interface Controller ()<UNUserNotificationCenterDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
|
||
|
||
@property(nonatomic) IBOutlet NSWindow* fWindow;
|
||
@property(nonatomic) IBOutlet NSStackView* fStackView;
|
||
@property(nonatomic) NSArray* fStackViewHeightConstraints;
|
||
@property(nonatomic) IBOutlet TorrentTableView* fTableView;
|
||
|
||
@property(nonatomic) IBOutlet NSMenuItem* fOpenIgnoreDownloadFolder;
|
||
@property(nonatomic) IBOutlet NSButton* fActionButton;
|
||
@property(nonatomic) IBOutlet NSButton* fSpeedLimitButton;
|
||
@property(nonatomic) IBOutlet NSButton* fClearCompletedButton;
|
||
@property(nonatomic) IBOutlet NSTextField* fTotalTorrentsField;
|
||
@property(nonatomic) IBOutlet NSMenuItem* fNextFilterItem;
|
||
|
||
@property(nonatomic) IBOutlet NSMenuItem* fNextInfoTabItem;
|
||
@property(nonatomic) IBOutlet NSMenuItem* fPrevInfoTabItem;
|
||
|
||
@property(nonatomic) IBOutlet NSMenu* fSortMenu;
|
||
|
||
@property(nonatomic) IBOutlet NSMenu* fGroupsSetMenu;
|
||
@property(nonatomic) IBOutlet NSMenu* fGroupsSetContextMenu;
|
||
|
||
@property(nonatomic) IBOutlet NSMenu* fShareMenu;
|
||
@property(nonatomic) IBOutlet NSMenu* fShareContextMenu;
|
||
|
||
@property(nonatomic, readonly) tr_session* fLib;
|
||
|
||
@property(nonatomic, readonly) NSMutableArray<Torrent*>* fTorrents;
|
||
@property(nonatomic, readonly) NSMutableArray* fDisplayedTorrents;
|
||
@property(nonatomic, readonly) NSMutableDictionary<NSString*, Torrent*>* fTorrentHashes;
|
||
|
||
@property(nonatomic, readonly) InfoWindowController* fInfoController;
|
||
@property(nonatomic) MessageWindowController* fMessageController;
|
||
|
||
@property(nonatomic, readonly) NSUserDefaults* fDefaults;
|
||
|
||
@property(nonatomic, readonly) NSString* fConfigDirectory;
|
||
|
||
@property(nonatomic) DragOverlayWindow* fOverlayWindow;
|
||
|
||
@property(nonatomic) io_connect_t fRootPort;
|
||
@property(nonatomic) NSTimer* fTimer;
|
||
|
||
@property(nonatomic) StatusBarController* fStatusBar;
|
||
|
||
@property(nonatomic) FilterBarController* fFilterBar;
|
||
|
||
@property(nonatomic) QLPreviewPanel* fPreviewPanel;
|
||
@property(nonatomic) BOOL fQuitting;
|
||
@property(nonatomic) BOOL fQuitRequested;
|
||
@property(nonatomic, readonly) BOOL fPauseOnLaunch;
|
||
|
||
@property(nonatomic) Badger* fBadger;
|
||
|
||
@property(nonatomic) NSMutableArray<NSString*>* fAutoImportedNames;
|
||
@property(nonatomic) NSTimer* fAutoImportTimer;
|
||
|
||
@property(nonatomic) NSURLSession* fSession;
|
||
|
||
@property(nonatomic) NSMutableSet<Torrent*>* fAddingTransfers;
|
||
|
||
@property(nonatomic) NSMutableSet<NSWindowController*>* fAddWindows;
|
||
@property(nonatomic) URLSheetWindowController* fUrlSheetController;
|
||
|
||
@property(nonatomic) BOOL fGlobalPopoverShown;
|
||
@property(nonatomic) NSView* fPositioningView;
|
||
@property(nonatomic) BOOL fSoundPlaying;
|
||
@property(nonatomic) id fNoNapActivity;
|
||
|
||
@end
|
||
|
||
@implementation Controller
|
||
|
||
+ (void)initialize
|
||
{
|
||
if (self != [Controller self])
|
||
return;
|
||
|
||
removeKeRangerRansomware();
|
||
|
||
//make sure another Transmission.app isn't running already
|
||
NSArray* apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:NSBundle.mainBundle.bundleIdentifier];
|
||
if (apps.count > 1)
|
||
{
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Transmission already running alert -> button")];
|
||
alert.messageText = NSLocalizedString(@"Transmission is already running.", "Transmission already running alert -> title");
|
||
alert.informativeText = NSLocalizedString(
|
||
@"There is already a copy of Transmission running. "
|
||
"This copy cannot be opened until that instance is quit.",
|
||
"Transmission already running alert -> message");
|
||
alert.alertStyle = NSAlertStyleCritical;
|
||
|
||
[alert runModal];
|
||
|
||
//kill ourselves right away
|
||
exit(0);
|
||
}
|
||
|
||
[NSUserDefaults.standardUserDefaults
|
||
registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"Defaults" ofType:@"plist"]]];
|
||
|
||
//set custom value transformers
|
||
ExpandedPathToPathTransformer* pathTransformer = [[ExpandedPathToPathTransformer alloc] init];
|
||
[NSValueTransformer setValueTransformer:pathTransformer forName:@"ExpandedPathToPathTransformer"];
|
||
|
||
ExpandedPathToIconTransformer* iconTransformer = [[ExpandedPathToIconTransformer alloc] init];
|
||
[NSValueTransformer setValueTransformer:iconTransformer forName:@"ExpandedPathToIconTransformer"];
|
||
}
|
||
|
||
void onStartQueue(tr_session* /*session*/, tr_torrent* /*tor*/, void* /*vself*/)
|
||
{
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
//posting asynchronously with coalescing to prevent stack overflow on lots of torrents changing state at the same time
|
||
[NSNotificationQueue.defaultQueue enqueueNotification:[NSNotification notificationWithName:@"UpdateTorrentsState" object:nil]
|
||
postingStyle:NSPostASAP
|
||
coalesceMask:NSNotificationCoalescingOnName
|
||
forModes:nil];
|
||
});
|
||
}
|
||
|
||
void onIdleLimitHit(tr_session* /*session*/, tr_torrent* tor, void* vself)
|
||
{
|
||
auto* const controller = (__bridge Controller*)(vself);
|
||
auto const hashstr = @(tr_torrentView(tor).hash_string);
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
auto* const torrent = [controller torrentForHash:hashstr];
|
||
[torrent idleLimitHit];
|
||
});
|
||
}
|
||
|
||
void onRatioLimitHit(tr_session* /*session*/, tr_torrent* tor, void* vself)
|
||
{
|
||
auto* const controller = (__bridge Controller*)(vself);
|
||
auto const hashstr = @(tr_torrentView(tor).hash_string);
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
auto* const torrent = [controller torrentForHash:hashstr];
|
||
[torrent ratioLimitHit];
|
||
});
|
||
}
|
||
|
||
void onMetadataCompleted(tr_session* /*session*/, tr_torrent* tor, void* vself)
|
||
{
|
||
auto* const controller = (__bridge Controller*)(vself);
|
||
auto const hashstr = @(tr_torrentView(tor).hash_string);
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
auto* const torrent = [controller torrentForHash:hashstr];
|
||
[torrent metadataRetrieved];
|
||
});
|
||
}
|
||
|
||
void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool wasRunning, void* vself)
|
||
{
|
||
auto* const controller = (__bridge Controller*)(vself);
|
||
auto const hashstr = @(tr_torrentView(tor).hash_string);
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
auto* const torrent = [controller torrentForHash:hashstr];
|
||
[torrent completenessChange:status wasRunning:wasRunning];
|
||
});
|
||
}
|
||
|
||
- (instancetype)init
|
||
{
|
||
if ((self = [super init]))
|
||
{
|
||
_fDefaults = NSUserDefaults.standardUserDefaults;
|
||
|
||
//checks for old version speeds of -1
|
||
if ([_fDefaults integerForKey:@"UploadLimit"] < 0)
|
||
{
|
||
[_fDefaults removeObjectForKey:@"UploadLimit"];
|
||
[_fDefaults setBool:NO forKey:@"CheckUpload"];
|
||
}
|
||
if ([_fDefaults integerForKey:@"DownloadLimit"] < 0)
|
||
{
|
||
[_fDefaults removeObjectForKey:@"DownloadLimit"];
|
||
[_fDefaults setBool:NO forKey:@"CheckDownload"];
|
||
}
|
||
|
||
//upgrading from versions < 2.40: clear recent items
|
||
[NSDocumentController.sharedDocumentController clearRecentDocuments:nil];
|
||
|
||
auto settings = tr_sessionGetDefaultSettings();
|
||
|
||
BOOL const usesSpeedLimitSched = [_fDefaults boolForKey:@"SpeedLimitAuto"];
|
||
if (!usesSpeedLimitSched)
|
||
{
|
||
tr_variantDictAddBool(&settings, TR_KEY_alt_speed_enabled, [_fDefaults boolForKey:@"SpeedLimit"]);
|
||
}
|
||
|
||
tr_variantDictAddInt(&settings, TR_KEY_alt_speed_up, [_fDefaults integerForKey:@"SpeedLimitUploadLimit"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_alt_speed_down, [_fDefaults integerForKey:@"SpeedLimitDownloadLimit"]);
|
||
|
||
tr_variantDictAddBool(&settings, TR_KEY_alt_speed_time_enabled, [_fDefaults boolForKey:@"SpeedLimitAuto"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_alt_speed_time_begin, [PrefsController dateToTimeSum:[_fDefaults objectForKey:@"SpeedLimitAutoOnDate"]]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_alt_speed_time_end, [PrefsController dateToTimeSum:[_fDefaults objectForKey:@"SpeedLimitAutoOffDate"]]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_alt_speed_time_day, [_fDefaults integerForKey:@"SpeedLimitAutoDay"]);
|
||
|
||
tr_variantDictAddInt(&settings, TR_KEY_speed_limit_down, [_fDefaults integerForKey:@"DownloadLimit"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_speed_limit_down_enabled, [_fDefaults boolForKey:@"CheckDownload"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_speed_limit_up, [_fDefaults integerForKey:@"UploadLimit"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_speed_limit_up_enabled, [_fDefaults boolForKey:@"CheckUpload"]);
|
||
|
||
//hidden prefs
|
||
if ([_fDefaults objectForKey:@"BindAddressIPv4"])
|
||
{
|
||
tr_variantDictAddStr(&settings, TR_KEY_bind_address_ipv4, [_fDefaults stringForKey:@"BindAddressIPv4"].UTF8String);
|
||
}
|
||
if ([_fDefaults objectForKey:@"BindAddressIPv6"])
|
||
{
|
||
tr_variantDictAddStr(&settings, TR_KEY_bind_address_ipv6, [_fDefaults stringForKey:@"BindAddressIPv6"].UTF8String);
|
||
}
|
||
|
||
tr_variantDictAddBool(&settings, TR_KEY_blocklist_enabled, [_fDefaults boolForKey:@"BlocklistNew"]);
|
||
if ([_fDefaults objectForKey:@"BlocklistURL"])
|
||
tr_variantDictAddStr(&settings, TR_KEY_blocklist_url, [_fDefaults stringForKey:@"BlocklistURL"].UTF8String);
|
||
tr_variantDictAddBool(&settings, TR_KEY_dht_enabled, [_fDefaults boolForKey:@"DHTGlobal"]);
|
||
tr_variantDictAddStr(
|
||
&settings,
|
||
TR_KEY_download_dir,
|
||
[_fDefaults stringForKey:@"DownloadFolder"].stringByExpandingTildeInPath.UTF8String);
|
||
tr_variantDictAddBool(&settings, TR_KEY_download_queue_enabled, [_fDefaults boolForKey:@"Queue"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_download_queue_size, [_fDefaults integerForKey:@"QueueDownloadNumber"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_idle_seeding_limit, [_fDefaults integerForKey:@"IdleLimitMinutes"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_idle_seeding_limit_enabled, [_fDefaults boolForKey:@"IdleLimitCheck"]);
|
||
tr_variantDictAddStr(
|
||
&settings,
|
||
TR_KEY_incomplete_dir,
|
||
[_fDefaults stringForKey:@"IncompleteDownloadFolder"].stringByExpandingTildeInPath.UTF8String);
|
||
tr_variantDictAddBool(&settings, TR_KEY_incomplete_dir_enabled, [_fDefaults boolForKey:@"UseIncompleteDownloadFolder"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_lpd_enabled, [_fDefaults boolForKey:@"LocalPeerDiscoveryGlobal"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_message_level, TR_LOG_DEBUG);
|
||
tr_variantDictAddInt(&settings, TR_KEY_peer_limit_global, [_fDefaults integerForKey:@"PeersTotal"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_peer_limit_per_torrent, [_fDefaults integerForKey:@"PeersTorrent"]);
|
||
|
||
NSInteger bindPort = [_fDefaults integerForKey:@"BindPort"];
|
||
if (bindPort <= 0 || bindPort > 65535)
|
||
{
|
||
// First launch, we avoid a default port to be less likely blocked on such port and to have more chances of success when connecting to swarms.
|
||
// Ideally, we should be setting port 0, then reading the port number assigned by the system and save that value. But that would be best handled by libtransmission itself.
|
||
// For now, we randomize the port as a Dynamic/Private/Ephemeral Port from 49152–65535
|
||
// https://datatracker.ietf.org/doc/html/rfc6335#section-6
|
||
uint16_t defaultPort = 49152 + arc4random_uniform(65536 - 49152);
|
||
[_fDefaults setInteger:defaultPort forKey:@"BindPort"];
|
||
}
|
||
|
||
BOOL const randomPort = [_fDefaults boolForKey:@"RandomPort"];
|
||
tr_variantDictAddBool(&settings, TR_KEY_peer_port_random_on_start, randomPort);
|
||
if (!randomPort)
|
||
{
|
||
tr_variantDictAddInt(&settings, TR_KEY_peer_port, [_fDefaults integerForKey:@"BindPort"]);
|
||
}
|
||
|
||
//hidden pref
|
||
if ([_fDefaults objectForKey:@"PeerSocketTOS"])
|
||
{
|
||
tr_variantDictAddStr(&settings, TR_KEY_peer_socket_tos, [_fDefaults stringForKey:@"PeerSocketTOS"].UTF8String);
|
||
}
|
||
|
||
tr_variantDictAddBool(&settings, TR_KEY_pex_enabled, [_fDefaults boolForKey:@"PEXGlobal"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_port_forwarding_enabled, [_fDefaults boolForKey:@"NatTraversal"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_queue_stalled_enabled, [_fDefaults boolForKey:@"CheckStalled"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_queue_stalled_minutes, [_fDefaults integerForKey:@"StalledMinutes"]);
|
||
tr_variantDictAddReal(&settings, TR_KEY_ratio_limit, [_fDefaults floatForKey:@"RatioLimit"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_ratio_limit_enabled, [_fDefaults boolForKey:@"RatioCheck"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_rename_partial_files, [_fDefaults boolForKey:@"RenamePartialFiles"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_rpc_authentication_required, [_fDefaults boolForKey:@"RPCAuthorize"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_rpc_enabled, [_fDefaults boolForKey:@"RPC"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_rpc_port, [_fDefaults integerForKey:@"RPCPort"]);
|
||
tr_variantDictAddStr(&settings, TR_KEY_rpc_username, [_fDefaults stringForKey:@"RPCUsername"].UTF8String);
|
||
tr_variantDictAddBool(&settings, TR_KEY_rpc_whitelist_enabled, [_fDefaults boolForKey:@"RPCUseWhitelist"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_rpc_host_whitelist_enabled, [_fDefaults boolForKey:@"RPCUseHostWhitelist"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_seed_queue_enabled, [_fDefaults boolForKey:@"QueueSeed"]);
|
||
tr_variantDictAddInt(&settings, TR_KEY_seed_queue_size, [_fDefaults integerForKey:@"QueueSeedNumber"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_start_added_torrents, [_fDefaults boolForKey:@"AutoStartDownload"]);
|
||
tr_variantDictAddBool(&settings, TR_KEY_utp_enabled, [_fDefaults boolForKey:@"UTPGlobal"]);
|
||
|
||
tr_variantDictAddBool(&settings, TR_KEY_script_torrent_done_enabled, [_fDefaults boolForKey:@"DoneScriptEnabled"]);
|
||
NSString* prefs_string = [_fDefaults stringForKey:@"DoneScriptPath"];
|
||
if (prefs_string != nil)
|
||
{
|
||
tr_variantDictAddStr(&settings, TR_KEY_script_torrent_done_filename, prefs_string.UTF8String);
|
||
}
|
||
|
||
// TODO: Add to GUI
|
||
if ([_fDefaults objectForKey:@"RPCHostWhitelist"])
|
||
{
|
||
tr_variantDictAddStr(&settings, TR_KEY_rpc_host_whitelist, [_fDefaults stringForKey:@"RPCHostWhitelist"].UTF8String);
|
||
}
|
||
|
||
initUnits();
|
||
|
||
auto const default_config_dir = tr_getDefaultConfigDir("Transmission");
|
||
_fLib = tr_sessionInit(default_config_dir.c_str(), YES, settings);
|
||
_fConfigDirectory = @(default_config_dir.c_str());
|
||
|
||
tr_sessionSetIdleLimitHitCallback(_fLib, onIdleLimitHit, (__bridge void*)(self));
|
||
tr_sessionSetQueueStartCallback(_fLib, onStartQueue, (__bridge void*)(self));
|
||
tr_sessionSetRatioLimitHitCallback(_fLib, onRatioLimitHit, (__bridge void*)(self));
|
||
tr_sessionSetMetadataCallback(_fLib, onMetadataCompleted, (__bridge void*)(self));
|
||
tr_sessionSetCompletenessCallback(_fLib, onTorrentCompletenessChanged, (__bridge void*)(self));
|
||
|
||
NSApp.delegate = self;
|
||
|
||
//register for magnet URLs (has to be in init)
|
||
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
|
||
andSelector:@selector(handleOpenContentsEvent:replyEvent:)
|
||
forEventClass:kInternetEventClass
|
||
andEventID:kAEGetURL];
|
||
|
||
_fTorrents = [[NSMutableArray alloc] init];
|
||
_fDisplayedTorrents = [[NSMutableArray alloc] init];
|
||
_fTorrentHashes = [[NSMutableDictionary alloc] init];
|
||
|
||
NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
|
||
configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
|
||
_fSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
|
||
|
||
_fInfoController = [[InfoWindowController alloc] init];
|
||
|
||
//needs to be done before init-ing the prefs controller
|
||
_fileWatcherQueue = [[VDKQueue alloc] init];
|
||
_fileWatcherQueue.delegate = self;
|
||
|
||
_prefsController = [[PrefsController alloc] initWithHandle:_fLib];
|
||
|
||
_fQuitting = NO;
|
||
_fGlobalPopoverShown = NO;
|
||
_fSoundPlaying = NO;
|
||
|
||
tr_sessionSetAltSpeedFunc(_fLib, altSpeedToggledCallback, (__bridge void*)(self));
|
||
if (usesSpeedLimitSched)
|
||
{
|
||
[_fDefaults setBool:tr_sessionUsesAltSpeed(_fLib) forKey:@"SpeedLimit"];
|
||
}
|
||
|
||
tr_sessionSetRPCCallback(_fLib, rpcCallback, (__bridge void*)(self));
|
||
|
||
[SUUpdater sharedUpdater].delegate = self;
|
||
_fQuitRequested = NO;
|
||
|
||
_fPauseOnLaunch = (GetCurrentKeyModifiers() & (optionKey | rightOptionKey)) != 0;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)awakeFromNib
|
||
{
|
||
[super awakeFromNib];
|
||
|
||
Toolbar* toolbar = [[Toolbar alloc] initWithIdentifier:@"TRMainToolbar"];
|
||
toolbar.delegate = self;
|
||
toolbar.allowsUserCustomization = YES;
|
||
toolbar.autosavesConfiguration = YES;
|
||
toolbar.displayMode = NSToolbarDisplayModeIconOnly;
|
||
self.fWindow.toolbar = toolbar;
|
||
|
||
self.fWindow.toolbarStyle = NSWindowToolbarStyleUnified;
|
||
self.fWindow.titleVisibility = NSWindowTitleHidden;
|
||
|
||
self.fWindow.delegate = self; //do manually to avoid placement issue
|
||
|
||
[self.fWindow makeFirstResponder:self.fTableView];
|
||
self.fWindow.excludedFromWindowsMenu = YES;
|
||
|
||
//make window primary view in fullscreen
|
||
self.fWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
|
||
|
||
//set table size
|
||
BOOL const small = [self.fDefaults boolForKey:@"SmallView"];
|
||
self.fTableView.rowHeight = small ? kRowHeightSmall : kRowHeightRegular;
|
||
self.fTableView.usesAutomaticRowHeights = NO;
|
||
self.fTableView.floatsGroupRows = YES;
|
||
//self.fTableView.usesAlternatingRowBackgroundColors = !small;
|
||
|
||
[self.fWindow setContentBorderThickness:NSMinY(self.fTableView.enclosingScrollView.frame) forEdge:NSMinYEdge];
|
||
self.fWindow.movableByWindowBackground = YES;
|
||
|
||
self.fTotalTorrentsField.cell.backgroundStyle = NSBackgroundStyleRaised;
|
||
|
||
self.fActionButton.toolTip = NSLocalizedString(@"Shortcuts for changing global settings.", "Main window -> 1st bottom left button (action) tooltip");
|
||
self.fSpeedLimitButton.toolTip = NSLocalizedString(
|
||
@"Speed Limit overrides the total bandwidth limits with its own limits.",
|
||
"Main window -> 2nd bottom left button (turtle) tooltip");
|
||
|
||
self.fClearCompletedButton.toolTip = NSLocalizedString(
|
||
@"Remove all transfers that have completed seeding.",
|
||
"Main window -> 3rd bottom left button (remove all) tooltip");
|
||
|
||
[self.fTableView registerForDraggedTypes:@[ kTorrentTableViewDataType ]];
|
||
[self.fWindow registerForDraggedTypes:@[ NSPasteboardTypeFileURL, NSPasteboardTypeURL ]];
|
||
|
||
//sort the sort menu items (localization is from strings file)
|
||
NSMutableArray* sortMenuItems = [NSMutableArray arrayWithCapacity:7];
|
||
NSUInteger sortMenuIndex = 0;
|
||
BOOL foundSortItem = NO;
|
||
for (NSMenuItem* item in self.fSortMenu.itemArray)
|
||
{
|
||
//assume all sort items are together and the Queue Order item is first
|
||
if (item.action == @selector(setSort:) && item.tag != SortTagOrder)
|
||
{
|
||
[sortMenuItems addObject:item];
|
||
[self.fSortMenu removeItemAtIndex:sortMenuIndex];
|
||
foundSortItem = YES;
|
||
}
|
||
else
|
||
{
|
||
if (foundSortItem)
|
||
{
|
||
break;
|
||
}
|
||
++sortMenuIndex;
|
||
}
|
||
}
|
||
|
||
[sortMenuItems sortUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES
|
||
selector:@selector(localizedCompare:)] ]];
|
||
|
||
for (NSMenuItem* item in sortMenuItems)
|
||
{
|
||
[self.fSortMenu insertItem:item atIndex:sortMenuIndex++];
|
||
}
|
||
|
||
//you would think this would be called later in this method from updateUI, but it's not reached in awakeFromNib
|
||
//this must be called after showStatusBar:
|
||
[self.fStatusBar updateWithDownload:0.0 upload:0.0];
|
||
|
||
//register for sleep notifications
|
||
IONotificationPortRef notify;
|
||
io_object_t iterator;
|
||
if ((self.fRootPort = IORegisterForSystemPower((__bridge void*)(self), ¬ify, sleepCallback, &iterator)))
|
||
{
|
||
CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify), kCFRunLoopCommonModes);
|
||
}
|
||
else
|
||
{
|
||
NSLog(@"Could not IORegisterForSystemPower");
|
||
}
|
||
|
||
auto* const session = self.fLib;
|
||
|
||
//load previous transfers
|
||
tr_ctor* ctor = tr_ctorNew(session);
|
||
tr_ctorSetPaused(ctor, TR_FORCE, true); // paused by default; unpause below after checking state history
|
||
auto const n_torrents = tr_sessionLoadTorrents(session, ctor);
|
||
tr_ctorFree(ctor);
|
||
|
||
// process the loaded torrents
|
||
auto torrents = std::vector<tr_torrent*>{};
|
||
torrents.resize(n_torrents);
|
||
tr_sessionGetAllTorrents(session, std::data(torrents), std::size(torrents));
|
||
for (auto* tor : torrents)
|
||
{
|
||
NSString* location;
|
||
if (tr_torrentGetDownloadDir(tor) != NULL)
|
||
{
|
||
location = @(tr_torrentGetDownloadDir(tor));
|
||
}
|
||
Torrent* torrent = [[Torrent alloc] initWithTorrentStruct:tor location:location lib:self.fLib];
|
||
[self.fTorrents addObject:torrent];
|
||
self.fTorrentHashes[torrent.hashString] = torrent;
|
||
}
|
||
|
||
//update previous transfers state by recreating a torrent from history
|
||
//and comparing to torrents already loaded via tr_sessionLoadTorrents
|
||
NSString* historyFile = [self.fConfigDirectory stringByAppendingPathComponent:kTransferPlist];
|
||
NSArray* history = [NSArray arrayWithContentsOfFile:historyFile];
|
||
if (!history)
|
||
{
|
||
//old version saved transfer info in prefs file
|
||
if ((history = [self.fDefaults arrayForKey:@"History"]))
|
||
{
|
||
[self.fDefaults removeObjectForKey:@"History"];
|
||
}
|
||
}
|
||
|
||
if (history)
|
||
{
|
||
// theoretical max without doing a lot of work
|
||
NSMutableArray* waitToStartTorrents = [NSMutableArray
|
||
arrayWithCapacity:((history.count > 0 && !self.fPauseOnLaunch) ? history.count - 1 : 0)];
|
||
|
||
Torrent* t = [[Torrent alloc] init];
|
||
for (NSDictionary* historyItem in history)
|
||
{
|
||
NSString* hash = historyItem[@"TorrentHash"];
|
||
if ([self.fTorrentHashes.allKeys containsObject:hash])
|
||
{
|
||
Torrent* torrent = self.fTorrentHashes[hash];
|
||
[t setResumeStatusForTorrent:torrent withHistory:historyItem forcePause:self.fPauseOnLaunch];
|
||
|
||
NSNumber* waitToStart;
|
||
if (!self.fPauseOnLaunch && (waitToStart = historyItem[@"WaitToStart"]) && waitToStart.boolValue)
|
||
{
|
||
[waitToStartTorrents addObject:torrent];
|
||
}
|
||
}
|
||
}
|
||
|
||
//now that all are loaded, let's set those in the queue to waiting
|
||
for (Torrent* torrent in waitToStartTorrents)
|
||
{
|
||
[torrent startTransfer];
|
||
}
|
||
}
|
||
|
||
self.fBadger = [[Badger alloc] init];
|
||
|
||
//observe notifications
|
||
NSNotificationCenter* nc = NSNotificationCenter.defaultCenter;
|
||
|
||
[nc addObserver:self selector:@selector(updateUI) name:@"UpdateUI" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(torrentFinishedDownloading:) name:@"TorrentFinishedDownloading" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(torrentRestartedDownloading:) name:@"TorrentRestartedDownloading" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(torrentFinishedSeeding:) name:@"TorrentFinishedSeeding" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(applyFilter) name:kTorrentDidChangeGroupNotification object:nil];
|
||
|
||
//avoids need of setting delegate
|
||
[nc addObserver:self selector:@selector(torrentTableViewSelectionDidChange:)
|
||
name:NSOutlineViewSelectionDidChangeNotification
|
||
object:self.fTableView];
|
||
|
||
[nc addObserver:self selector:@selector(changeAutoImport) name:@"AutoImportSettingChange" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(updateForAutoSize) name:@"AutoSizeSettingChange" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(updateForExpandCollapse) name:@"OutlineExpandCollapse" object:nil];
|
||
|
||
[nc addObserver:self.fWindow selector:@selector(makeKeyWindow) name:@"MakeWindowKey" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(fullUpdateUI) name:@"UpdateTorrentsState" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(applyFilter) name:@"ApplyFilter" object:nil];
|
||
|
||
//open newly created torrent file
|
||
[nc addObserver:self selector:@selector(beginCreateFile:) name:@"BeginCreateTorrentFile" object:nil];
|
||
|
||
//open newly created torrent file
|
||
[nc addObserver:self selector:@selector(openCreatedFile:) name:@"OpenCreatedTorrentFile" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(applyFilter) name:@"UpdateGroups" object:nil];
|
||
|
||
[nc addObserver:self selector:@selector(updateWindowAfterToolbarChange) name:@"ToolbarDidChange" object:nil];
|
||
|
||
[self updateMainWindow];
|
||
|
||
//timer to update the interface every second
|
||
[self updateUI];
|
||
self.fTimer = [NSTimer scheduledTimerWithTimeInterval:kUpdateUISeconds target:self selector:@selector(updateUI) userInfo:nil
|
||
repeats:YES];
|
||
[NSRunLoop.currentRunLoop addTimer:self.fTimer forMode:NSModalPanelRunLoopMode];
|
||
[NSRunLoop.currentRunLoop addTimer:self.fTimer forMode:NSEventTrackingRunLoopMode];
|
||
|
||
[self.fWindow makeKeyAndOrderFront:nil];
|
||
|
||
if ([self.fDefaults boolForKey:@"InfoVisible"])
|
||
{
|
||
[self showInfo:nil];
|
||
}
|
||
}
|
||
|
||
#pragma mark - NSApplicationDelegate
|
||
|
||
- (void)applicationWillFinishLaunching:(NSNotification*)notification
|
||
{
|
||
// user notifications
|
||
UNUserNotificationCenter.currentNotificationCenter.delegate = self;
|
||
UNNotificationAction* actionShow = [UNNotificationAction actionWithIdentifier:@"actionShow"
|
||
title:NSLocalizedString(@"Show", "notification button")
|
||
options:UNNotificationActionOptionForeground];
|
||
UNNotificationCategory* categoryShow = [UNNotificationCategory categoryWithIdentifier:@"categoryShow" actions:@[ actionShow ]
|
||
intentIdentifiers:@[]
|
||
options:UNNotificationCategoryOptionNone];
|
||
[UNUserNotificationCenter.currentNotificationCenter setNotificationCategories:[NSSet setWithObject:categoryShow]];
|
||
[UNUserNotificationCenter.currentNotificationCenter
|
||
requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge)
|
||
completionHandler:^(BOOL /*granted*/, NSError* _Nullable error) {
|
||
if (error.code > 0)
|
||
{
|
||
NSLog(@"UserNotifications not configured: %@", error.localizedDescription);
|
||
}
|
||
}];
|
||
}
|
||
|
||
- (void)applicationDidFinishLaunching:(NSNotification*)notification
|
||
{
|
||
//cover our asses
|
||
if ([NSUserDefaults.standardUserDefaults boolForKey:@"WarningLegal"])
|
||
{
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"I Accept", "Legal alert -> button")];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Quit", "Legal alert -> button")];
|
||
alert.messageText = NSLocalizedString(@"Welcome to Transmission", "Legal alert -> title");
|
||
alert.informativeText = NSLocalizedString(
|
||
@"Transmission is a file-sharing program."
|
||
" When you run a torrent, its data will be made available to others by means of upload."
|
||
" You and you alone are fully responsible for exercising proper judgement and abiding by your local laws.",
|
||
"Legal alert -> message");
|
||
alert.alertStyle = NSAlertStyleInformational;
|
||
|
||
if ([alert runModal] == NSAlertSecondButtonReturn)
|
||
{
|
||
exit(0);
|
||
}
|
||
|
||
[NSUserDefaults.standardUserDefaults setBool:NO forKey:@"WarningLegal"];
|
||
}
|
||
|
||
NSApp.servicesProvider = self;
|
||
|
||
self.fNoNapActivity = [NSProcessInfo.processInfo beginActivityWithOptions:NSActivityUserInitiatedAllowingIdleSystemSleep
|
||
reason:@"No napping on the job!"];
|
||
|
||
//register for dock icon drags (has to be in applicationDidFinishLaunching: to work)
|
||
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleOpenContentsEvent:replyEvent:)
|
||
forEventClass:kCoreEventClass
|
||
andEventID:kAEOpenContents];
|
||
|
||
//if we were opened from a user notification, do the corresponding action
|
||
UNNotificationResponse* launchNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey];
|
||
if (launchNotification)
|
||
{
|
||
[self userNotificationCenter:UNUserNotificationCenter.currentNotificationCenter didReceiveNotificationResponse:launchNotification
|
||
withCompletionHandler:^{
|
||
}];
|
||
}
|
||
|
||
//auto importing
|
||
[self checkAutoImportDirectory];
|
||
|
||
//registering the Web UI to Bonjour
|
||
if ([self.fDefaults boolForKey:@"RPC"] && [self.fDefaults boolForKey:@"RPCWebDiscovery"])
|
||
{
|
||
[BonjourController.defaultController startWithPort:static_cast<int>([self.fDefaults integerForKey:@"RPCPort"])];
|
||
}
|
||
|
||
//shamelessly ask for donations
|
||
if ([self.fDefaults boolForKey:@"WarningDonate"])
|
||
{
|
||
BOOL const firstLaunch = tr_sessionGetCumulativeStats(self.fLib).sessionCount <= 1;
|
||
|
||
NSDate* lastDonateDate = [self.fDefaults objectForKey:@"DonateAskDate"];
|
||
BOOL const timePassed = !lastDonateDate || (-1 * lastDonateDate.timeIntervalSinceNow) >= kDonateNagTime;
|
||
|
||
if (!firstLaunch && timePassed)
|
||
{
|
||
[self.fDefaults setObject:[NSDate date] forKey:@"DonateAskDate"];
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.messageText = NSLocalizedString(@"Support open-source indie software", "Donation beg -> title");
|
||
|
||
NSString* donateMessage = [NSString
|
||
stringWithFormat:@"%@\n\n%@",
|
||
NSLocalizedString(
|
||
@"Transmission is a full-featured torrent application."
|
||
" A lot of time and effort have gone into development, coding, and refinement."
|
||
" If you enjoy using it, please consider showing your love with a donation.",
|
||
"Donation beg -> message"),
|
||
NSLocalizedString(@"Donate or not, there will be no difference to your torrenting experience.", "Donation beg -> message")];
|
||
|
||
alert.informativeText = donateMessage;
|
||
alert.alertStyle = NSAlertStyleInformational;
|
||
|
||
[alert addButtonWithTitle:[NSLocalizedString(@"Donate", "Donation beg -> button") stringByAppendingEllipsis]];
|
||
NSButton* noDonateButton = [alert addButtonWithTitle:NSLocalizedString(@"Nope", "Donation beg -> button")];
|
||
noDonateButton.keyEquivalent = @"\e"; //escape key
|
||
|
||
// hide the "don't show again" check the first time - give them time to try the app
|
||
BOOL const allowNeverAgain = lastDonateDate != nil;
|
||
alert.showsSuppressionButton = allowNeverAgain;
|
||
if (allowNeverAgain)
|
||
{
|
||
alert.suppressionButton.title = NSLocalizedString(@"Don't bug me about this ever again.", "Donation beg -> button");
|
||
}
|
||
|
||
NSInteger const donateResult = [alert runModal];
|
||
if (donateResult == NSAlertFirstButtonReturn)
|
||
{
|
||
[self linkDonate:self];
|
||
}
|
||
|
||
if (allowNeverAgain)
|
||
{
|
||
[self.fDefaults setBool:(alert.suppressionButton.state != NSControlStateValueOn) forKey:@"WarningDonate"];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
- (BOOL)applicationShouldHandleReopen:(NSApplication*)app hasVisibleWindows:(BOOL)visibleWindows
|
||
{
|
||
NSWindow* mainWindow = NSApp.mainWindow;
|
||
if (!mainWindow || !mainWindow.visible)
|
||
{
|
||
[self.fWindow makeKeyAndOrderFront:nil];
|
||
}
|
||
|
||
return NO;
|
||
}
|
||
|
||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender
|
||
{
|
||
if (self.fQuitRequested || ![self.fDefaults boolForKey:@"CheckQuit"])
|
||
{
|
||
return NSTerminateNow;
|
||
}
|
||
|
||
NSUInteger active = 0, downloading = 0;
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.active && !torrent.stalled)
|
||
{
|
||
active++;
|
||
if (!torrent.allDownloaded)
|
||
{
|
||
downloading++;
|
||
}
|
||
}
|
||
}
|
||
|
||
BOOL preventedByTransfer = [self.fDefaults boolForKey:@"CheckQuitDownloading"] ? downloading > 0 : active > 0;
|
||
|
||
if (!preventedByTransfer)
|
||
{
|
||
return NSTerminateNow;
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.alertStyle = NSAlertStyleInformational;
|
||
alert.messageText = NSLocalizedString(@"Are you sure you want to quit?", "Confirm Quit panel -> title");
|
||
alert.informativeText = active == 1 ?
|
||
NSLocalizedString(
|
||
@"There is an active transfer that will be paused on quit."
|
||
" The transfer will automatically resume on the next launch.",
|
||
"Confirm Quit panel -> message") :
|
||
[NSString localizedStringWithFormat:NSLocalizedString(
|
||
@"There are %lu active transfers that will be paused on quit."
|
||
" The transfers will automatically resume on the next launch.",
|
||
"Confirm Quit panel -> message"),
|
||
active];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Quit", "Confirm Quit panel -> button")];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", "Confirm Quit panel -> button")];
|
||
alert.showsSuppressionButton = YES;
|
||
|
||
[alert beginSheetModalForWindow:self.fWindow completionHandler:^(NSModalResponse returnCode) {
|
||
if (alert.suppressionButton.state == NSControlStateValueOn)
|
||
{
|
||
[self.fDefaults setBool:NO forKey:@"CheckQuit"];
|
||
}
|
||
[NSApp replyToApplicationShouldTerminate:returnCode == NSAlertFirstButtonReturn];
|
||
}];
|
||
|
||
return NSTerminateLater;
|
||
}
|
||
|
||
- (void)applicationWillTerminate:(NSNotification*)notification
|
||
{
|
||
self.fQuitting = YES;
|
||
|
||
[NSProcessInfo.processInfo endActivity:self.fNoNapActivity];
|
||
|
||
//stop the Bonjour service
|
||
if (BonjourController.defaultControllerExists)
|
||
{
|
||
[BonjourController.defaultController stop];
|
||
}
|
||
|
||
//stop blocklist download
|
||
if (BlocklistDownloader.isRunning)
|
||
{
|
||
[[BlocklistDownloader downloader] cancelDownload];
|
||
}
|
||
|
||
//stop timers and notification checking
|
||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||
|
||
[self.fTimer invalidate];
|
||
|
||
if (self.fAutoImportTimer)
|
||
{
|
||
if (self.fAutoImportTimer.valid)
|
||
{
|
||
[self.fAutoImportTimer invalidate];
|
||
}
|
||
}
|
||
|
||
//remove all torrent downloads
|
||
[self.fSession invalidateAndCancel];
|
||
|
||
//remember window states
|
||
[self.fDefaults setBool:self.fInfoController.window.visible forKey:@"InfoVisible"];
|
||
|
||
if ([QLPreviewPanel sharedPreviewPanelExists] && [QLPreviewPanel sharedPreviewPanel].visible)
|
||
{
|
||
[[QLPreviewPanel sharedPreviewPanel] updateController];
|
||
}
|
||
|
||
// close all windows
|
||
for (NSWindow* window in NSApp.windows)
|
||
{
|
||
[window close];
|
||
}
|
||
|
||
// clear the badge
|
||
[self.fBadger updateBadgeWithDownload:0 upload:0];
|
||
|
||
//save history
|
||
[self updateTorrentHistory];
|
||
[self.fTableView saveCollapsedGroups];
|
||
|
||
_fileWatcherQueue = nil;
|
||
|
||
//complete cleanup: this can take many seconds
|
||
tr_sessionClose(self.fLib);
|
||
}
|
||
|
||
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication*)app
|
||
{
|
||
return YES;
|
||
}
|
||
|
||
#pragma mark -
|
||
|
||
- (tr_session*)sessionHandle
|
||
{
|
||
return self.fLib;
|
||
}
|
||
|
||
- (void)handleOpenContentsEvent:(NSAppleEventDescriptor*)event replyEvent:(NSAppleEventDescriptor*)replyEvent
|
||
{
|
||
NSString* urlString = nil;
|
||
|
||
NSAppleEventDescriptor* directObject = [event paramDescriptorForKeyword:keyDirectObject];
|
||
if (directObject.descriptorType == typeAEList)
|
||
{
|
||
for (NSInteger i = 1; i <= directObject.numberOfItems; i++)
|
||
{
|
||
if ((urlString = [directObject descriptorAtIndex:i].stringValue))
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
urlString = directObject.stringValue;
|
||
}
|
||
|
||
if (urlString)
|
||
{
|
||
[self openURL:urlString];
|
||
}
|
||
}
|
||
|
||
#pragma mark - NSURLSessionDelegate
|
||
|
||
- (void)URLSession:(nonnull NSURLSession*)session
|
||
dataTask:(nonnull NSURLSessionDataTask*)dataTask
|
||
didReceiveResponse:(nonnull NSURLResponse*)response
|
||
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler
|
||
{
|
||
NSString* suggestedName = response.suggestedFilename;
|
||
if ([suggestedName.pathExtension caseInsensitiveCompare:@"torrent"] == NSOrderedSame)
|
||
{
|
||
completionHandler(NSURLSessionResponseBecomeDownload);
|
||
return;
|
||
}
|
||
completionHandler(NSURLSessionResponseCancel);
|
||
|
||
NSString* message = [NSString
|
||
stringWithFormat:NSLocalizedString(@"It appears that the file \"%@\" from %@ is not a torrent file.", "Download not a torrent -> message"),
|
||
suggestedName,
|
||
dataTask.originalRequest.URL.absoluteString.stringByRemovingPercentEncoding];
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Download not a torrent -> button")];
|
||
alert.messageText = NSLocalizedString(@"Torrent download failed", "Download not a torrent -> title");
|
||
alert.informativeText = message;
|
||
[alert runModal];
|
||
});
|
||
}
|
||
|
||
- (void)URLSession:(nonnull NSURLSession*)session
|
||
dataTask:(nonnull NSURLSessionDataTask*)dataTask
|
||
didBecomeDownloadTask:(nonnull NSURLSessionDownloadTask*)downloadTask
|
||
{
|
||
// Required delegate method to proceed with NSURLSessionResponseBecomeDownload.
|
||
// nothing to do
|
||
}
|
||
|
||
- (void)URLSession:(nonnull NSURLSession*)session
|
||
downloadTask:(nonnull NSURLSessionDownloadTask*)downloadTask
|
||
didFinishDownloadingToURL:(nonnull NSURL*)location
|
||
{
|
||
NSString* path = [NSTemporaryDirectory() stringByAppendingPathComponent:downloadTask.response.suggestedFilename.lastPathComponent];
|
||
NSError* error;
|
||
[NSFileManager.defaultManager moveItemAtPath:location.path toPath:path error:&error];
|
||
if (error)
|
||
{
|
||
[self URLSession:session task:downloadTask didCompleteWithError:error];
|
||
return;
|
||
}
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
[self openFiles:@[ path ] addType:AddTypeURL forcePath:nil];
|
||
|
||
//delete the torrent file after opening
|
||
[NSFileManager.defaultManager removeItemAtPath:path error:NULL];
|
||
});
|
||
}
|
||
|
||
- (void)URLSession:(nonnull NSURLSession*)session
|
||
task:(nonnull NSURLSessionTask*)task
|
||
didCompleteWithError:(nullable NSError*)error
|
||
{
|
||
if (!error || error.code == NSURLErrorCancelled)
|
||
{
|
||
// no errors or we already displayed an alert
|
||
return;
|
||
}
|
||
|
||
NSString* urlString = task.currentRequest.URL.absoluteString;
|
||
if ([urlString rangeOfString:@"magnet:" options:(NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound)
|
||
{
|
||
// originalRequest was a redirect to a magnet
|
||
[self performSelectorOnMainThread:@selector(openMagnet:) withObject:urlString waitUntilDone:NO];
|
||
return;
|
||
}
|
||
|
||
NSString* message = [NSString
|
||
stringWithFormat:NSLocalizedString(@"The torrent could not be downloaded from %@: %@.", "Torrent download failed -> message"),
|
||
task.originalRequest.URL.absoluteString.stringByRemovingPercentEncoding,
|
||
error.localizedDescription];
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Torrent download failed -> button")];
|
||
alert.messageText = NSLocalizedString(@"Torrent download failed", "Torrent download error -> title");
|
||
alert.informativeText = message;
|
||
[alert runModal];
|
||
});
|
||
}
|
||
|
||
#pragma mark -
|
||
|
||
- (void)application:(NSApplication*)app openFiles:(NSArray<NSString*>*)filenames
|
||
{
|
||
[self openFiles:filenames addType:AddTypeManual forcePath:nil];
|
||
}
|
||
|
||
- (void)openFiles:(NSArray<NSString*>*)filenames addType:(AddType)type forcePath:(NSString*)path
|
||
{
|
||
BOOL deleteTorrentFile, canToggleDelete = NO;
|
||
switch (type)
|
||
{
|
||
case AddTypeCreated:
|
||
deleteTorrentFile = NO;
|
||
break;
|
||
case AddTypeURL:
|
||
deleteTorrentFile = YES;
|
||
break;
|
||
default:
|
||
deleteTorrentFile = [self.fDefaults boolForKey:@"DeleteOriginalTorrent"];
|
||
canToggleDelete = YES;
|
||
}
|
||
|
||
for (NSString* torrentPath in filenames)
|
||
{
|
||
auto metainfo = tr_torrent_metainfo{};
|
||
if (!metainfo.parse_torrent_file(torrentPath.UTF8String)) // invalid torrent
|
||
{
|
||
if (type != AddTypeAuto)
|
||
{
|
||
[self invalidOpenAlert:torrentPath.lastPathComponent];
|
||
}
|
||
continue;
|
||
}
|
||
|
||
auto foundTorrent = tr_torrentFindFromMetainfo(self.fLib, &metainfo);
|
||
if (foundTorrent != nullptr) // dupe torrent
|
||
{
|
||
if (tr_torrentHasMetadata(foundTorrent))
|
||
{
|
||
[self duplicateOpenAlert:@(metainfo.name().c_str())];
|
||
}
|
||
// foundTorrent is a magnet, fill it with file's metainfo
|
||
else if (!tr_torrentSetMetainfoFromFile(foundTorrent, &metainfo, torrentPath.UTF8String))
|
||
{
|
||
[self duplicateOpenAlert:@(metainfo.name().c_str())];
|
||
}
|
||
continue;
|
||
}
|
||
|
||
//determine download location
|
||
NSString* location;
|
||
BOOL lockDestination = NO; //don't override the location with a group location if it has a hardcoded path
|
||
if (path)
|
||
{
|
||
location = path.stringByExpandingTildeInPath;
|
||
lockDestination = YES;
|
||
}
|
||
else if ([self.fDefaults boolForKey:@"DownloadLocationConstant"])
|
||
{
|
||
location = [self.fDefaults stringForKey:@"DownloadFolder"].stringByExpandingTildeInPath;
|
||
}
|
||
else if (type != AddTypeURL)
|
||
{
|
||
location = torrentPath.stringByDeletingLastPathComponent;
|
||
}
|
||
else
|
||
{
|
||
location = nil;
|
||
}
|
||
|
||
//determine to show the options window
|
||
auto const is_multifile = metainfo.file_count() > 1;
|
||
BOOL const showWindow = type == AddTypeShowOptions ||
|
||
([self.fDefaults boolForKey:@"DownloadAsk"] && (is_multifile || ![self.fDefaults boolForKey:@"DownloadAskMulti"]) &&
|
||
(type != AddTypeAuto || ![self.fDefaults boolForKey:@"DownloadAskManual"]));
|
||
|
||
Torrent* torrent;
|
||
if (!(torrent = [[Torrent alloc] initWithPath:torrentPath location:location
|
||
deleteTorrentFile:showWindow ? NO : deleteTorrentFile
|
||
lib:self.fLib]))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
//change the location if the group calls for it (this has to wait until after the torrent is created)
|
||
if (!lockDestination && [GroupsController.groups usesCustomDownloadLocationForIndex:torrent.groupValue])
|
||
{
|
||
location = [GroupsController.groups customDownloadLocationForIndex:torrent.groupValue];
|
||
[torrent changeDownloadFolderBeforeUsing:location determinationType:TorrentDeterminationAutomatic];
|
||
}
|
||
|
||
//verify the data right away if it was newly created
|
||
if (type == AddTypeCreated)
|
||
{
|
||
[torrent resetCache];
|
||
}
|
||
|
||
//show the add window or add directly
|
||
if (showWindow || !location)
|
||
{
|
||
AddWindowController* addController = [[AddWindowController alloc] initWithTorrent:torrent destination:location
|
||
lockDestination:lockDestination
|
||
controller:self
|
||
torrentFile:torrentPath
|
||
deleteTorrentCheckEnableInitially:deleteTorrentFile
|
||
canToggleDelete:canToggleDelete];
|
||
[addController showWindow:self];
|
||
|
||
if (!self.fAddWindows)
|
||
{
|
||
self.fAddWindows = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddWindows addObject:addController];
|
||
}
|
||
else
|
||
{
|
||
if ([self.fDefaults boolForKey:@"AutoStartDownload"])
|
||
{
|
||
[torrent startTransfer];
|
||
}
|
||
|
||
[torrent update];
|
||
[self.fTorrents addObject:torrent];
|
||
|
||
if (!self.fAddingTransfers)
|
||
{
|
||
self.fAddingTransfers = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddingTransfers addObject:torrent];
|
||
}
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)askOpenConfirmed:(AddWindowController*)addController add:(BOOL)add
|
||
{
|
||
Torrent* torrent = addController.torrent;
|
||
|
||
if (add)
|
||
{
|
||
torrent.queuePosition = self.fTorrents.count;
|
||
|
||
[torrent update];
|
||
[self.fTorrents addObject:torrent];
|
||
|
||
if (!self.fAddingTransfers)
|
||
{
|
||
self.fAddingTransfers = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddingTransfers addObject:torrent];
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
else
|
||
{
|
||
[torrent closeRemoveTorrent:NO];
|
||
}
|
||
|
||
[self.fAddWindows removeObject:addController];
|
||
if (self.fAddWindows.count == 0)
|
||
{
|
||
self.fAddWindows = nil;
|
||
}
|
||
}
|
||
|
||
- (void)openMagnet:(NSString*)address
|
||
{
|
||
tr_torrent* duplicateTorrent;
|
||
if ((duplicateTorrent = tr_torrentFindFromMagnetLink(self.fLib, address.UTF8String)))
|
||
{
|
||
NSString* name = @(tr_torrentName(duplicateTorrent));
|
||
[self duplicateOpenMagnetAlert:address transferName:name];
|
||
return;
|
||
}
|
||
|
||
//determine download location
|
||
NSString* location = nil;
|
||
if ([self.fDefaults boolForKey:@"DownloadLocationConstant"])
|
||
{
|
||
location = [self.fDefaults stringForKey:@"DownloadFolder"].stringByExpandingTildeInPath;
|
||
}
|
||
|
||
Torrent* torrent;
|
||
if (!(torrent = [[Torrent alloc] initWithMagnetAddress:address location:location lib:self.fLib]))
|
||
{
|
||
[self invalidOpenMagnetAlert:address];
|
||
return;
|
||
}
|
||
|
||
//change the location if the group calls for it (this has to wait until after the torrent is created)
|
||
if ([GroupsController.groups usesCustomDownloadLocationForIndex:torrent.groupValue])
|
||
{
|
||
location = [GroupsController.groups customDownloadLocationForIndex:torrent.groupValue];
|
||
[torrent changeDownloadFolderBeforeUsing:location determinationType:TorrentDeterminationAutomatic];
|
||
}
|
||
|
||
if ([self.fDefaults boolForKey:@"MagnetOpenAsk"] || !location)
|
||
{
|
||
AddMagnetWindowController* addController = [[AddMagnetWindowController alloc] initWithTorrent:torrent destination:location
|
||
controller:self];
|
||
[addController showWindow:self];
|
||
|
||
if (!self.fAddWindows)
|
||
{
|
||
self.fAddWindows = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddWindows addObject:addController];
|
||
}
|
||
else
|
||
{
|
||
if ([self.fDefaults boolForKey:@"AutoStartDownload"])
|
||
{
|
||
[torrent startTransfer];
|
||
}
|
||
|
||
[torrent update];
|
||
[self.fTorrents addObject:torrent];
|
||
|
||
if (!self.fAddingTransfers)
|
||
{
|
||
self.fAddingTransfers = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddingTransfers addObject:torrent];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)askOpenMagnetConfirmed:(AddMagnetWindowController*)addController add:(BOOL)add
|
||
{
|
||
Torrent* torrent = addController.torrent;
|
||
|
||
if (add)
|
||
{
|
||
torrent.queuePosition = self.fTorrents.count;
|
||
|
||
[torrent update];
|
||
[self.fTorrents addObject:torrent];
|
||
|
||
if (!self.fAddingTransfers)
|
||
{
|
||
self.fAddingTransfers = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddingTransfers addObject:torrent];
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
else
|
||
{
|
||
[torrent closeRemoveTorrent:NO];
|
||
}
|
||
|
||
[self.fAddWindows removeObject:addController];
|
||
if (self.fAddWindows.count == 0)
|
||
{
|
||
self.fAddWindows = nil;
|
||
}
|
||
}
|
||
|
||
- (void)openCreatedFile:(NSNotification*)notification
|
||
{
|
||
NSDictionary* dict = notification.userInfo;
|
||
[self openFiles:@[ dict[@"File"] ] addType:AddTypeCreated forcePath:dict[@"Path"]];
|
||
}
|
||
|
||
- (void)openFilesWithDict:(NSDictionary*)dictionary
|
||
{
|
||
[self openFiles:dictionary[@"Filenames"] addType:static_cast<AddType>([dictionary[@"AddType"] intValue]) forcePath:nil];
|
||
}
|
||
|
||
//called on by applescript
|
||
- (void)open:(NSArray*)files
|
||
{
|
||
NSDictionary* dict = @{ @"Filenames" : files, @"AddType" : @(AddTypeManual) };
|
||
[self performSelectorOnMainThread:@selector(openFilesWithDict:) withObject:dict waitUntilDone:NO];
|
||
}
|
||
|
||
- (void)openShowSheet:(id)sender
|
||
{
|
||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||
|
||
panel.allowsMultipleSelection = YES;
|
||
panel.canChooseFiles = YES;
|
||
panel.canChooseDirectories = NO;
|
||
|
||
panel.allowedFileTypes = @[ @"org.bittorrent.torrent", @"torrent" ];
|
||
|
||
[panel beginSheetModalForWindow:self.fWindow completionHandler:^(NSInteger result) {
|
||
if (result == NSModalResponseOK)
|
||
{
|
||
NSMutableArray* filenames = [NSMutableArray arrayWithCapacity:panel.URLs.count];
|
||
for (NSURL* url in panel.URLs)
|
||
{
|
||
[filenames addObject:url.path];
|
||
}
|
||
|
||
NSDictionary* dictionary = @{
|
||
@"Filenames" : filenames,
|
||
@"AddType" : sender == self.fOpenIgnoreDownloadFolder ? @(AddTypeShowOptions) : @(AddTypeManual)
|
||
};
|
||
[self performSelectorOnMainThread:@selector(openFilesWithDict:) withObject:dictionary waitUntilDone:NO];
|
||
}
|
||
}];
|
||
}
|
||
|
||
- (void)invalidOpenAlert:(NSString*)filename
|
||
{
|
||
if (![self.fDefaults boolForKey:@"WarningInvalidOpen"])
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.messageText = [NSString
|
||
stringWithFormat:NSLocalizedString(@"\"%@\" is not a valid torrent file.", "Open invalid alert -> title"), filename];
|
||
alert.informativeText = NSLocalizedString(@"The torrent file cannot be opened because it contains invalid data.", "Open invalid alert -> message");
|
||
|
||
alert.alertStyle = NSAlertStyleWarning;
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Open invalid alert -> button")];
|
||
|
||
[alert runModal];
|
||
if (alert.suppressionButton.state == NSControlStateValueOn)
|
||
{
|
||
[self.fDefaults setBool:NO forKey:@"WarningInvalidOpen"];
|
||
}
|
||
}
|
||
|
||
- (void)invalidOpenMagnetAlert:(NSString*)address
|
||
{
|
||
if (![self.fDefaults boolForKey:@"WarningInvalidOpen"])
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.messageText = NSLocalizedString(@"Adding magnetized transfer failed.", "Magnet link failed -> title");
|
||
alert.informativeText = [NSString stringWithFormat:NSLocalizedString(
|
||
@"There was an error when adding the magnet link \"%@\"."
|
||
" The transfer will not occur.",
|
||
"Magnet link failed -> message"),
|
||
address];
|
||
alert.alertStyle = NSAlertStyleWarning;
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Magnet link failed -> button")];
|
||
|
||
[alert runModal];
|
||
if (alert.suppressionButton.state == NSControlStateValueOn)
|
||
{
|
||
[self.fDefaults setBool:NO forKey:@"WarningInvalidOpen"];
|
||
}
|
||
}
|
||
|
||
- (void)duplicateOpenAlert:(NSString*)name
|
||
{
|
||
if (![self.fDefaults boolForKey:@"WarningDuplicate"])
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.messageText = [NSString
|
||
stringWithFormat:NSLocalizedString(@"A transfer of \"%@\" already exists.", "Open duplicate alert -> title"), name];
|
||
alert.informativeText = NSLocalizedString(
|
||
@"The transfer cannot be added because it is a duplicate of an already existing transfer.",
|
||
"Open duplicate alert -> message");
|
||
|
||
alert.alertStyle = NSAlertStyleWarning;
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Open duplicate alert -> button")];
|
||
alert.showsSuppressionButton = YES;
|
||
|
||
[alert runModal];
|
||
if (alert.suppressionButton.state)
|
||
{
|
||
[self.fDefaults setBool:NO forKey:@"WarningDuplicate"];
|
||
}
|
||
}
|
||
|
||
- (void)duplicateOpenMagnetAlert:(NSString*)address transferName:(NSString*)name
|
||
{
|
||
if (![self.fDefaults boolForKey:@"WarningDuplicate"])
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
if (name)
|
||
{
|
||
alert.messageText = [NSString
|
||
stringWithFormat:NSLocalizedString(@"A transfer of \"%@\" already exists.", "Open duplicate magnet alert -> title"), name];
|
||
}
|
||
else
|
||
{
|
||
alert.messageText = NSLocalizedString(@"Magnet link is a duplicate of an existing transfer.", "Open duplicate magnet alert -> title");
|
||
}
|
||
alert.informativeText = [NSString
|
||
stringWithFormat:NSLocalizedString(
|
||
@"The magnet link \"%@\" cannot be added because it is a duplicate of an already existing transfer.",
|
||
"Open duplicate magnet alert -> message"),
|
||
address];
|
||
alert.alertStyle = NSAlertStyleWarning;
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Open duplicate magnet alert -> button")];
|
||
alert.showsSuppressionButton = YES;
|
||
|
||
[alert runModal];
|
||
if (alert.suppressionButton.state)
|
||
{
|
||
[self.fDefaults setBool:NO forKey:@"WarningDuplicate"];
|
||
}
|
||
}
|
||
|
||
- (void)openURL:(NSString*)urlString
|
||
{
|
||
if ([urlString rangeOfString:@"magnet:" options:(NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound)
|
||
{
|
||
[self openMagnet:urlString];
|
||
}
|
||
else
|
||
{
|
||
if ([urlString rangeOfString:@"://"].location == NSNotFound)
|
||
{
|
||
if ([urlString rangeOfString:@"."].location == NSNotFound)
|
||
{
|
||
NSInteger beforeCom;
|
||
if ((beforeCom = [urlString rangeOfString:@"/"].location) != NSNotFound)
|
||
{
|
||
urlString = [NSString stringWithFormat:@"http://www.%@.com/%@",
|
||
[urlString substringToIndex:beforeCom],
|
||
[urlString substringFromIndex:beforeCom + 1]];
|
||
}
|
||
else
|
||
{
|
||
urlString = [NSString stringWithFormat:@"http://www.%@.com/", urlString];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
urlString = [@"http://" stringByAppendingString:urlString];
|
||
}
|
||
}
|
||
|
||
NSURL* url = [NSURL URLWithString:urlString];
|
||
if (url == nil)
|
||
{
|
||
NSLog(@"Detected non-URL string \"%@\". Ignoring.", urlString);
|
||
return;
|
||
}
|
||
|
||
[self.fSession getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask*>* _Nonnull tasks) {
|
||
for (NSURLSessionTask* task in tasks)
|
||
{
|
||
if ([task.originalRequest.URL isEqual:url])
|
||
{
|
||
NSLog(@"Already downloading %@", url);
|
||
return;
|
||
}
|
||
}
|
||
NSURLSessionDataTask* download = [self.fSession dataTaskWithURL:url];
|
||
[download resume];
|
||
}];
|
||
}
|
||
}
|
||
|
||
- (void)openURLShowSheet:(id)sender
|
||
{
|
||
if (!self.fUrlSheetController)
|
||
{
|
||
self.fUrlSheetController = [[URLSheetWindowController alloc] init];
|
||
|
||
[self.fWindow beginSheet:self.fUrlSheetController.window completionHandler:^(NSModalResponse returnCode) {
|
||
if (returnCode == 1)
|
||
{
|
||
NSString* urlString = self.fUrlSheetController.urlString;
|
||
urlString = [urlString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
[self openURL:urlString];
|
||
});
|
||
}
|
||
self.fUrlSheetController = nil;
|
||
}];
|
||
}
|
||
}
|
||
|
||
- (void)openPasteboard
|
||
{
|
||
// 1. If Pasteboard contains URL objects, we treat those and only those
|
||
NSArray<NSURL*>* arrayOfURLs = [NSPasteboard.generalPasteboard readObjectsForClasses:@[ [NSURL class] ] options:nil];
|
||
|
||
if (arrayOfURLs.count > 0)
|
||
{
|
||
for (NSURL* url in arrayOfURLs)
|
||
{
|
||
[self openURL:url.absoluteString];
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 2. If Pasteboard contains String objects, we'll search for both links and magnets
|
||
NSArray<NSString*>* arrayOfStrings = [NSPasteboard.generalPasteboard readObjectsForClasses:@[ [NSString class] ] options:nil];
|
||
if (arrayOfStrings.count == 0)
|
||
{
|
||
return;
|
||
}
|
||
// The link detector (can't detect magnets)
|
||
NSDataDetector* linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
|
||
// The magnet detector
|
||
// https://www.bittorrent.org/beps/bep_0009.html defines the magnet URI format as `magnet:?query` where query is non-empty.
|
||
// https://datatracker.ietf.org/doc/html/rfc3986 defines the query format rigorously as `([!$\&-;=?-Z_a-z~]|%[0-9A-F]{2})*`.
|
||
// But `tr_urlParse` acknowledges that magnet links can be malformed by "not escaping text in the display name".
|
||
// Those malformed magnets aren't URI anymore, and since a display name can potentially contain any Unicode except '/' (see `isUnixReservedChar`), we may want to be liberal on what we accept.
|
||
// In practice, copy-pasted magnets might most often be separated by Horizontal tab, Line feed, Carriage Return, Space, XML delimiters '<' '>', JSON delimiter '"' and Markdown delimiter '`'.
|
||
// But for now, we'll keep the historical separator choice from 8392476b30491ffe7d8d64210f5cf3c3dd1d69ca, whitespaceAndNewlineCharacterSet, which is `[\p{Z}\v]`.
|
||
NSRegularExpression* magnetDetector = [NSRegularExpression regularExpressionWithPattern:@"magnet:?([^\\p{Z}\\v])+" options:kNilOptions
|
||
error:nil];
|
||
for (NSString* itemString in arrayOfStrings)
|
||
{
|
||
// We open all links
|
||
for (NSTextCheckingResult* result in [linkDetector matchesInString:itemString options:0
|
||
range:NSMakeRange(0, itemString.length)])
|
||
{
|
||
[self openURL:result.URL.absoluteString];
|
||
}
|
||
|
||
// We open all magnets
|
||
for (NSTextCheckingResult* result in [magnetDetector matchesInString:itemString options:0
|
||
range:NSMakeRange(0, itemString.length)])
|
||
{
|
||
[self openURL:[itemString substringWithRange:result.range]];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)createFile:(id)sender
|
||
{
|
||
[CreatorWindowController createTorrentFile:self.fLib];
|
||
}
|
||
|
||
- (void)resumeSelectedTorrents:(id)sender
|
||
{
|
||
[self resumeTorrents:self.fTableView.selectedTorrents];
|
||
}
|
||
|
||
- (void)resumeAllTorrents:(id)sender
|
||
{
|
||
NSMutableArray<Torrent*>* torrents = [NSMutableArray arrayWithCapacity:self.fTorrents.count];
|
||
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (!torrent.finishedSeeding)
|
||
{
|
||
[torrents addObject:torrent];
|
||
}
|
||
}
|
||
|
||
[self resumeTorrents:torrents];
|
||
}
|
||
|
||
- (void)resumeTorrents:(NSArray<Torrent*>*)torrents
|
||
{
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent startTransfer];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)resumeSelectedTorrentsNoWait:(id)sender
|
||
{
|
||
[self resumeTorrentsNoWait:self.fTableView.selectedTorrents];
|
||
}
|
||
|
||
- (void)resumeWaitingTorrents:(id)sender
|
||
{
|
||
NSMutableArray<Torrent*>* torrents = [NSMutableArray arrayWithCapacity:self.fTorrents.count];
|
||
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.waitingToStart)
|
||
{
|
||
[torrents addObject:torrent];
|
||
}
|
||
}
|
||
|
||
[self resumeTorrentsNoWait:torrents];
|
||
}
|
||
|
||
- (void)resumeTorrentsNoWait:(NSArray<Torrent*>*)torrents
|
||
{
|
||
//iterate through instead of all at once to ensure no conflicts
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent startTransferNoQueue];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)stopSelectedTorrents:(id)sender
|
||
{
|
||
[self stopTorrents:self.fTableView.selectedTorrents];
|
||
}
|
||
|
||
- (void)stopAllTorrents:(id)sender
|
||
{
|
||
[self stopTorrents:self.fTorrents];
|
||
}
|
||
|
||
- (void)stopTorrents:(NSArray<Torrent*>*)torrents
|
||
{
|
||
//don't want any of these starting then stopping
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
if (torrent.waitingToStart)
|
||
{
|
||
[torrent stopTransfer];
|
||
}
|
||
}
|
||
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent stopTransfer];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)removeTorrents:(NSArray<Torrent*>*)torrents deleteData:(BOOL)deleteData
|
||
{
|
||
if ([self.fDefaults boolForKey:@"CheckRemove"])
|
||
{
|
||
NSUInteger active = 0, downloading = 0;
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
if (torrent.active)
|
||
{
|
||
++active;
|
||
if (!torrent.seeding)
|
||
{
|
||
++downloading;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ([self.fDefaults boolForKey:@"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
|
||
{
|
||
NSString *title, *message;
|
||
|
||
NSUInteger const selected = torrents.count;
|
||
if (selected == 1)
|
||
{
|
||
NSString* torrentName = torrents[0].name;
|
||
|
||
if (deleteData)
|
||
{
|
||
title = [NSString stringWithFormat:NSLocalizedString(
|
||
@"Are you sure you want to remove \"%@\" from the transfer list"
|
||
" and trash the data file?",
|
||
"Removal confirm panel -> title"),
|
||
torrentName];
|
||
}
|
||
else
|
||
{
|
||
title = [NSString
|
||
stringWithFormat:NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list?", "Removal confirm panel -> title"),
|
||
torrentName];
|
||
}
|
||
|
||
message = NSLocalizedString(
|
||
@"This transfer is active."
|
||
" Once removed, continuing the transfer will require the torrent file or magnet link.",
|
||
"Removal confirm panel -> message");
|
||
}
|
||
else
|
||
{
|
||
if (deleteData)
|
||
{
|
||
title = [NSString localizedStringWithFormat:NSLocalizedString(
|
||
@"Are you sure you want to remove %lu transfers from the transfer list"
|
||
" and trash the data files?",
|
||
"Removal confirm panel -> title"),
|
||
selected];
|
||
}
|
||
else
|
||
{
|
||
title = [NSString localizedStringWithFormat:NSLocalizedString(
|
||
@"Are you sure you want to remove %lu transfers from the transfer list?",
|
||
"Removal confirm panel -> title"),
|
||
selected];
|
||
}
|
||
|
||
if (selected == active)
|
||
{
|
||
message = [NSString localizedStringWithFormat:NSLocalizedString(@"There are %lu active transfers.", "Removal confirm panel -> message part 1"),
|
||
active];
|
||
}
|
||
else
|
||
{
|
||
message = [NSString localizedStringWithFormat:NSLocalizedString(@"There are %1$lu transfers (%2$lu active).", "Removal confirm panel -> message part 1"),
|
||
selected,
|
||
active];
|
||
}
|
||
message = [message stringByAppendingFormat:@" %@",
|
||
NSLocalizedString(
|
||
@"Once removed, continuing the transfers will require the torrent files or magnet links.",
|
||
"Removal confirm panel -> message part 2")];
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.alertStyle = NSAlertStyleInformational;
|
||
alert.messageText = title;
|
||
alert.informativeText = message;
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Remove", "Removal confirm panel -> button")];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", "Removal confirm panel -> button")];
|
||
|
||
[alert beginSheetModalForWindow:self.fWindow completionHandler:^(NSModalResponse returnCode) {
|
||
if (returnCode == NSAlertFirstButtonReturn)
|
||
{
|
||
[self confirmRemoveTorrents:torrents deleteData:deleteData];
|
||
}
|
||
}];
|
||
return;
|
||
}
|
||
}
|
||
|
||
[self confirmRemoveTorrents:torrents deleteData:deleteData];
|
||
}
|
||
|
||
- (void)confirmRemoveTorrents:(NSArray<Torrent*>*)torrents deleteData:(BOOL)deleteData
|
||
{
|
||
//miscellaneous
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
//don't want any of these starting then stopping
|
||
if (torrent.waitingToStart)
|
||
{
|
||
[torrent stopTransfer];
|
||
}
|
||
|
||
//let's expand all groups that have removed items - they either don't exist anymore, are already expanded, or are collapsed (rpc)
|
||
[self.fTableView removeCollapsedGroup:torrent.groupValue];
|
||
|
||
//we can't assume the window is active - RPC removal, for example
|
||
[self.fBadger removeTorrent:torrent];
|
||
}
|
||
|
||
//#5106 - don't try to remove torrents that have already been removed (fix for a bug, but better safe than crash anyway)
|
||
NSIndexSet* indexesToRemove = [torrents indexesOfObjectsWithOptions:NSEnumerationConcurrent
|
||
passingTest:^BOOL(Torrent* torrent, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
return [self.fTorrents indexOfObjectIdenticalTo:torrent] != NSNotFound;
|
||
}];
|
||
if (torrents.count != indexesToRemove.count)
|
||
{
|
||
NSLog(
|
||
@"trying to remove %ld transfers, but %ld have already been removed",
|
||
torrents.count,
|
||
torrents.count - indexesToRemove.count);
|
||
torrents = [torrents objectsAtIndexes:indexesToRemove];
|
||
|
||
if (indexesToRemove.count == 0)
|
||
{
|
||
[self fullUpdateUI];
|
||
return;
|
||
}
|
||
}
|
||
|
||
[self.fTorrents removeObjectsInArray:torrents];
|
||
|
||
//set up helpers to remove from the table
|
||
__block BOOL beganUpdate = NO;
|
||
|
||
void (^doTableRemoval)(NSMutableArray*, id) = ^(NSMutableArray<Torrent*>* displayedTorrents, id parent) {
|
||
NSIndexSet* indexes = [displayedTorrents indexesOfObjectsWithOptions:NSEnumerationConcurrent
|
||
passingTest:^BOOL(Torrent* obj, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
return [torrents containsObject:obj];
|
||
}];
|
||
|
||
if (indexes.count > 0)
|
||
{
|
||
if (!beganUpdate)
|
||
{
|
||
[NSAnimationContext beginGrouping]; //this has to be before we set the completion handler (#4874)
|
||
|
||
//we can't closeRemoveTorrent: until it's no longer in the GUI at all
|
||
NSAnimationContext.currentContext.completionHandler = ^{
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent closeRemoveTorrent:deleteData];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
};
|
||
|
||
[self.fTableView beginUpdates];
|
||
beganUpdate = YES;
|
||
}
|
||
|
||
[self.fTableView removeItemsAtIndexes:indexes inParent:parent withAnimation:NSTableViewAnimationSlideLeft];
|
||
|
||
[displayedTorrents removeObjectsAtIndexes:indexes];
|
||
}
|
||
};
|
||
|
||
//if not removed from the displayed torrents here, fullUpdateUI might cause a crash
|
||
if (self.fDisplayedTorrents.count > 0)
|
||
{
|
||
if ([self.fDisplayedTorrents[0] isKindOfClass:[TorrentGroup class]])
|
||
{
|
||
for (TorrentGroup* group in self.fDisplayedTorrents)
|
||
{
|
||
doTableRemoval(group.torrents, group);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
doTableRemoval(self.fDisplayedTorrents, nil);
|
||
}
|
||
|
||
if (beganUpdate)
|
||
{
|
||
[self.fTableView endUpdates];
|
||
[NSAnimationContext endGrouping];
|
||
}
|
||
}
|
||
|
||
if (!beganUpdate)
|
||
{
|
||
//do here if we're not doing it at the end of the animation
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent closeRemoveTorrent:deleteData];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)removeNoDelete:(id)sender
|
||
{
|
||
[self removeTorrents:self.fTableView.selectedTorrents deleteData:NO];
|
||
}
|
||
|
||
- (void)removeDeleteData:(id)sender
|
||
{
|
||
[self removeTorrents:self.fTableView.selectedTorrents deleteData:YES];
|
||
}
|
||
|
||
- (void)clearCompleted:(id)sender
|
||
{
|
||
NSMutableArray<Torrent*>* torrents = [NSMutableArray array];
|
||
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.finishedSeeding)
|
||
{
|
||
[torrents addObject:torrent];
|
||
}
|
||
}
|
||
|
||
if ([self.fDefaults boolForKey:@"WarningRemoveCompleted"])
|
||
{
|
||
NSString *message, *info;
|
||
if (torrents.count == 1)
|
||
{
|
||
NSString* torrentName = torrents[0].name;
|
||
message = [NSString
|
||
stringWithFormat:NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list?", "Remove completed confirm panel -> title"),
|
||
torrentName];
|
||
|
||
info = NSLocalizedString(
|
||
@"Once removed, continuing the transfer will require the torrent file or magnet link.",
|
||
"Remove completed confirm panel -> message");
|
||
}
|
||
else
|
||
{
|
||
message = [NSString localizedStringWithFormat:NSLocalizedString(
|
||
@"Are you sure you want to remove %lu completed transfers from the transfer list?",
|
||
"Remove completed confirm panel -> title"),
|
||
torrents.count];
|
||
|
||
info = NSLocalizedString(
|
||
@"Once removed, continuing the transfers will require the torrent files or magnet links.",
|
||
"Remove completed confirm panel -> message");
|
||
}
|
||
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
alert.messageText = message;
|
||
alert.informativeText = info;
|
||
alert.alertStyle = NSAlertStyleWarning;
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Remove", "Remove completed confirm panel -> button")];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", "Remove completed confirm panel -> button")];
|
||
alert.showsSuppressionButton = YES;
|
||
|
||
NSInteger const returnCode = [alert runModal];
|
||
if (alert.suppressionButton.state)
|
||
{
|
||
[self.fDefaults setBool:NO forKey:@"WarningRemoveCompleted"];
|
||
}
|
||
|
||
if (returnCode != NSAlertFirstButtonReturn)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
[self confirmRemoveTorrents:torrents deleteData:NO];
|
||
}
|
||
|
||
- (void)moveDataFilesSelected:(id)sender
|
||
{
|
||
[self moveDataFiles:self.fTableView.selectedTorrents];
|
||
}
|
||
|
||
- (void)moveDataFiles:(NSArray<Torrent*>*)torrents
|
||
{
|
||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||
panel.prompt = NSLocalizedString(@"Select", "Move torrent -> prompt");
|
||
panel.allowsMultipleSelection = NO;
|
||
panel.canChooseFiles = NO;
|
||
panel.canChooseDirectories = YES;
|
||
panel.canCreateDirectories = YES;
|
||
|
||
NSUInteger count = torrents.count;
|
||
if (count == 1)
|
||
{
|
||
panel.message = [NSString
|
||
stringWithFormat:NSLocalizedString(@"Select the new folder for \"%@\".", "Move torrent -> select destination folder"),
|
||
torrents[0].name];
|
||
}
|
||
else
|
||
{
|
||
panel.message = [NSString
|
||
localizedStringWithFormat:NSLocalizedString(@"Select the new folder for %lu data files.", "Move torrent -> select destination folder"),
|
||
count];
|
||
}
|
||
|
||
[panel beginSheetModalForWindow:self.fWindow completionHandler:^(NSInteger result) {
|
||
if (result == NSModalResponseOK)
|
||
{
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent moveTorrentDataFileTo:panel.URLs[0].path];
|
||
}
|
||
}
|
||
}];
|
||
}
|
||
|
||
- (void)copyTorrentFiles:(id)sender
|
||
{
|
||
[self copyTorrentFileForTorrents:[[NSMutableArray alloc] initWithArray:self.fTableView.selectedTorrents]];
|
||
}
|
||
|
||
- (void)copyTorrentFileForTorrents:(NSMutableArray<Torrent*>*)torrents
|
||
{
|
||
if (torrents.count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Torrent* torrent = torrents[0];
|
||
|
||
if (!torrent.magnet && [NSFileManager.defaultManager fileExistsAtPath:torrent.torrentLocation])
|
||
{
|
||
NSSavePanel* panel = [NSSavePanel savePanel];
|
||
panel.allowedFileTypes = @[ @"org.bittorrent.torrent", @"torrent" ];
|
||
panel.extensionHidden = NO;
|
||
|
||
panel.nameFieldStringValue = torrent.name;
|
||
|
||
[panel beginSheetModalForWindow:self.fWindow completionHandler:^(NSInteger result) {
|
||
//copy torrent to new location with name of data file
|
||
if (result == NSModalResponseOK)
|
||
{
|
||
[torrent copyTorrentFileTo:panel.URL.path];
|
||
}
|
||
|
||
[torrents removeObjectAtIndex:0];
|
||
[self performSelectorOnMainThread:@selector(copyTorrentFileForTorrents:) withObject:torrents waitUntilDone:NO];
|
||
}];
|
||
}
|
||
else
|
||
{
|
||
if (!torrent.magnet)
|
||
{
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Torrent file copy alert -> button")];
|
||
alert.messageText = [NSString
|
||
stringWithFormat:NSLocalizedString(@"Copy of \"%@\" Cannot Be Created", "Torrent file copy alert -> title"),
|
||
torrent.name];
|
||
alert.informativeText = [NSString
|
||
stringWithFormat:NSLocalizedString(@"The torrent file (%@) cannot be found.", "Torrent file copy alert -> message"),
|
||
torrent.torrentLocation];
|
||
alert.alertStyle = NSAlertStyleWarning;
|
||
|
||
[alert runModal];
|
||
}
|
||
|
||
[torrents removeObjectAtIndex:0];
|
||
[self copyTorrentFileForTorrents:torrents];
|
||
}
|
||
}
|
||
|
||
- (void)copyMagnetLinks:(id)sender
|
||
{
|
||
[self.fTableView copy:sender];
|
||
}
|
||
|
||
- (void)revealFile:(id)sender
|
||
{
|
||
NSArray* selected = self.fTableView.selectedTorrents;
|
||
NSMutableArray* paths = [NSMutableArray arrayWithCapacity:selected.count];
|
||
for (Torrent* torrent in selected)
|
||
{
|
||
NSString* location = torrent.dataLocation;
|
||
if (location)
|
||
{
|
||
[paths addObject:[NSURL fileURLWithPath:location]];
|
||
}
|
||
}
|
||
|
||
if (paths.count > 0)
|
||
{
|
||
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:paths];
|
||
}
|
||
}
|
||
|
||
- (IBAction)renameSelected:(id)sender
|
||
{
|
||
NSArray* selected = self.fTableView.selectedTorrents;
|
||
NSAssert(selected.count == 1, @"1 transfer needs to be selected to rename, but %ld are selected", selected.count);
|
||
Torrent* torrent = selected[0];
|
||
|
||
[FileRenameSheetController presentSheetForTorrent:torrent modalForWindow:self.fWindow completionHandler:^(BOOL didRename) {
|
||
if (didRename)
|
||
{
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
[self fullUpdateUI];
|
||
|
||
[NSNotificationCenter.defaultCenter postNotificationName:@"ResetInspector" object:self
|
||
userInfo:@{ @"Torrent" : torrent }];
|
||
});
|
||
}
|
||
}];
|
||
}
|
||
|
||
- (void)announceSelectedTorrents:(id)sender
|
||
{
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (torrent.canManualAnnounce)
|
||
{
|
||
[torrent manualAnnounce];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)verifySelectedTorrents:(id)sender
|
||
{
|
||
[self verifyTorrents:self.fTableView.selectedTorrents];
|
||
}
|
||
|
||
- (void)verifyTorrents:(NSArray<Torrent*>*)torrents
|
||
{
|
||
for (Torrent* torrent in torrents)
|
||
{
|
||
[torrent resetCache];
|
||
}
|
||
|
||
[self applyFilter];
|
||
}
|
||
|
||
- (NSArray<Torrent*>*)selectedTorrents
|
||
{
|
||
return self.fTableView.selectedTorrents;
|
||
}
|
||
|
||
- (void)showPreferenceWindow:(id)sender
|
||
{
|
||
NSWindow* window = _prefsController.window;
|
||
if (!window.visible)
|
||
{
|
||
[window center];
|
||
}
|
||
|
||
[window makeKeyAndOrderFront:nil];
|
||
}
|
||
|
||
- (void)showAboutWindow:(id)sender
|
||
{
|
||
[AboutWindowController.aboutController showWindow:nil];
|
||
}
|
||
|
||
- (void)showInfo:(id)sender
|
||
{
|
||
if (self.fInfoController.window.visible)
|
||
{
|
||
[self.fInfoController close];
|
||
}
|
||
else
|
||
{
|
||
[self.fInfoController updateInfoStats];
|
||
[self.fInfoController.window orderFront:nil];
|
||
|
||
if (self.fInfoController.canQuickLook && [QLPreviewPanel sharedPreviewPanelExists] &&
|
||
[QLPreviewPanel sharedPreviewPanel].visible)
|
||
{
|
||
[[QLPreviewPanel sharedPreviewPanel] reloadData];
|
||
}
|
||
}
|
||
|
||
[self.fWindow.toolbar validateVisibleItems];
|
||
}
|
||
|
||
- (void)resetInfo
|
||
{
|
||
[self.fInfoController setInfoForTorrents:self.fTableView.selectedTorrents];
|
||
|
||
if ([QLPreviewPanel sharedPreviewPanelExists] && [QLPreviewPanel sharedPreviewPanel].visible)
|
||
{
|
||
[[QLPreviewPanel sharedPreviewPanel] reloadData];
|
||
}
|
||
}
|
||
|
||
- (void)setInfoTab:(id)sender
|
||
{
|
||
if (sender == self.fNextInfoTabItem)
|
||
{
|
||
[self.fInfoController setNextTab];
|
||
}
|
||
else
|
||
{
|
||
[self.fInfoController setPreviousTab];
|
||
}
|
||
}
|
||
|
||
- (MessageWindowController*)messageWindowController
|
||
{
|
||
if (!self.fMessageController)
|
||
{
|
||
self.fMessageController = [[MessageWindowController alloc] init];
|
||
}
|
||
|
||
return self.fMessageController;
|
||
}
|
||
|
||
- (void)showMessageWindow:(id)sender
|
||
{
|
||
[self.messageWindowController showWindow:nil];
|
||
}
|
||
|
||
- (void)showStatsWindow:(id)sender
|
||
{
|
||
[StatsWindowController.statsWindow showWindow:nil];
|
||
}
|
||
|
||
- (void)updateUI
|
||
{
|
||
CGFloat dlRate = 0.0, ulRate = 0.0;
|
||
BOOL anyCompleted = NO;
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
[torrent update];
|
||
|
||
//pull the upload and download speeds - most consistent by using current stats
|
||
dlRate += torrent.downloadRate;
|
||
ulRate += torrent.uploadRate;
|
||
|
||
anyCompleted |= torrent.finishedSeeding;
|
||
}
|
||
|
||
if (!NSApp.hidden)
|
||
{
|
||
if (self.fWindow.visible)
|
||
{
|
||
[self sortTorrentsAndIncludeQueueOrder:NO];
|
||
|
||
[self.fStatusBar updateWithDownload:dlRate upload:ulRate];
|
||
|
||
self.fClearCompletedButton.hidden = !anyCompleted;
|
||
}
|
||
|
||
//update non-constant parts of info window
|
||
if (self.fInfoController.window.visible)
|
||
{
|
||
[self.fInfoController updateInfoStats];
|
||
}
|
||
|
||
[self.fTableView reloadVisibleRows];
|
||
}
|
||
|
||
//badge dock
|
||
[self.fBadger updateBadgeWithDownload:dlRate upload:ulRate];
|
||
}
|
||
|
||
#warning can this be removed or refined?
|
||
- (void)fullUpdateUI
|
||
{
|
||
[self updateUI];
|
||
[self applyFilter];
|
||
[self.fWindow.toolbar validateVisibleItems];
|
||
[self updateTorrentHistory];
|
||
}
|
||
|
||
- (void)setBottomCountText:(BOOL)filtering
|
||
{
|
||
NSString* totalTorrentsString;
|
||
NSUInteger totalCount = self.fTorrents.count;
|
||
if (totalCount != 1)
|
||
{
|
||
totalTorrentsString = [NSString localizedStringWithFormat:NSLocalizedString(@"%lu transfers", "Status bar transfer count"), totalCount];
|
||
}
|
||
else
|
||
{
|
||
totalTorrentsString = NSLocalizedString(@"1 transfer", "Status bar transfer count");
|
||
}
|
||
|
||
if (filtering)
|
||
{
|
||
NSUInteger count = self.fTableView.numberOfRows; //have to factor in collapsed rows
|
||
if (count > 0 && ![self.fDisplayedTorrents[0] isKindOfClass:[Torrent class]])
|
||
{
|
||
count -= self.fDisplayedTorrents.count;
|
||
}
|
||
|
||
totalTorrentsString = [NSString stringWithFormat:NSLocalizedString(@"%@ of %@", "Status bar transfer count"),
|
||
[NSString localizedStringWithFormat:@"%lu", count],
|
||
totalTorrentsString];
|
||
}
|
||
|
||
self.fTotalTorrentsField.stringValue = totalTorrentsString;
|
||
}
|
||
|
||
#pragma mark - UNUserNotificationCenterDelegate
|
||
|
||
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
|
||
willPresentNotification:(UNNotification*)notification
|
||
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
|
||
{
|
||
completionHandler(-1);
|
||
}
|
||
|
||
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
|
||
didReceiveNotificationResponse:(UNNotificationResponse*)response
|
||
withCompletionHandler:(void (^)(void))completionHandler
|
||
{
|
||
if (!response.notification.request.content.userInfo.count)
|
||
{
|
||
completionHandler();
|
||
return;
|
||
}
|
||
|
||
if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier])
|
||
{
|
||
[self didActivateNotificationByDefaultActionWithUserInfo:response.notification.request.content.userInfo];
|
||
}
|
||
else if ([response.actionIdentifier isEqualToString:@"actionShow"])
|
||
{
|
||
[self didActivateNotificationByActionShowWithUserInfo:response.notification.request.content.userInfo];
|
||
}
|
||
completionHandler();
|
||
}
|
||
|
||
- (void)didActivateNotificationByActionShowWithUserInfo:(NSDictionary<NSString*, id>*)userInfo
|
||
{
|
||
Torrent* torrent = [self torrentForHash:userInfo[@"Hash"]];
|
||
NSString* location = torrent.dataLocation;
|
||
if (!location)
|
||
{
|
||
location = userInfo[@"Location"];
|
||
}
|
||
if (location)
|
||
{
|
||
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:@[ [NSURL fileURLWithPath:location] ]];
|
||
}
|
||
}
|
||
|
||
- (void)didActivateNotificationByDefaultActionWithUserInfo:(NSDictionary<NSString*, id>*)userInfo
|
||
{
|
||
Torrent* torrent = [self torrentForHash:userInfo[@"Hash"]];
|
||
if (!torrent)
|
||
{
|
||
return;
|
||
}
|
||
//select in the table - first see if it's already shown
|
||
NSInteger row = [self.fTableView rowForItem:torrent];
|
||
if (row == -1)
|
||
{
|
||
//if it's not shown, see if it's in a collapsed row
|
||
if ([self.fDefaults boolForKey:@"SortByGroup"])
|
||
{
|
||
__block TorrentGroup* parent = nil;
|
||
[self.fDisplayedTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent
|
||
usingBlock:^(TorrentGroup* group, NSUInteger /*idx*/, BOOL* stop) {
|
||
if ([group.torrents containsObject:torrent])
|
||
{
|
||
parent = group;
|
||
*stop = YES;
|
||
}
|
||
}];
|
||
if (parent)
|
||
{
|
||
[[self.fTableView animator] expandItem:parent];
|
||
row = [self.fTableView rowForItem:torrent];
|
||
}
|
||
}
|
||
|
||
if (row == -1)
|
||
{
|
||
//not found - must be filtering
|
||
NSAssert([self.fDefaults boolForKey:@"FilterBar"], @"expected the filter to be enabled");
|
||
[self.fFilterBar reset:YES];
|
||
|
||
row = [self.fTableView rowForItem:torrent];
|
||
|
||
//if it's not shown, it has to be in a collapsed row...again
|
||
if ([self.fDefaults boolForKey:@"SortByGroup"])
|
||
{
|
||
__block TorrentGroup* parent = nil;
|
||
[self.fDisplayedTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent
|
||
usingBlock:^(TorrentGroup* group, NSUInteger /*idx*/, BOOL* stop) {
|
||
if ([group.torrents containsObject:torrent])
|
||
{
|
||
parent = group;
|
||
*stop = YES;
|
||
}
|
||
}];
|
||
if (parent)
|
||
{
|
||
[[self.fTableView animator] expandItem:parent];
|
||
row = [self.fTableView rowForItem:torrent];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
NSAssert1(row != -1, @"expected a row to be found for torrent %@", torrent);
|
||
|
||
[self showMainWindow:nil];
|
||
[self.fTableView selectAndScrollToRow:row];
|
||
}
|
||
|
||
#pragma mark -
|
||
|
||
- (Torrent*)torrentForHash:(NSString*)hash
|
||
{
|
||
NSParameterAssert(hash != nil);
|
||
|
||
__block Torrent* torrent = nil;
|
||
[self.fTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(Torrent* obj, NSUInteger /*idx*/, BOOL* stop) {
|
||
if ([obj.hashString isEqualToString:hash])
|
||
{
|
||
torrent = obj;
|
||
*stop = YES;
|
||
}
|
||
}];
|
||
return torrent;
|
||
}
|
||
|
||
- (void)torrentFinishedDownloading:(NSNotification*)notification
|
||
{
|
||
Torrent* torrent = notification.object;
|
||
|
||
if ([notification.userInfo[@"WasRunning"] boolValue])
|
||
{
|
||
if (!self.fSoundPlaying && [self.fDefaults boolForKey:@"PlayDownloadSound"])
|
||
{
|
||
NSSound* sound;
|
||
if ((sound = [NSSound soundNamed:[self.fDefaults stringForKey:@"DownloadSound"]]))
|
||
{
|
||
sound.delegate = self;
|
||
self.fSoundPlaying = YES;
|
||
[sound play];
|
||
}
|
||
}
|
||
|
||
NSString* title = NSLocalizedString(@"Download Complete", "notification title");
|
||
NSString* body = torrent.name;
|
||
NSString* location = torrent.dataLocation;
|
||
NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithObject:torrent.hashString forKey:@"Hash"];
|
||
if (location)
|
||
{
|
||
userInfo[@"Location"] = location;
|
||
}
|
||
|
||
NSString* identifier = [@"Download Complete " stringByAppendingString:torrent.hashString];
|
||
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
||
content.title = title;
|
||
content.body = body;
|
||
content.categoryIdentifier = @"categoryShow";
|
||
content.userInfo = userInfo;
|
||
|
||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil];
|
||
[UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil];
|
||
|
||
if (!self.fWindow.mainWindow)
|
||
{
|
||
[self.fBadger addCompletedTorrent:torrent];
|
||
}
|
||
|
||
//bounce download stack
|
||
[NSDistributedNotificationCenter.defaultCenter postNotificationName:@"com.apple.DownloadFileFinished"
|
||
object:torrent.dataLocation];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)torrentRestartedDownloading:(NSNotification*)notification
|
||
{
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)torrentFinishedSeeding:(NSNotification*)notification
|
||
{
|
||
Torrent* torrent = notification.object;
|
||
|
||
if (!self.fSoundPlaying && [self.fDefaults boolForKey:@"PlaySeedingSound"])
|
||
{
|
||
NSSound* sound;
|
||
if ((sound = [NSSound soundNamed:[self.fDefaults stringForKey:@"SeedingSound"]]))
|
||
{
|
||
sound.delegate = self;
|
||
self.fSoundPlaying = YES;
|
||
[sound play];
|
||
}
|
||
}
|
||
|
||
NSString* title = NSLocalizedString(@"Seeding Complete", "notification title");
|
||
NSString* body = torrent.name;
|
||
NSString* location = torrent.dataLocation;
|
||
NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithObject:torrent.hashString forKey:@"Hash"];
|
||
if (location)
|
||
{
|
||
userInfo[@"Location"] = location;
|
||
}
|
||
|
||
NSString* identifier = [@"Seeding Complete " stringByAppendingString:torrent.hashString];
|
||
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
||
content.title = title;
|
||
content.body = body;
|
||
content.categoryIdentifier = @"categoryShow";
|
||
content.userInfo = userInfo;
|
||
|
||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil];
|
||
[UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil];
|
||
|
||
//removing from the list calls fullUpdateUI
|
||
if (torrent.removeWhenFinishSeeding)
|
||
{
|
||
[self confirmRemoveTorrents:@[ torrent ] deleteData:NO];
|
||
}
|
||
else
|
||
{
|
||
if (!self.fWindow.mainWindow)
|
||
{
|
||
[self.fBadger addCompletedTorrent:torrent];
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
|
||
if ([self.fTableView.selectedTorrents containsObject:torrent])
|
||
{
|
||
[self.fInfoController updateInfoStats];
|
||
[self.fInfoController updateOptions];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)updateTorrentHistory
|
||
{
|
||
NSMutableArray* history = [NSMutableArray arrayWithCapacity:self.fTorrents.count];
|
||
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
[history addObject:torrent.history];
|
||
self.fTorrentHashes[torrent.hashString] = torrent;
|
||
}
|
||
|
||
NSString* historyFile = [self.fConfigDirectory stringByAppendingPathComponent:kTransferPlist];
|
||
[history writeToFile:historyFile atomically:YES];
|
||
}
|
||
|
||
- (void)setSort:(id)sender
|
||
{
|
||
SortType sortType;
|
||
NSMenuItem* senderMenuItem = sender;
|
||
switch (senderMenuItem.tag)
|
||
{
|
||
case SortTagOrder:
|
||
sortType = SortTypeOrder;
|
||
[self.fDefaults setBool:NO forKey:@"SortReverse"];
|
||
break;
|
||
case SortTagDate:
|
||
sortType = SortTypeDate;
|
||
break;
|
||
case SortTagName:
|
||
sortType = SortTypeName;
|
||
break;
|
||
case SortTagProgress:
|
||
sortType = SortTypeProgress;
|
||
break;
|
||
case SortTagState:
|
||
sortType = SortTypeState;
|
||
break;
|
||
case SortTagTracker:
|
||
sortType = SortTypeTracker;
|
||
break;
|
||
case SortTagActivity:
|
||
sortType = SortTypeActivity;
|
||
break;
|
||
case SortTagSize:
|
||
sortType = SortTypeSize;
|
||
break;
|
||
case SortTagETA:
|
||
sortType = SortTypeETA;
|
||
break;
|
||
default:
|
||
NSAssert1(NO, @"Unknown sort tag received: %ld", senderMenuItem.tag);
|
||
return;
|
||
}
|
||
|
||
[self.fDefaults setObject:sortType forKey:@"Sort"];
|
||
|
||
[self sortTorrentsAndIncludeQueueOrder:YES];
|
||
}
|
||
|
||
- (void)setSortByGroup:(id)sender
|
||
{
|
||
BOOL sortByGroup = ![self.fDefaults boolForKey:@"SortByGroup"];
|
||
[self.fDefaults setBool:sortByGroup forKey:@"SortByGroup"];
|
||
|
||
[self applyFilter];
|
||
}
|
||
|
||
- (void)setSortReverse:(id)sender
|
||
{
|
||
BOOL const setReverse = ((NSMenuItem*)sender).tag == SortOrderTagDescending;
|
||
if (setReverse != [self.fDefaults boolForKey:@"SortReverse"])
|
||
{
|
||
[self.fDefaults setBool:setReverse forKey:@"SortReverse"];
|
||
[self sortTorrentsAndIncludeQueueOrder:NO];
|
||
}
|
||
}
|
||
|
||
- (void)sortTorrentsAndIncludeQueueOrder:(BOOL)includeQueueOrder
|
||
{
|
||
//actually sort
|
||
[self sortTorrentsCallUpdates:YES includeQueueOrder:includeQueueOrder];
|
||
self.fTableView.needsDisplay = YES;
|
||
}
|
||
|
||
- (void)sortTorrentsCallUpdates:(BOOL)callUpdates includeQueueOrder:(BOOL)includeQueueOrder
|
||
{
|
||
BOOL const asc = ![self.fDefaults boolForKey:@"SortReverse"];
|
||
|
||
NSArray* descriptors;
|
||
NSSortDescriptor* nameDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:asc
|
||
selector:@selector(localizedStandardCompare:)];
|
||
|
||
NSString* sortType = [self.fDefaults stringForKey:@"Sort"];
|
||
if ([sortType isEqualToString:SortTypeState])
|
||
{
|
||
NSSortDescriptor* stateDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"stateSortKey" ascending:!asc];
|
||
NSSortDescriptor* progressDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"progress" ascending:!asc];
|
||
NSSortDescriptor* ratioDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"ratio" ascending:!asc];
|
||
|
||
descriptors = @[ stateDescriptor, progressDescriptor, ratioDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeProgress])
|
||
{
|
||
NSSortDescriptor* progressDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"progress" ascending:asc];
|
||
NSSortDescriptor* ratioProgressDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"progressStopRatio" ascending:asc];
|
||
NSSortDescriptor* ratioDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"ratio" ascending:asc];
|
||
|
||
descriptors = @[ progressDescriptor, ratioProgressDescriptor, ratioDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeETA])
|
||
{
|
||
NSSortDescriptor* etaDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"eta" ascending:asc];
|
||
// falling back on sort by progress
|
||
NSSortDescriptor* progressDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"progress" ascending:asc];
|
||
NSSortDescriptor* ratioProgressDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"progressStopRatio" ascending:asc];
|
||
NSSortDescriptor* ratioDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"ratio" ascending:asc];
|
||
|
||
descriptors = @[ etaDescriptor, progressDescriptor, ratioProgressDescriptor, ratioDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeTracker])
|
||
{
|
||
NSSortDescriptor* trackerDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"trackerSortKey" ascending:asc
|
||
selector:@selector(localizedCaseInsensitiveCompare:)];
|
||
|
||
descriptors = @[ trackerDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeActivity])
|
||
{
|
||
NSSortDescriptor* rateDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"totalRate" ascending:asc];
|
||
NSSortDescriptor* activityDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"dateActivityOrAdd" ascending:asc];
|
||
|
||
descriptors = @[ rateDescriptor, activityDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeDate])
|
||
{
|
||
NSSortDescriptor* dateDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"dateAdded" ascending:asc];
|
||
|
||
descriptors = @[ dateDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeSize])
|
||
{
|
||
NSSortDescriptor* sizeDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"totalSizeSelected" ascending:asc];
|
||
|
||
descriptors = @[ sizeDescriptor, nameDescriptor ];
|
||
}
|
||
else if ([sortType isEqualToString:SortTypeName])
|
||
{
|
||
descriptors = @[ nameDescriptor ];
|
||
}
|
||
else
|
||
{
|
||
NSAssert1([sortType isEqualToString:SortTypeOrder], @"Unknown sort type received: %@", sortType);
|
||
|
||
if (!includeQueueOrder)
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSSortDescriptor* orderDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"queuePosition" ascending:asc];
|
||
|
||
descriptors = @[ orderDescriptor ];
|
||
}
|
||
|
||
BOOL beganTableUpdate = !callUpdates;
|
||
|
||
//actually sort
|
||
if ([self.fDefaults boolForKey:@"SortByGroup"])
|
||
{
|
||
for (TorrentGroup* group in self.fDisplayedTorrents)
|
||
{
|
||
[self rearrangeTorrentTableArray:group.torrents forParent:group withSortDescriptors:descriptors
|
||
beganTableUpdate:&beganTableUpdate];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
[self rearrangeTorrentTableArray:self.fDisplayedTorrents forParent:nil withSortDescriptors:descriptors
|
||
beganTableUpdate:&beganTableUpdate];
|
||
}
|
||
|
||
if (beganTableUpdate && callUpdates)
|
||
{
|
||
[self.fTableView endUpdates];
|
||
}
|
||
}
|
||
|
||
#warning redo so that we search a copy once again (best explained by changing sorting from ascending to descending)
|
||
- (void)rearrangeTorrentTableArray:(NSMutableArray*)rearrangeArray
|
||
forParent:parent
|
||
withSortDescriptors:(NSArray*)descriptors
|
||
beganTableUpdate:(BOOL*)beganTableUpdate
|
||
{
|
||
for (NSUInteger currentIndex = 1; currentIndex < rearrangeArray.count; ++currentIndex)
|
||
{
|
||
//manually do the sorting in-place
|
||
NSUInteger const insertIndex = [rearrangeArray indexOfObject:rearrangeArray[currentIndex]
|
||
inSortedRange:NSMakeRange(0, currentIndex)
|
||
options:(NSBinarySearchingInsertionIndex | NSBinarySearchingLastEqual)
|
||
usingComparator:^NSComparisonResult(id obj1, id obj2) {
|
||
for (NSSortDescriptor* descriptor in descriptors)
|
||
{
|
||
NSComparisonResult const result = [descriptor compareObject:obj1
|
||
toObject:obj2];
|
||
if (result != NSOrderedSame)
|
||
{
|
||
return result;
|
||
}
|
||
}
|
||
|
||
return NSOrderedSame;
|
||
}];
|
||
|
||
if (insertIndex != currentIndex)
|
||
{
|
||
if (!*beganTableUpdate)
|
||
{
|
||
*beganTableUpdate = YES;
|
||
[self.fTableView beginUpdates];
|
||
}
|
||
|
||
[rearrangeArray moveObjectAtIndex:currentIndex toIndex:insertIndex];
|
||
[self.fTableView moveItemAtIndex:currentIndex inParent:parent toIndex:insertIndex inParent:parent];
|
||
}
|
||
}
|
||
|
||
NSAssert2(
|
||
[rearrangeArray isEqualToArray:[rearrangeArray sortedArrayUsingDescriptors:descriptors]],
|
||
@"Torrent rearranging didn't work! %@ %@",
|
||
rearrangeArray,
|
||
[rearrangeArray sortedArrayUsingDescriptors:descriptors]);
|
||
}
|
||
|
||
- (void)applyFilter
|
||
{
|
||
NSString* filterType = [self.fDefaults stringForKey:@"Filter"];
|
||
BOOL filterActive = NO, filterDownload = NO, filterSeed = NO, filterPause = NO, filterError = NO, filterStatus = YES;
|
||
if ([filterType isEqualToString:FilterTypeActive])
|
||
{
|
||
filterActive = YES;
|
||
}
|
||
else if ([filterType isEqualToString:FilterTypeDownload])
|
||
{
|
||
filterDownload = YES;
|
||
}
|
||
else if ([filterType isEqualToString:FilterTypeSeed])
|
||
{
|
||
filterSeed = YES;
|
||
}
|
||
else if ([filterType isEqualToString:FilterTypePause])
|
||
{
|
||
filterPause = YES;
|
||
}
|
||
else if ([filterType isEqualToString:FilterTypeError])
|
||
{
|
||
filterError = YES;
|
||
}
|
||
else
|
||
{
|
||
filterStatus = NO;
|
||
}
|
||
|
||
NSInteger const groupFilterValue = [self.fDefaults integerForKey:@"FilterGroup"];
|
||
BOOL const filterGroup = groupFilterValue != kGroupFilterAllTag;
|
||
|
||
NSArray<NSString*>* searchStrings = self.fFilterBar.searchStrings;
|
||
if (searchStrings && searchStrings.count == 0)
|
||
{
|
||
searchStrings = nil;
|
||
}
|
||
BOOL const filterTracker = searchStrings && [[self.fDefaults stringForKey:@"FilterSearchType"] isEqualToString:FilterSearchTypeTracker];
|
||
|
||
std::atomic<int32_t> active{ 0 }, downloading{ 0 }, seeding{ 0 }, paused{ 0 }, error{ 0 };
|
||
// Pointers to be captured by Obj-C Block as const*
|
||
auto* activeRef = &active;
|
||
auto* downloadingRef = &downloading;
|
||
auto* seedingRef = &seeding;
|
||
auto* pausedRef = &paused;
|
||
auto* errorRef = &error;
|
||
//filter & get counts of each type
|
||
NSIndexSet* indexesOfNonFilteredTorrents = [self.fTorrents
|
||
indexesOfObjectsWithOptions:NSEnumerationConcurrent passingTest:^BOOL(Torrent* torrent, NSUInteger /*torrentIdx*/, BOOL* /*stopTorrentsEnumeration*/) {
|
||
//check status
|
||
if (torrent.active && !torrent.checkingWaiting)
|
||
{
|
||
BOOL const isActive = torrent.transmitting;
|
||
if (isActive)
|
||
{
|
||
std::atomic_fetch_add_explicit(activeRef, 1, std::memory_order_relaxed);
|
||
}
|
||
|
||
if (torrent.seeding)
|
||
{
|
||
std::atomic_fetch_add_explicit(seedingRef, 1, std::memory_order_relaxed);
|
||
if (filterStatus && !((filterActive && isActive) || filterSeed))
|
||
{
|
||
return NO;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
std::atomic_fetch_add_explicit(downloadingRef, 1, std::memory_order_relaxed);
|
||
if (filterStatus && !((filterActive && isActive) || filterDownload))
|
||
{
|
||
return NO;
|
||
}
|
||
}
|
||
}
|
||
else if (torrent.error)
|
||
{
|
||
std::atomic_fetch_add_explicit(errorRef, 1, std::memory_order_relaxed);
|
||
if (filterStatus && !filterError)
|
||
{
|
||
return NO;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
std::atomic_fetch_add_explicit(pausedRef, 1, std::memory_order_relaxed);
|
||
if (filterStatus && !filterPause)
|
||
{
|
||
return NO;
|
||
}
|
||
}
|
||
|
||
//checkGroup
|
||
if (filterGroup)
|
||
if (torrent.groupValue != groupFilterValue)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
//check text field
|
||
if (searchStrings)
|
||
{
|
||
__block BOOL removeTextField = NO;
|
||
if (filterTracker)
|
||
{
|
||
NSArray<NSString*>* trackers = torrent.allTrackersFlat;
|
||
|
||
//to count, we need each string in at least 1 tracker
|
||
[searchStrings
|
||
enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSString* searchString, NSUInteger /*idx*/, BOOL* stop) {
|
||
__block BOOL found = NO;
|
||
[trackers enumerateObjectsWithOptions:NSEnumerationConcurrent
|
||
usingBlock:^(NSString* tracker, NSUInteger /*trackerIdx*/, BOOL* stopEnumerateTrackers) {
|
||
if ([tracker rangeOfString:searchString
|
||
options:(NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch)]
|
||
.location != NSNotFound)
|
||
{
|
||
found = YES;
|
||
*stopEnumerateTrackers = YES;
|
||
}
|
||
}];
|
||
if (!found)
|
||
{
|
||
removeTextField = YES;
|
||
*stop = YES;
|
||
}
|
||
}];
|
||
}
|
||
else
|
||
{
|
||
[searchStrings enumerateObjectsWithOptions:NSEnumerationConcurrent
|
||
usingBlock:^(NSString* searchString, NSUInteger /*idx*/, BOOL* stop) {
|
||
if ([torrent.name rangeOfString:searchString
|
||
options:(NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch)]
|
||
.location == NSNotFound)
|
||
{
|
||
removeTextField = YES;
|
||
*stop = YES;
|
||
}
|
||
}];
|
||
}
|
||
|
||
if (removeTextField)
|
||
{
|
||
return NO;
|
||
}
|
||
}
|
||
|
||
return YES;
|
||
}];
|
||
|
||
NSArray<Torrent*>* allTorrents = [self.fTorrents objectsAtIndexes:indexesOfNonFilteredTorrents];
|
||
|
||
//set button tooltips
|
||
if (self.fFilterBar)
|
||
{
|
||
[self.fFilterBar setCountAll:self.fTorrents.count active:active.load() downloading:downloading.load()
|
||
seeding:seeding.load()
|
||
paused:paused.load()
|
||
error:error.load()];
|
||
}
|
||
|
||
//if either the previous or current lists are blank, set its value to the other
|
||
BOOL const groupRows = allTorrents.count > 0 ?
|
||
[self.fDefaults boolForKey:@"SortByGroup"] :
|
||
(self.fDisplayedTorrents.count > 0 && [self.fDisplayedTorrents[0] isKindOfClass:[TorrentGroup class]]);
|
||
BOOL const wasGroupRows = self.fDisplayedTorrents.count > 0 ? [self.fDisplayedTorrents[0] isKindOfClass:[TorrentGroup class]] : groupRows;
|
||
|
||
#warning could probably be merged with later code somehow
|
||
//clear display cache for not-shown torrents
|
||
if (self.fDisplayedTorrents.count > 0)
|
||
{
|
||
//for each torrent, removes the previous piece info if it's not in allTorrents, and keeps track of which torrents we already found in allTorrents
|
||
void (^removePreviousFinishedPieces)(id, NSUInteger, BOOL*) = ^(Torrent* torrent, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
//we used to keep track of which torrents we already found in allTorrents, but it wasn't safe for concurrent enumeration
|
||
if (![allTorrents containsObject:torrent])
|
||
{
|
||
torrent.previousFinishedPieces = nil;
|
||
}
|
||
};
|
||
|
||
if (wasGroupRows)
|
||
{
|
||
[self.fDisplayedTorrents
|
||
enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
[((TorrentGroup*)obj).torrents enumerateObjectsWithOptions:NSEnumerationConcurrent
|
||
usingBlock:removePreviousFinishedPieces];
|
||
}];
|
||
}
|
||
else
|
||
{
|
||
[self.fDisplayedTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:removePreviousFinishedPieces];
|
||
}
|
||
}
|
||
|
||
BOOL beganUpdates = NO;
|
||
|
||
//don't animate torrents when first launching
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
NSAnimationContext.currentContext.duration = 0;
|
||
});
|
||
[NSAnimationContext beginGrouping];
|
||
|
||
//add/remove torrents (and rearrange for groups), one by one
|
||
if (!groupRows && !wasGroupRows)
|
||
{
|
||
NSMutableIndexSet* addIndexes = [NSMutableIndexSet indexSet];
|
||
NSMutableIndexSet* removePreviousIndexes = [NSMutableIndexSet
|
||
indexSetWithIndexesInRange:NSMakeRange(0, self.fDisplayedTorrents.count)];
|
||
|
||
//for each of the torrents to add, find if it already exists (and keep track of those we've already added & those we need to remove)
|
||
[allTorrents enumerateObjectsWithOptions:0 usingBlock:^(Torrent* obj, NSUInteger previousIndex, BOOL* /*stopEnumerate*/) {
|
||
NSUInteger const currentIndex = [self.fDisplayedTorrents
|
||
indexOfObjectAtIndexes:removePreviousIndexes
|
||
options:NSEnumerationConcurrent
|
||
passingTest:^BOOL(id objDisplay, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
return obj == objDisplay;
|
||
}];
|
||
if (currentIndex == NSNotFound)
|
||
{
|
||
[addIndexes addIndex:previousIndex];
|
||
}
|
||
else
|
||
{
|
||
[removePreviousIndexes removeIndex:currentIndex];
|
||
}
|
||
}];
|
||
|
||
if (addIndexes.count > 0 || removePreviousIndexes.count > 0)
|
||
{
|
||
beganUpdates = YES;
|
||
[self.fTableView beginUpdates];
|
||
|
||
//remove torrents we didn't find
|
||
if (removePreviousIndexes.count > 0)
|
||
{
|
||
[self.fDisplayedTorrents removeObjectsAtIndexes:removePreviousIndexes];
|
||
[self.fTableView removeItemsAtIndexes:removePreviousIndexes inParent:nil withAnimation:NSTableViewAnimationSlideDown];
|
||
}
|
||
|
||
//add new torrents
|
||
if (addIndexes.count > 0)
|
||
{
|
||
//slide new torrents in differently
|
||
if (self.fAddingTransfers)
|
||
{
|
||
NSIndexSet* newAddIndexes = [allTorrents
|
||
indexesOfObjectsAtIndexes:addIndexes
|
||
options:NSEnumerationConcurrent
|
||
passingTest:^BOOL(Torrent* obj, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
return [self.fAddingTransfers containsObject:obj];
|
||
}];
|
||
|
||
[addIndexes removeIndexes:newAddIndexes];
|
||
|
||
[self.fDisplayedTorrents addObjectsFromArray:[allTorrents objectsAtIndexes:newAddIndexes]];
|
||
[self.fTableView insertItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(
|
||
self.fDisplayedTorrents.count -
|
||
newAddIndexes.count,
|
||
newAddIndexes.count)]
|
||
inParent:nil
|
||
withAnimation:NSTableViewAnimationSlideLeft];
|
||
}
|
||
|
||
[self.fDisplayedTorrents addObjectsFromArray:[allTorrents objectsAtIndexes:addIndexes]];
|
||
[self.fTableView
|
||
insertItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(
|
||
self.fDisplayedTorrents.count - addIndexes.count,
|
||
addIndexes.count)]
|
||
inParent:nil
|
||
withAnimation:NSTableViewAnimationSlideDown];
|
||
}
|
||
}
|
||
}
|
||
else if (groupRows && wasGroupRows)
|
||
{
|
||
NSAssert(groupRows && wasGroupRows, @"Should have had group rows and should remain with group rows");
|
||
|
||
#warning don't always do?
|
||
beganUpdates = YES;
|
||
[self.fTableView beginUpdates];
|
||
|
||
NSMutableIndexSet* unusedAllTorrentsIndexes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, allTorrents.count)];
|
||
|
||
NSMutableDictionary* groupsByIndex = [NSMutableDictionary dictionaryWithCapacity:self.fDisplayedTorrents.count];
|
||
for (TorrentGroup* group in self.fDisplayedTorrents)
|
||
{
|
||
groupsByIndex[@(group.groupIndex)] = group;
|
||
}
|
||
|
||
NSUInteger const originalGroupCount = self.fDisplayedTorrents.count;
|
||
for (NSUInteger index = 0; index < originalGroupCount; ++index)
|
||
{
|
||
TorrentGroup* group = self.fDisplayedTorrents[index];
|
||
|
||
NSMutableIndexSet* removeIndexes = [NSMutableIndexSet indexSet];
|
||
|
||
//needs to be a signed integer
|
||
for (NSUInteger indexInGroup = 0; indexInGroup < group.torrents.count; ++indexInGroup)
|
||
{
|
||
Torrent* torrent = group.torrents[indexInGroup];
|
||
NSUInteger const allIndex = [allTorrents indexOfObjectAtIndexes:unusedAllTorrentsIndexes options:NSEnumerationConcurrent
|
||
passingTest:^BOOL(Torrent* obj, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
return obj == torrent;
|
||
}];
|
||
if (allIndex == NSNotFound)
|
||
{
|
||
[removeIndexes addIndex:indexInGroup];
|
||
}
|
||
else
|
||
{
|
||
BOOL markTorrentAsUsed = YES;
|
||
|
||
NSInteger const groupValue = torrent.groupValue;
|
||
if (groupValue != group.groupIndex)
|
||
{
|
||
TorrentGroup* newGroup = groupsByIndex[@(groupValue)];
|
||
if (!newGroup)
|
||
{
|
||
newGroup = [[TorrentGroup alloc] initWithGroup:groupValue];
|
||
groupsByIndex[@(groupValue)] = newGroup;
|
||
[self.fDisplayedTorrents addObject:newGroup];
|
||
|
||
[self.fTableView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:self.fDisplayedTorrents.count - 1]
|
||
inParent:nil
|
||
withAnimation:NSTableViewAnimationEffectFade];
|
||
[self.fTableView isGroupCollapsed:groupValue] ? [self.fTableView collapseItem:newGroup] :
|
||
[self.fTableView expandItem:newGroup];
|
||
}
|
||
else //if we haven't processed the other group yet, we have to make sure we don't flag it for removal the next time
|
||
{
|
||
//ugggh, but shouldn't happen too often
|
||
if ([self.fDisplayedTorrents indexOfObject:newGroup
|
||
inRange:NSMakeRange(index + 1, originalGroupCount - (index + 1))] != NSNotFound)
|
||
{
|
||
markTorrentAsUsed = NO;
|
||
}
|
||
}
|
||
|
||
[group.torrents removeObjectAtIndex:indexInGroup];
|
||
[newGroup.torrents addObject:torrent];
|
||
|
||
[self.fTableView moveItemAtIndex:indexInGroup inParent:group toIndex:newGroup.torrents.count - 1
|
||
inParent:newGroup];
|
||
|
||
--indexInGroup;
|
||
}
|
||
|
||
if (markTorrentAsUsed)
|
||
{
|
||
[unusedAllTorrentsIndexes removeIndex:allIndex];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (removeIndexes.count > 0)
|
||
{
|
||
[group.torrents removeObjectsAtIndexes:removeIndexes];
|
||
[self.fTableView removeItemsAtIndexes:removeIndexes inParent:group withAnimation:NSTableViewAnimationEffectFade];
|
||
}
|
||
}
|
||
|
||
//add remaining new torrents
|
||
for (Torrent* torrent in [allTorrents objectsAtIndexes:unusedAllTorrentsIndexes])
|
||
{
|
||
NSInteger const groupValue = torrent.groupValue;
|
||
TorrentGroup* group = groupsByIndex[@(groupValue)];
|
||
if (!group)
|
||
{
|
||
group = [[TorrentGroup alloc] initWithGroup:groupValue];
|
||
groupsByIndex[@(groupValue)] = group;
|
||
[self.fDisplayedTorrents addObject:group];
|
||
|
||
[self.fTableView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:self.fDisplayedTorrents.count - 1] inParent:nil
|
||
withAnimation:NSTableViewAnimationEffectFade];
|
||
[self.fTableView isGroupCollapsed:groupValue] ? [self.fTableView collapseItem:group] : [self.fTableView expandItem:group];
|
||
}
|
||
|
||
[group.torrents addObject:torrent];
|
||
|
||
BOOL const newTorrent = [self.fAddingTransfers containsObject:torrent];
|
||
[self.fTableView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:group.torrents.count - 1] inParent:group
|
||
withAnimation:newTorrent ? NSTableViewAnimationSlideLeft : NSTableViewAnimationSlideDown];
|
||
}
|
||
|
||
//remove empty groups
|
||
NSIndexSet* removeGroupIndexes = [self.fDisplayedTorrents
|
||
indexesOfObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, originalGroupCount)]
|
||
options:NSEnumerationConcurrent passingTest:^BOOL(id obj, NSUInteger /*idx*/, BOOL* /*stop*/) {
|
||
return ((TorrentGroup*)obj).torrents.count == 0;
|
||
}];
|
||
|
||
if (removeGroupIndexes.count > 0)
|
||
{
|
||
[self.fDisplayedTorrents removeObjectsAtIndexes:removeGroupIndexes];
|
||
[self.fTableView removeItemsAtIndexes:removeGroupIndexes inParent:nil withAnimation:NSTableViewAnimationEffectFade];
|
||
}
|
||
|
||
//now that all groups are there, sort them - don't insert on the fly in case groups were reordered in prefs
|
||
NSSortDescriptor* groupDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"groupOrderValue" ascending:YES];
|
||
[self rearrangeTorrentTableArray:self.fDisplayedTorrents forParent:nil withSortDescriptors:@[ groupDescriptor ]
|
||
beganTableUpdate:&beganUpdates];
|
||
}
|
||
else
|
||
{
|
||
NSAssert(groupRows != wasGroupRows, @"Trying toggling group-torrent reordering when we weren't expecting to.");
|
||
|
||
//set all groups as expanded
|
||
[self.fTableView removeAllCollapsedGroups];
|
||
|
||
// we need to remember selected values
|
||
NSArray<Torrent*>* selectedTorrents = self.fTableView.selectedTorrents;
|
||
|
||
beganUpdates = YES;
|
||
[self.fTableView beginUpdates];
|
||
|
||
[self.fTableView removeItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.fDisplayedTorrents.count)]
|
||
inParent:nil
|
||
withAnimation:NSTableViewAnimationSlideDown];
|
||
|
||
if (groupRows)
|
||
{
|
||
//a map for quickly finding groups
|
||
NSMutableDictionary* groupsByIndex = [NSMutableDictionary dictionaryWithCapacity:GroupsController.groups.numberOfGroups];
|
||
for (Torrent* torrent in allTorrents)
|
||
{
|
||
NSInteger const groupValue = torrent.groupValue;
|
||
TorrentGroup* group = groupsByIndex[@(groupValue)];
|
||
if (!group)
|
||
{
|
||
group = [[TorrentGroup alloc] initWithGroup:groupValue];
|
||
groupsByIndex[@(groupValue)] = group;
|
||
}
|
||
|
||
[group.torrents addObject:torrent];
|
||
}
|
||
|
||
[self.fDisplayedTorrents setArray:groupsByIndex.allValues];
|
||
|
||
//we need the groups to be sorted, and we can do it without moving items in the table, too!
|
||
NSSortDescriptor* groupDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"groupOrderValue" ascending:YES];
|
||
[self.fDisplayedTorrents sortUsingDescriptors:@[ groupDescriptor ]];
|
||
}
|
||
else
|
||
[self.fDisplayedTorrents setArray:allTorrents];
|
||
|
||
[self.fTableView insertItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.fDisplayedTorrents.count)]
|
||
inParent:nil
|
||
withAnimation:NSTableViewAnimationEffectFade];
|
||
|
||
if (groupRows)
|
||
{
|
||
//actually expand group rows
|
||
for (TorrentGroup* group in self.fDisplayedTorrents)
|
||
[self.fTableView expandItem:group];
|
||
}
|
||
|
||
self.fTableView.selectedTorrents = selectedTorrents;
|
||
}
|
||
|
||
//sort the torrents (won't sort the groups, though)
|
||
[self sortTorrentsCallUpdates:!beganUpdates includeQueueOrder:YES];
|
||
|
||
if (beganUpdates)
|
||
{
|
||
[self.fTableView endUpdates];
|
||
}
|
||
[NSAnimationContext endGrouping];
|
||
|
||
//reloaddata, otherwise the tableview has a bunch of empty cells
|
||
[self.fTableView reloadData];
|
||
|
||
[self resetInfo]; //if group is already selected, but the torrents in it change
|
||
|
||
[self setBottomCountText:groupRows || filterStatus || filterGroup || searchStrings];
|
||
|
||
[self setWindowSizeToFit];
|
||
|
||
if (self.fAddingTransfers)
|
||
{
|
||
self.fAddingTransfers = nil;
|
||
}
|
||
}
|
||
|
||
- (void)switchFilter:(id)sender
|
||
{
|
||
[self.fFilterBar switchFilter:sender == self.fNextFilterItem];
|
||
}
|
||
|
||
- (IBAction)showGlobalPopover:(id)sender
|
||
{
|
||
if (self.fGlobalPopoverShown)
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSPopover* popover = [[NSPopover alloc] init];
|
||
popover.behavior = NSPopoverBehaviorTransient;
|
||
GlobalOptionsPopoverViewController* viewController = [[GlobalOptionsPopoverViewController alloc] initWithHandle:self.fLib];
|
||
popover.contentViewController = viewController;
|
||
popover.delegate = self;
|
||
|
||
NSView* senderView = sender;
|
||
CGFloat width = NSWidth(senderView.frame);
|
||
|
||
if (NSMinX(self.fWindow.frame) < width || NSMaxX(self.fWindow.screen.visibleFrame) - NSMinX(self.fWindow.frame) < width * 2)
|
||
{
|
||
// Ugly hack to hide NSPopover arrow.
|
||
self.fPositioningView = [[NSView alloc] initWithFrame:senderView.bounds];
|
||
self.fPositioningView.identifier = @"positioningView";
|
||
[senderView addSubview:self.fPositioningView];
|
||
[popover showRelativeToRect:self.fPositioningView.bounds ofView:self.fPositioningView preferredEdge:NSMaxYEdge];
|
||
self.fPositioningView.bounds = NSOffsetRect(self.fPositioningView.bounds, 0, NSHeight(self.fPositioningView.bounds));
|
||
}
|
||
else
|
||
{
|
||
[popover showRelativeToRect:senderView.bounds ofView:senderView preferredEdge:NSMaxYEdge];
|
||
}
|
||
}
|
||
|
||
//don't show multiple popovers when clicking the gear button repeatedly
|
||
- (void)popoverWillShow:(NSNotification*)notification
|
||
{
|
||
self.fGlobalPopoverShown = YES;
|
||
}
|
||
|
||
- (void)popoverDidClose:(NSNotification*)notification
|
||
{
|
||
[self.fPositioningView removeFromSuperview];
|
||
self.fGlobalPopoverShown = NO;
|
||
}
|
||
|
||
- (void)menuNeedsUpdate:(NSMenu*)menu
|
||
{
|
||
if (menu == self.fGroupsSetMenu || menu == self.fGroupsSetContextMenu)
|
||
{
|
||
[menu removeAllItems];
|
||
|
||
NSMenu* groupMenu = [GroupsController.groups groupMenuWithTarget:self action:@selector(setGroup:) isSmall:NO];
|
||
|
||
NSInteger const groupMenuCount = groupMenu.numberOfItems;
|
||
for (NSInteger i = 0; i < groupMenuCount; i++)
|
||
{
|
||
NSMenuItem* item = [groupMenu itemAtIndex:0];
|
||
[groupMenu removeItemAtIndex:0];
|
||
[menu addItem:item];
|
||
}
|
||
}
|
||
else if (menu == self.fShareMenu || menu == self.fShareContextMenu)
|
||
{
|
||
[menu removeAllItems];
|
||
|
||
for (NSMenuItem* item in ShareTorrentFileHelper.sharedHelper.menuItems)
|
||
{
|
||
[menu addItem:item];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)setGroup:(id)sender
|
||
{
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
[self.fTableView removeCollapsedGroup:torrent.groupValue]; //remove old collapsed group
|
||
|
||
[torrent setGroupValue:((NSMenuItem*)sender).tag determinationType:TorrentDeterminationUserSpecified];
|
||
}
|
||
|
||
[self applyFilter];
|
||
[self updateUI];
|
||
[self updateTorrentHistory];
|
||
}
|
||
|
||
- (void)toggleSpeedLimit:(id)sender
|
||
{
|
||
[self.fDefaults setBool:![self.fDefaults boolForKey:@"SpeedLimit"] forKey:@"SpeedLimit"];
|
||
[self speedLimitChanged:sender];
|
||
}
|
||
|
||
- (void)speedLimitChanged:(id)sender
|
||
{
|
||
tr_sessionUseAltSpeed(self.fLib, [self.fDefaults boolForKey:@"SpeedLimit"]);
|
||
[self.fStatusBar updateSpeedFieldsToolTips];
|
||
}
|
||
|
||
- (void)altSpeedToggledCallbackIsLimited:(NSDictionary*)dict
|
||
{
|
||
BOOL const isLimited = [dict[@"Active"] boolValue];
|
||
|
||
[self.fDefaults setBool:isLimited forKey:@"SpeedLimit"];
|
||
[self.fStatusBar updateSpeedFieldsToolTips];
|
||
|
||
if (![dict[@"ByUser"] boolValue])
|
||
{
|
||
NSString* title = isLimited ? NSLocalizedString(@"Speed Limit Auto Enabled", "notification title") :
|
||
NSLocalizedString(@"Speed Limit Auto Disabled", "notification title");
|
||
NSString* body = NSLocalizedString(@"Bandwidth settings changed", "notification description");
|
||
|
||
NSString* identifier = @"Bandwidth settings changed";
|
||
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
||
content.title = title;
|
||
content.body = body;
|
||
|
||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil];
|
||
[UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil];
|
||
}
|
||
}
|
||
|
||
- (void)sound:(NSSound*)sound didFinishPlaying:(BOOL)finishedPlaying
|
||
{
|
||
self.fSoundPlaying = NO;
|
||
}
|
||
|
||
- (void)VDKQueue:(VDKQueue*)queue receivedNotification:(NSString*)notification forPath:(NSString*)fpath
|
||
{
|
||
//don't assume that just because we're watching for write notification, we'll only receive write notifications
|
||
|
||
if (![self.fDefaults boolForKey:@"AutoImport"] || ![self.fDefaults stringForKey:@"AutoImportDirectory"])
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (self.fAutoImportTimer.valid)
|
||
{
|
||
[self.fAutoImportTimer invalidate];
|
||
}
|
||
|
||
//check again in 10 seconds in case torrent file wasn't complete
|
||
self.fAutoImportTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self
|
||
selector:@selector(checkAutoImportDirectory)
|
||
userInfo:nil
|
||
repeats:NO];
|
||
|
||
[self checkAutoImportDirectory];
|
||
}
|
||
|
||
- (void)changeAutoImport
|
||
{
|
||
if (self.fAutoImportTimer.valid)
|
||
{
|
||
[self.fAutoImportTimer invalidate];
|
||
}
|
||
self.fAutoImportTimer = nil;
|
||
|
||
self.fAutoImportedNames = nil;
|
||
|
||
[self checkAutoImportDirectory];
|
||
}
|
||
|
||
- (void)checkAutoImportDirectory
|
||
{
|
||
NSString* path;
|
||
if (![self.fDefaults boolForKey:@"AutoImport"] || !(path = [self.fDefaults stringForKey:@"AutoImportDirectory"]))
|
||
{
|
||
return;
|
||
}
|
||
|
||
path = path.stringByExpandingTildeInPath;
|
||
|
||
NSArray<NSString*>* importedNames;
|
||
if (!(importedNames = [NSFileManager.defaultManager contentsOfDirectoryAtPath:path error:NULL]))
|
||
{
|
||
return;
|
||
}
|
||
|
||
//only check files that have not been checked yet
|
||
NSMutableArray* newNames = [importedNames mutableCopy];
|
||
|
||
if (self.fAutoImportedNames)
|
||
{
|
||
[newNames removeObjectsInArray:self.fAutoImportedNames];
|
||
}
|
||
else
|
||
{
|
||
self.fAutoImportedNames = [[NSMutableArray alloc] init];
|
||
}
|
||
[self.fAutoImportedNames setArray:importedNames];
|
||
|
||
for (NSString* file in newNames)
|
||
{
|
||
if ([file hasPrefix:@"."])
|
||
{
|
||
continue;
|
||
}
|
||
|
||
NSString* fullFile = [path stringByAppendingPathComponent:file];
|
||
|
||
if (!([[NSWorkspace.sharedWorkspace typeOfFile:fullFile error:NULL] isEqualToString:@"org.bittorrent.torrent"] ||
|
||
[fullFile.pathExtension caseInsensitiveCompare:@"torrent"] == NSOrderedSame))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
NSDictionary<NSFileAttributeKey, id>* fileAttributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullFile
|
||
error:nil];
|
||
if (fileAttributes.fileSize == 0)
|
||
{
|
||
// Workaround for Firefox downloads happening in two steps: first time being an empty file
|
||
[self.fAutoImportedNames removeObject:file];
|
||
continue;
|
||
}
|
||
|
||
auto metainfo = tr_torrent_metainfo{};
|
||
if (!metainfo.parse_torrent_file(fullFile.UTF8String))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
[self openFiles:@[ fullFile ] addType:AddTypeAuto forcePath:nil];
|
||
|
||
NSString* notificationTitle = NSLocalizedString(@"Torrent File Auto Added", "notification title");
|
||
|
||
NSString* identifier = [@"Torrent File Auto Added " stringByAppendingString:file];
|
||
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
||
content.title = notificationTitle;
|
||
content.body = file;
|
||
|
||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil];
|
||
[UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil];
|
||
}
|
||
}
|
||
|
||
- (void)beginCreateFile:(NSNotification*)notification
|
||
{
|
||
if (![self.fDefaults boolForKey:@"AutoImport"])
|
||
{
|
||
return;
|
||
}
|
||
|
||
NSString *location = ((NSURL*)notification.object).path, *path = [self.fDefaults stringForKey:@"AutoImportDirectory"];
|
||
|
||
if (location && path && [location.stringByDeletingLastPathComponent.stringByExpandingTildeInPath isEqualToString:path.stringByExpandingTildeInPath])
|
||
{
|
||
[self.fAutoImportedNames addObject:location.lastPathComponent];
|
||
}
|
||
}
|
||
|
||
- (NSInteger)outlineView:(NSOutlineView*)outlineView numberOfChildrenOfItem:(id)item
|
||
{
|
||
if (item)
|
||
{
|
||
return ((TorrentGroup*)item).torrents.count;
|
||
}
|
||
else
|
||
{
|
||
return self.fDisplayedTorrents.count;
|
||
}
|
||
}
|
||
|
||
- (id)outlineView:(NSOutlineView*)outlineView child:(NSInteger)index ofItem:(id)item
|
||
{
|
||
if (item)
|
||
{
|
||
return ((TorrentGroup*)item).torrents[index];
|
||
}
|
||
else
|
||
{
|
||
return self.fDisplayedTorrents[index];
|
||
}
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView isItemExpandable:(id)item
|
||
{
|
||
return ![item isKindOfClass:[Torrent class]];
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView writeItems:(NSArray*)items toPasteboard:(NSPasteboard*)pasteboard
|
||
{
|
||
//only allow reordering of rows if sorting by order
|
||
if ([self.fDefaults boolForKey:@"SortByGroup"] || [[self.fDefaults stringForKey:@"Sort"] isEqualToString:SortTypeOrder])
|
||
{
|
||
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
|
||
for (id torrent in items)
|
||
{
|
||
if (![torrent isKindOfClass:[Torrent class]])
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
[indexSet addIndex:[self.fTableView rowForItem:torrent]];
|
||
}
|
||
|
||
[pasteboard declareTypes:@[ kTorrentTableViewDataType ] owner:self];
|
||
[pasteboard setData:[NSKeyedArchiver archivedDataWithRootObject:indexSet requiringSecureCoding:YES error:nil]
|
||
forType:kTorrentTableViewDataType];
|
||
return YES;
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
- (NSDragOperation)outlineView:(NSOutlineView*)outlineView
|
||
validateDrop:(id<NSDraggingInfo>)info
|
||
proposedItem:(id)item
|
||
proposedChildIndex:(NSInteger)index
|
||
{
|
||
NSPasteboard* pasteboard = info.draggingPasteboard;
|
||
if ([pasteboard.types containsObject:kTorrentTableViewDataType])
|
||
{
|
||
if ([self.fDefaults boolForKey:@"SortByGroup"])
|
||
{
|
||
if (!item)
|
||
{
|
||
return NSDragOperationNone;
|
||
}
|
||
|
||
if ([[self.fDefaults stringForKey:@"Sort"] isEqualToString:SortTypeOrder])
|
||
{
|
||
if ([item isKindOfClass:[Torrent class]])
|
||
{
|
||
TorrentGroup* group = [self.fTableView parentForItem:item];
|
||
index = [group.torrents indexOfObject:item] + 1;
|
||
item = group;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if ([item isKindOfClass:[Torrent class]])
|
||
{
|
||
item = [self.fTableView parentForItem:item];
|
||
}
|
||
index = NSOutlineViewDropOnItemIndex;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (index == NSOutlineViewDropOnItemIndex)
|
||
{
|
||
return NSDragOperationNone;
|
||
}
|
||
|
||
if (item)
|
||
{
|
||
index = [self.fTableView rowForItem:item] + 1;
|
||
item = nil;
|
||
}
|
||
}
|
||
|
||
[self.fTableView setDropItem:item dropChildIndex:index];
|
||
return NSDragOperationGeneric;
|
||
}
|
||
|
||
return NSDragOperationNone;
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView acceptDrop:(id<NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)newRow
|
||
{
|
||
NSPasteboard* pasteboard = info.draggingPasteboard;
|
||
if ([pasteboard.types containsObject:kTorrentTableViewDataType])
|
||
{
|
||
NSIndexSet* indexes = [NSKeyedUnarchiver unarchivedObjectOfClass:NSIndexSet.class fromData:[pasteboard dataForType:kTorrentTableViewDataType]
|
||
error:nil];
|
||
|
||
//get the torrents to move
|
||
NSMutableArray* movingTorrents = [NSMutableArray arrayWithCapacity:indexes.count];
|
||
for (NSUInteger i = indexes.firstIndex; i != NSNotFound; i = [indexes indexGreaterThanIndex:i])
|
||
{
|
||
Torrent* torrent = [self.fTableView itemAtRow:i];
|
||
[movingTorrents addObject:torrent];
|
||
}
|
||
|
||
//change groups
|
||
if (item)
|
||
{
|
||
TorrentGroup* group = (TorrentGroup*)item;
|
||
NSInteger const groupIndex = group.groupIndex;
|
||
|
||
for (Torrent* torrent in movingTorrents)
|
||
{
|
||
[torrent setGroupValue:groupIndex determinationType:TorrentDeterminationUserSpecified];
|
||
}
|
||
}
|
||
|
||
//reorder queue order
|
||
if (newRow != NSOutlineViewDropOnItemIndex)
|
||
{
|
||
TorrentGroup* group = (TorrentGroup*)item;
|
||
//find torrent to place under
|
||
NSArray* groupTorrents = group ? group.torrents : self.fDisplayedTorrents;
|
||
Torrent* topTorrent = nil;
|
||
for (NSInteger i = newRow - 1; i >= 0; i--)
|
||
{
|
||
Torrent* tempTorrent = groupTorrents[i];
|
||
if (![movingTorrents containsObject:tempTorrent])
|
||
{
|
||
topTorrent = tempTorrent;
|
||
break;
|
||
}
|
||
}
|
||
|
||
//remove objects to reinsert
|
||
[self.fTorrents removeObjectsInArray:movingTorrents];
|
||
|
||
//insert objects at new location
|
||
NSUInteger const insertIndex = topTorrent ? [self.fTorrents indexOfObject:topTorrent] + 1 : 0;
|
||
NSIndexSet* insertIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, movingTorrents.count)];
|
||
[self.fTorrents insertObjects:movingTorrents atIndexes:insertIndexes];
|
||
|
||
//we need to make sure the queue order is updated in the Torrent object before we sort - safest to just reset all queue positions
|
||
NSUInteger i = 0;
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
torrent.queuePosition = i++;
|
||
[torrent update];
|
||
}
|
||
|
||
//do the drag animation here so that the dragged torrents are the ones that are animated as moving, and not the torrents around them
|
||
[self.fTableView beginUpdates];
|
||
|
||
NSUInteger insertDisplayIndex = topTorrent ? [groupTorrents indexOfObject:topTorrent] + 1 : 0;
|
||
|
||
for (Torrent* torrent in movingTorrents)
|
||
{
|
||
TorrentGroup* oldParent = item ? [self.fTableView parentForItem:torrent] : nil;
|
||
NSMutableArray* oldTorrents = oldParent ? oldParent.torrents : self.fDisplayedTorrents;
|
||
NSUInteger const oldIndex = [oldTorrents indexOfObject:torrent];
|
||
|
||
if (item == oldParent)
|
||
{
|
||
if (oldIndex < insertDisplayIndex)
|
||
{
|
||
--insertDisplayIndex;
|
||
}
|
||
[oldTorrents moveObjectAtIndex:oldIndex toIndex:insertDisplayIndex];
|
||
}
|
||
else
|
||
{
|
||
NSAssert(item && oldParent, @"Expected to be dragging between group rows");
|
||
|
||
NSMutableArray* newTorrents = ((TorrentGroup*)item).torrents;
|
||
[newTorrents insertObject:torrent atIndex:insertDisplayIndex];
|
||
[oldTorrents removeObjectAtIndex:oldIndex];
|
||
}
|
||
|
||
[self.fTableView moveItemAtIndex:oldIndex inParent:oldParent toIndex:insertDisplayIndex inParent:item];
|
||
|
||
++insertDisplayIndex;
|
||
}
|
||
|
||
[self.fTableView endUpdates];
|
||
}
|
||
|
||
[self applyFilter];
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (void)torrentTableViewSelectionDidChange:(NSNotification*)notification
|
||
{
|
||
[self resetInfo];
|
||
[self.fWindow.toolbar validateVisibleItems];
|
||
}
|
||
|
||
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info
|
||
{
|
||
NSPasteboard* pasteboard = info.draggingPasteboard;
|
||
if ([pasteboard.types containsObject:NSPasteboardTypeFileURL])
|
||
{
|
||
//check if any torrent files can be added
|
||
BOOL torrent = NO;
|
||
NSArray<NSURL*>* files = [pasteboard readObjectsForClasses:@[ NSURL.class ]
|
||
options:@{ NSPasteboardURLReadingFileURLsOnlyKey : @YES }];
|
||
for (NSURL* fileToParse in files)
|
||
{
|
||
if ([[NSWorkspace.sharedWorkspace typeOfFile:fileToParse.path error:NULL] isEqualToString:@"org.bittorrent.torrent"] ||
|
||
[fileToParse.pathExtension caseInsensitiveCompare:@"torrent"] == NSOrderedSame)
|
||
{
|
||
torrent = YES;
|
||
auto metainfo = tr_torrent_metainfo{};
|
||
if (metainfo.parse_torrent_file(fileToParse.path.UTF8String))
|
||
{
|
||
if (!self.fOverlayWindow)
|
||
{
|
||
self.fOverlayWindow = [[DragOverlayWindow alloc] initForWindow:self.fWindow];
|
||
}
|
||
NSMutableArray<NSString*>* filesToOpen = [NSMutableArray arrayWithCapacity:files.count];
|
||
for (NSURL* fileToOpen in files)
|
||
{
|
||
[filesToOpen addObject:fileToOpen.path];
|
||
}
|
||
[self.fOverlayWindow setTorrents:filesToOpen];
|
||
|
||
return NSDragOperationCopy;
|
||
}
|
||
}
|
||
}
|
||
|
||
//create a torrent file if a single file
|
||
if (!torrent && files.count == 1)
|
||
{
|
||
if (!self.fOverlayWindow)
|
||
{
|
||
self.fOverlayWindow = [[DragOverlayWindow alloc] initForWindow:self.fWindow];
|
||
}
|
||
[self.fOverlayWindow setFile:[files[0] lastPathComponent]];
|
||
|
||
return NSDragOperationCopy;
|
||
}
|
||
}
|
||
else if ([pasteboard.types containsObject:NSPasteboardTypeURL])
|
||
{
|
||
if (!self.fOverlayWindow)
|
||
{
|
||
self.fOverlayWindow = [[DragOverlayWindow alloc] initForWindow:self.fWindow];
|
||
}
|
||
[self.fOverlayWindow setURL:[NSURL URLFromPasteboard:pasteboard].relativeString];
|
||
|
||
return NSDragOperationCopy;
|
||
}
|
||
|
||
return NSDragOperationNone;
|
||
}
|
||
|
||
- (void)draggingExited:(id<NSDraggingInfo>)info
|
||
{
|
||
if (self.fOverlayWindow)
|
||
{
|
||
[self.fOverlayWindow fadeOut];
|
||
}
|
||
}
|
||
|
||
- (BOOL)performDragOperation:(id<NSDraggingInfo>)info
|
||
{
|
||
if (self.fOverlayWindow)
|
||
{
|
||
[self.fOverlayWindow fadeOut];
|
||
}
|
||
|
||
NSPasteboard* pasteboard = info.draggingPasteboard;
|
||
if ([pasteboard.types containsObject:NSPasteboardTypeFileURL])
|
||
{
|
||
BOOL torrent = NO, accept = YES;
|
||
|
||
//create an array of files that can be opened
|
||
NSArray<NSURL*>* files = [pasteboard readObjectsForClasses:@[ NSURL.class ]
|
||
options:@{ NSPasteboardURLReadingFileURLsOnlyKey : @YES }];
|
||
NSMutableArray<NSString*>* filesToOpen = [NSMutableArray arrayWithCapacity:files.count];
|
||
for (NSURL* file in files)
|
||
{
|
||
if ([[NSWorkspace.sharedWorkspace typeOfFile:file.path error:NULL] isEqualToString:@"org.bittorrent.torrent"] ||
|
||
[file.pathExtension caseInsensitiveCompare:@"torrent"] == NSOrderedSame)
|
||
{
|
||
torrent = YES;
|
||
auto metainfo = tr_torrent_metainfo{};
|
||
if (metainfo.parse_torrent_file(file.path.UTF8String))
|
||
{
|
||
[filesToOpen addObject:file.path];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (filesToOpen.count > 0)
|
||
{
|
||
[self application:NSApp openFiles:filesToOpen];
|
||
}
|
||
else
|
||
{
|
||
if (!torrent && files.count == 1)
|
||
{
|
||
[CreatorWindowController createTorrentFile:self.fLib forFile:files[0]];
|
||
}
|
||
else
|
||
{
|
||
accept = NO;
|
||
}
|
||
}
|
||
|
||
return accept;
|
||
}
|
||
else if ([pasteboard.types containsObject:NSPasteboardTypeURL])
|
||
{
|
||
NSURL* url;
|
||
if ((url = [NSURL URLFromPasteboard:pasteboard]))
|
||
{
|
||
[self openURL:url.absoluteString];
|
||
return YES;
|
||
}
|
||
}
|
||
|
||
return NO;
|
||
}
|
||
|
||
- (void)toggleSmallView:(id)sender
|
||
{
|
||
BOOL makeSmall = ![self.fDefaults boolForKey:@"SmallView"];
|
||
[self.fDefaults setBool:makeSmall forKey:@"SmallView"];
|
||
|
||
//self.fTableView.usesAlternatingRowBackgroundColors = !makeSmall;
|
||
|
||
self.fTableView.rowHeight = makeSmall ? kRowHeightSmall : kRowHeightRegular;
|
||
|
||
[self.fTableView beginUpdates];
|
||
[self.fTableView
|
||
noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.fTableView.numberOfRows)]];
|
||
[self.fTableView endUpdates];
|
||
|
||
//reloaddata, otherwise the tableview has a bunch of empty cells
|
||
[self.fTableView reloadData];
|
||
[self updateForAutoSize];
|
||
}
|
||
|
||
- (void)togglePiecesBar:(id)sender
|
||
{
|
||
[self.fDefaults setBool:![self.fDefaults boolForKey:@"PiecesBar"] forKey:@"PiecesBar"];
|
||
[self.fTableView togglePiecesBar];
|
||
}
|
||
|
||
- (void)toggleAvailabilityBar:(id)sender
|
||
{
|
||
[self.fDefaults setBool:![self.fDefaults boolForKey:@"DisplayProgressBarAvailable"] forKey:@"DisplayProgressBarAvailable"];
|
||
[self.fTableView display];
|
||
}
|
||
|
||
- (void)toggleStatusBar:(id)sender
|
||
{
|
||
BOOL const show = self.fStatusBar == nil;
|
||
[self.fDefaults setBool:show forKey:@"StatusBar"];
|
||
[self updateMainWindow];
|
||
}
|
||
|
||
- (void)toggleFilterBar:(id)sender
|
||
{
|
||
BOOL const show = self.fFilterBar == nil;
|
||
|
||
//disable filtering when hiding (have to do before updateMainWindow:)
|
||
if (!show)
|
||
{
|
||
[self.fFilterBar reset:NO];
|
||
}
|
||
|
||
[self.fDefaults setBool:show forKey:@"FilterBar"];
|
||
[self updateMainWindow];
|
||
}
|
||
|
||
- (IBAction)toggleToolbarShown:(id)sender
|
||
{
|
||
[self.fWindow toggleToolbarShown:sender];
|
||
}
|
||
|
||
- (void)focusFilterField
|
||
{
|
||
if (!self.fFilterBar)
|
||
{
|
||
[self toggleFilterBar:self];
|
||
}
|
||
[self.fFilterBar focusSearchField];
|
||
}
|
||
|
||
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel*)panel
|
||
{
|
||
return !self.fQuitting;
|
||
}
|
||
|
||
- (void)beginPreviewPanelControl:(QLPreviewPanel*)panel
|
||
{
|
||
self.fPreviewPanel = panel;
|
||
self.fPreviewPanel.delegate = self;
|
||
self.fPreviewPanel.dataSource = self;
|
||
}
|
||
|
||
- (void)endPreviewPanelControl:(QLPreviewPanel*)panel
|
||
{
|
||
self.fPreviewPanel = nil;
|
||
[self.fWindow.toolbar validateVisibleItems];
|
||
}
|
||
|
||
- (NSArray<Torrent*>*)quickLookableTorrents
|
||
{
|
||
NSArray* selectedTorrents = self.fTableView.selectedTorrents;
|
||
NSMutableArray* qlArray = [NSMutableArray arrayWithCapacity:selectedTorrents.count];
|
||
|
||
for (Torrent* torrent in selectedTorrents)
|
||
{
|
||
if ((torrent.folder || torrent.complete) && torrent.dataLocation)
|
||
{
|
||
[qlArray addObject:torrent];
|
||
}
|
||
}
|
||
|
||
return qlArray;
|
||
}
|
||
|
||
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel*)panel
|
||
{
|
||
if (self.fInfoController.canQuickLook)
|
||
{
|
||
return self.fInfoController.quickLookURLs.count;
|
||
}
|
||
else
|
||
{
|
||
return [self quickLookableTorrents].count;
|
||
}
|
||
}
|
||
|
||
- (id<QLPreviewItem>)previewPanel:(QLPreviewPanel*)panel previewItemAtIndex:(NSInteger)index
|
||
{
|
||
if (self.fInfoController.canQuickLook)
|
||
{
|
||
return self.fInfoController.quickLookURLs[index];
|
||
}
|
||
else
|
||
{
|
||
return [self quickLookableTorrents][index];
|
||
}
|
||
}
|
||
|
||
- (BOOL)previewPanel:(QLPreviewPanel*)panel handleEvent:(NSEvent*)event
|
||
{
|
||
/*if ([event type] == NSKeyDown)
|
||
{
|
||
[super keyDown: event];
|
||
return YES;
|
||
}*/
|
||
|
||
return NO;
|
||
}
|
||
|
||
- (NSRect)previewPanel:(QLPreviewPanel*)panel sourceFrameOnScreenForPreviewItem:(id<QLPreviewItem>)item
|
||
{
|
||
if (self.fInfoController.canQuickLook)
|
||
{
|
||
return [self.fInfoController quickLookSourceFrameForPreviewItem:item];
|
||
}
|
||
else
|
||
{
|
||
if (!self.fWindow.visible)
|
||
{
|
||
return NSZeroRect;
|
||
}
|
||
|
||
NSInteger const row = [self.fTableView rowForItem:item];
|
||
if (row == -1)
|
||
{
|
||
return NSZeroRect;
|
||
}
|
||
|
||
NSRect frame = [self.fTableView iconRectForRow:row];
|
||
|
||
if (!NSIntersectsRect(self.fTableView.visibleRect, frame))
|
||
{
|
||
return NSZeroRect;
|
||
}
|
||
|
||
frame.origin = [self.fTableView convertPoint:frame.origin toView:nil];
|
||
frame = [self.fWindow convertRectToScreen:frame];
|
||
frame.origin.y -= frame.size.height;
|
||
return frame;
|
||
}
|
||
}
|
||
|
||
- (void)showToolbarShare:(id)sender
|
||
{
|
||
NSParameterAssert([sender isKindOfClass:[NSButton class]]);
|
||
NSButton* senderButton = sender;
|
||
|
||
NSSharingServicePicker* picker = [[NSSharingServicePicker alloc] initWithItems:ShareTorrentFileHelper.sharedHelper.shareTorrentURLs];
|
||
picker.delegate = self;
|
||
|
||
[picker showRelativeToRect:senderButton.bounds ofView:senderButton preferredEdge:NSMinYEdge];
|
||
}
|
||
|
||
- (id<NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker*)sharingServicePicker
|
||
delegateForSharingService:(NSSharingService*)sharingService
|
||
{
|
||
return self;
|
||
}
|
||
|
||
- (NSWindow*)sharingService:(NSSharingService*)sharingService
|
||
sourceWindowForShareItems:(NSArray*)items
|
||
sharingContentScope:(NSSharingContentScope*)sharingContentScope
|
||
{
|
||
return self.fWindow;
|
||
}
|
||
|
||
- (ButtonToolbarItem*)standardToolbarButtonWithIdentifier:(NSString*)ident
|
||
{
|
||
return [self toolbarButtonWithIdentifier:ident forToolbarButtonClass:[ButtonToolbarItem class]];
|
||
}
|
||
|
||
- (__kindof ButtonToolbarItem*)toolbarButtonWithIdentifier:(NSString*)ident forToolbarButtonClass:(Class)klass
|
||
{
|
||
ButtonToolbarItem* item = [[klass alloc] initWithItemIdentifier:ident];
|
||
|
||
NSButton* button = [[NSButton alloc] init];
|
||
button.bezelStyle = NSBezelStyleTexturedRounded;
|
||
button.stringValue = @"";
|
||
|
||
item.view = button;
|
||
|
||
return item;
|
||
}
|
||
|
||
- (NSToolbarItem*)toolbar:(NSToolbar*)toolbar itemForItemIdentifier:(NSString*)ident willBeInsertedIntoToolbar:(BOOL)flag
|
||
{
|
||
if ([ident isEqualToString:ToolbarItemIdentifierCreate])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
|
||
item.label = NSLocalizedString(@"Create", "Create toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip");
|
||
item.image = [NSImage imageWithSystemSymbolName:@"doc.badge.plus" accessibilityDescription:nil];
|
||
item.target = self;
|
||
item.action = @selector(createFile:);
|
||
item.autovalidates = NO;
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierOpenFile])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
|
||
item.label = NSLocalizedString(@"Open", "Open toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip");
|
||
item.image = [NSImage imageWithSystemSymbolName:@"folder" accessibilityDescription:nil];
|
||
item.target = self;
|
||
item.action = @selector(openShowSheet:);
|
||
item.autovalidates = NO;
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierOpenWeb])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
|
||
item.label = NSLocalizedString(@"Open Address", "Open address toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Open Torrent Address", "Open address toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Open torrent web address", "Open address toolbar item -> tooltip");
|
||
item.image = [NSImage imageWithSystemSymbolName:@"globe" accessibilityDescription:nil];
|
||
item.target = self;
|
||
item.action = @selector(openURLShowSheet:);
|
||
item.autovalidates = NO;
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierRemove])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
|
||
item.label = NSLocalizedString(@"Remove", "Remove toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip");
|
||
item.image = [NSImage imageWithSystemSymbolName:@"nosign" accessibilityDescription:nil];
|
||
item.target = self;
|
||
item.action = @selector(removeNoDelete:);
|
||
item.visibilityPriority = NSToolbarItemVisibilityPriorityHigh;
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierInfo])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
((NSButtonCell*)((NSButton*)item.view).cell).showsStateBy = NSContentsCellMask; //blue when enabled
|
||
|
||
item.label = NSLocalizedString(@"Inspector", "Inspector toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip");
|
||
item.image = [NSImage imageWithSystemSymbolName:@"info.circle" accessibilityDescription:nil];
|
||
item.target = self;
|
||
item.action = @selector(showInfo:);
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierPauseResumeAll])
|
||
{
|
||
GroupToolbarItem* groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier:ident];
|
||
|
||
NSToolbarItem* itemPause = [self standardToolbarButtonWithIdentifier:ToolbarItemIdentifierPauseAll];
|
||
NSToolbarItem* itemResume = [self standardToolbarButtonWithIdentifier:ToolbarItemIdentifierResumeAll];
|
||
|
||
NSSegmentedControl* segmentedControl = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
|
||
segmentedControl.segmentStyle = NSSegmentStyleTexturedRounded;
|
||
segmentedControl.trackingMode = NSSegmentSwitchTrackingMomentary;
|
||
segmentedControl.segmentCount = 2;
|
||
|
||
[segmentedControl setTag:ToolbarGroupTagPause forSegment:ToolbarGroupTagPause];
|
||
[segmentedControl setImage:[NSImage imageWithSystemSymbolName:@"pause.circle.fill" accessibilityDescription:nil]
|
||
forSegment:ToolbarGroupTagPause];
|
||
[segmentedControl setToolTip:NSLocalizedString(@"Pause all transfers", "All toolbar item -> tooltip")
|
||
forSegment:ToolbarGroupTagPause];
|
||
|
||
[segmentedControl setTag:ToolbarGroupTagResume forSegment:ToolbarGroupTagResume];
|
||
[segmentedControl setImage:[NSImage imageWithSystemSymbolName:@"arrow.clockwise.circle.fill" accessibilityDescription:nil]
|
||
forSegment:ToolbarGroupTagResume];
|
||
[segmentedControl setToolTip:NSLocalizedString(@"Resume all transfers", "All toolbar item -> tooltip")
|
||
forSegment:ToolbarGroupTagResume];
|
||
if ([toolbar isKindOfClass:Toolbar.class] && ((Toolbar*)toolbar).isRunningCustomizationPalette)
|
||
{
|
||
// On macOS 13.2, the palette autolayout will hang unless the segmentedControl width is longer than the groupItem paletteLabel (matters especially in Russian and French).
|
||
[segmentedControl setWidth:64 forSegment:ToolbarGroupTagPause];
|
||
[segmentedControl setWidth:64 forSegment:ToolbarGroupTagResume];
|
||
}
|
||
|
||
groupItem.label = NSLocalizedString(@"Apply All", "All toolbar item -> label");
|
||
groupItem.paletteLabel = NSLocalizedString(@"Pause / Resume All", "All toolbar item -> palette label");
|
||
groupItem.visibilityPriority = NSToolbarItemVisibilityPriorityHigh;
|
||
groupItem.subitems = @[ itemPause, itemResume ];
|
||
groupItem.view = segmentedControl;
|
||
groupItem.target = self;
|
||
groupItem.action = @selector(allToolbarClicked:);
|
||
|
||
[groupItem createMenu:@[
|
||
NSLocalizedString(@"Pause All", "All toolbar item -> label"),
|
||
NSLocalizedString(@"Resume All", "All toolbar item -> label")
|
||
]];
|
||
|
||
return groupItem;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierPauseResumeSelected])
|
||
{
|
||
GroupToolbarItem* groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier:ident];
|
||
|
||
NSToolbarItem* itemPause = [self standardToolbarButtonWithIdentifier:ToolbarItemIdentifierPauseSelected];
|
||
NSToolbarItem* itemResume = [self standardToolbarButtonWithIdentifier:ToolbarItemIdentifierResumeSelected];
|
||
|
||
NSSegmentedControl* segmentedControl = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
|
||
segmentedControl.segmentStyle = NSSegmentStyleTexturedRounded;
|
||
segmentedControl.trackingMode = NSSegmentSwitchTrackingMomentary;
|
||
segmentedControl.segmentCount = 2;
|
||
|
||
[segmentedControl setTag:ToolbarGroupTagPause forSegment:ToolbarGroupTagPause];
|
||
[segmentedControl setImage:[NSImage imageWithSystemSymbolName:@"pause" accessibilityDescription:nil]
|
||
forSegment:ToolbarGroupTagPause];
|
||
[segmentedControl setToolTip:NSLocalizedString(@"Pause selected transfers", "Selected toolbar item -> tooltip")
|
||
forSegment:ToolbarGroupTagPause];
|
||
|
||
[segmentedControl setTag:ToolbarGroupTagResume forSegment:ToolbarGroupTagResume];
|
||
[segmentedControl setImage:[NSImage imageWithSystemSymbolName:@"arrow.clockwise" accessibilityDescription:nil]
|
||
forSegment:ToolbarGroupTagResume];
|
||
[segmentedControl setToolTip:NSLocalizedString(@"Resume selected transfers", "Selected toolbar item -> tooltip")
|
||
forSegment:ToolbarGroupTagResume];
|
||
if ([toolbar isKindOfClass:Toolbar.class] && ((Toolbar*)toolbar).isRunningCustomizationPalette)
|
||
{
|
||
// On macOS 13.2, the palette autolayout will hang unless the segmentedControl width is longer than the groupItem paletteLabel (matters especially in Russian and French).
|
||
[segmentedControl setWidth:64 forSegment:ToolbarGroupTagPause];
|
||
[segmentedControl setWidth:64 forSegment:ToolbarGroupTagResume];
|
||
}
|
||
|
||
groupItem.label = NSLocalizedString(@"Apply Selected", "Selected toolbar item -> label");
|
||
groupItem.paletteLabel = NSLocalizedString(@"Pause / Resume Selected", "Selected toolbar item -> palette label");
|
||
groupItem.visibilityPriority = NSToolbarItemVisibilityPriorityHigh;
|
||
groupItem.subitems = @[ itemPause, itemResume ];
|
||
groupItem.view = segmentedControl;
|
||
groupItem.target = self;
|
||
groupItem.action = @selector(selectedToolbarClicked:);
|
||
|
||
[groupItem createMenu:@[
|
||
NSLocalizedString(@"Pause Selected", "Selected toolbar item -> label"),
|
||
NSLocalizedString(@"Resume Selected", "Selected toolbar item -> label")
|
||
]];
|
||
|
||
return groupItem;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierFilter])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
((NSButtonCell*)((NSButton*)item.view).cell).showsStateBy = NSContentsCellMask; //blue when enabled
|
||
|
||
item.label = NSLocalizedString(@"Filter", "Filter toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip");
|
||
item.image = [NSImage imageWithSystemSymbolName:@"magnifyingglass" accessibilityDescription:nil];
|
||
item.target = self;
|
||
item.action = @selector(toggleFilterBar:);
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierQuickLook])
|
||
{
|
||
ButtonToolbarItem* item = [self standardToolbarButtonWithIdentifier:ident];
|
||
((NSButtonCell*)((NSButton*)item.view).cell).showsStateBy = NSContentsCellMask; //blue when enabled
|
||
|
||
item.label = NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> tooltip");
|
||
item.image = [NSImage imageNamed:NSImageNameQuickLookTemplate];
|
||
item.target = self;
|
||
item.action = @selector(toggleQuickLook:);
|
||
item.visibilityPriority = NSToolbarItemVisibilityPriorityLow;
|
||
|
||
return item;
|
||
}
|
||
else if ([ident isEqualToString:ToolbarItemIdentifierShare])
|
||
{
|
||
ShareToolbarItem* item = [self toolbarButtonWithIdentifier:ident forToolbarButtonClass:[ShareToolbarItem class]];
|
||
|
||
item.label = NSLocalizedString(@"Share", "Share toolbar item -> label");
|
||
item.paletteLabel = NSLocalizedString(@"Share", "Share toolbar item -> palette label");
|
||
item.toolTip = NSLocalizedString(@"Share torrent file", "Share toolbar item -> tooltip");
|
||
item.image = [NSImage imageNamed:NSImageNameShareTemplate];
|
||
item.visibilityPriority = NSToolbarItemVisibilityPriorityLow;
|
||
|
||
NSButton* itemButton = (NSButton*)item.view;
|
||
itemButton.target = self;
|
||
itemButton.action = @selector(showToolbarShare:);
|
||
[itemButton sendActionOn:NSEventMaskLeftMouseDown];
|
||
|
||
return item;
|
||
}
|
||
else
|
||
{
|
||
return nil;
|
||
}
|
||
}
|
||
|
||
- (void)allToolbarClicked:(id)sender
|
||
{
|
||
NSInteger tagValue = [sender isKindOfClass:[NSSegmentedControl class]] ? [(NSSegmentedControl*)sender selectedTag] :
|
||
((NSControl*)sender).tag;
|
||
switch (tagValue)
|
||
{
|
||
case ToolbarGroupTagPause:
|
||
[self stopAllTorrents:sender];
|
||
break;
|
||
case ToolbarGroupTagResume:
|
||
[self resumeAllTorrents:sender];
|
||
break;
|
||
}
|
||
}
|
||
|
||
- (void)selectedToolbarClicked:(id)sender
|
||
{
|
||
NSInteger tagValue = [sender isKindOfClass:[NSSegmentedControl class]] ? [(NSSegmentedControl*)sender selectedTag] :
|
||
((NSControl*)sender).tag;
|
||
switch (tagValue)
|
||
{
|
||
case ToolbarGroupTagPause:
|
||
[self stopSelectedTorrents:sender];
|
||
break;
|
||
case ToolbarGroupTagResume:
|
||
[self resumeSelectedTorrents:sender];
|
||
break;
|
||
}
|
||
}
|
||
|
||
- (NSArray*)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
|
||
{
|
||
return @[
|
||
ToolbarItemIdentifierCreate,
|
||
ToolbarItemIdentifierOpenFile,
|
||
ToolbarItemIdentifierOpenWeb,
|
||
ToolbarItemIdentifierRemove,
|
||
ToolbarItemIdentifierPauseResumeSelected,
|
||
ToolbarItemIdentifierPauseResumeAll,
|
||
ToolbarItemIdentifierShare,
|
||
ToolbarItemIdentifierQuickLook,
|
||
ToolbarItemIdentifierFilter,
|
||
ToolbarItemIdentifierInfo,
|
||
NSToolbarSpaceItemIdentifier,
|
||
NSToolbarFlexibleSpaceItemIdentifier
|
||
];
|
||
}
|
||
|
||
- (NSArray*)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
|
||
{
|
||
return @[
|
||
ToolbarItemIdentifierCreate,
|
||
ToolbarItemIdentifierOpenFile,
|
||
ToolbarItemIdentifierRemove,
|
||
NSToolbarSpaceItemIdentifier,
|
||
ToolbarItemIdentifierPauseResumeAll,
|
||
NSToolbarFlexibleSpaceItemIdentifier,
|
||
ToolbarItemIdentifierShare,
|
||
ToolbarItemIdentifierQuickLook,
|
||
ToolbarItemIdentifierFilter,
|
||
ToolbarItemIdentifierInfo,
|
||
];
|
||
}
|
||
|
||
- (BOOL)validateToolbarItem:(NSToolbarItem*)toolbarItem
|
||
{
|
||
NSString* ident = toolbarItem.itemIdentifier;
|
||
|
||
//enable remove item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierRemove])
|
||
{
|
||
return self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
//enable pause all item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierPauseAll])
|
||
{
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.active || torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable resume all item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierResumeAll])
|
||
{
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (!torrent.active && !torrent.waitingToStart && !torrent.finishedSeeding)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable pause item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierPauseSelected])
|
||
{
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (torrent.active || torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable resume item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierResumeSelected])
|
||
{
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (!torrent.active && !torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//set info item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierInfo])
|
||
{
|
||
((NSButton*)toolbarItem.view).state = self.fInfoController.window.visible;
|
||
return YES;
|
||
}
|
||
|
||
//set filter item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierFilter])
|
||
{
|
||
((NSButton*)toolbarItem.view).state = self.fFilterBar != nil;
|
||
return YES;
|
||
}
|
||
|
||
//set quick look item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierQuickLook])
|
||
{
|
||
((NSButton*)toolbarItem.view).state = self.fPreviewPanel != nil;
|
||
return self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
//enable share item
|
||
if ([ident isEqualToString:ToolbarItemIdentifierShare])
|
||
{
|
||
return self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
|
||
{
|
||
SEL action = menuItem.action;
|
||
|
||
if (action == @selector(toggleSpeedLimit:))
|
||
{
|
||
menuItem.state = [self.fDefaults boolForKey:@"SpeedLimit"] ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return YES;
|
||
}
|
||
|
||
//only enable some items if it is in a context menu or the window is usable
|
||
BOOL canUseTable = self.fWindow.keyWindow || menuItem.menu.supermenu != NSApp.mainMenu;
|
||
|
||
//enable open items
|
||
if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
|
||
{
|
||
return self.fWindow.attachedSheet == nil;
|
||
}
|
||
|
||
//enable sort options
|
||
if (action == @selector(setSort:))
|
||
{
|
||
SortType sortType;
|
||
switch (menuItem.tag)
|
||
{
|
||
case SortTagOrder:
|
||
sortType = SortTypeOrder;
|
||
break;
|
||
case SortTagDate:
|
||
sortType = SortTypeDate;
|
||
break;
|
||
case SortTagName:
|
||
sortType = SortTypeName;
|
||
break;
|
||
case SortTagProgress:
|
||
sortType = SortTypeProgress;
|
||
break;
|
||
case SortTagState:
|
||
sortType = SortTypeState;
|
||
break;
|
||
case SortTagTracker:
|
||
sortType = SortTypeTracker;
|
||
break;
|
||
case SortTagActivity:
|
||
sortType = SortTypeActivity;
|
||
break;
|
||
case SortTagSize:
|
||
sortType = SortTypeSize;
|
||
break;
|
||
case SortTagETA:
|
||
sortType = SortTypeETA;
|
||
break;
|
||
default:
|
||
NSAssert1(NO, @"Unknown sort tag received: %ld", [menuItem tag]);
|
||
sortType = SortTypeOrder;
|
||
}
|
||
|
||
menuItem.state = [sortType isEqualToString:[self.fDefaults stringForKey:@"Sort"]] ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
if (action == @selector(setGroup:))
|
||
{
|
||
BOOL checked = NO;
|
||
|
||
NSInteger index = menuItem.tag;
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (index == torrent.groupValue)
|
||
{
|
||
checked = YES;
|
||
break;
|
||
}
|
||
}
|
||
|
||
menuItem.state = checked ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return canUseTable && self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
if (action == @selector(toggleSmallView:))
|
||
{
|
||
menuItem.state = [self.fDefaults boolForKey:@"SmallView"] ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
if (action == @selector(togglePiecesBar:))
|
||
{
|
||
menuItem.state = [self.fDefaults boolForKey:@"PiecesBar"] ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
if (action == @selector(toggleAvailabilityBar:))
|
||
{
|
||
menuItem.state = [self.fDefaults boolForKey:@"DisplayProgressBarAvailable"] ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
//enable show info
|
||
if (action == @selector(showInfo:))
|
||
{
|
||
NSString* title = self.fInfoController.window.visible ? NSLocalizedString(@"Hide Inspector", "View menu -> Inspector") :
|
||
NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
|
||
menuItem.title = title;
|
||
|
||
return YES;
|
||
}
|
||
|
||
//enable prev/next inspector tab
|
||
if (action == @selector(setInfoTab:))
|
||
{
|
||
return self.fInfoController.window.visible;
|
||
}
|
||
|
||
//enable toggle status bar
|
||
if (action == @selector(toggleStatusBar:))
|
||
{
|
||
NSString* title = !self.fStatusBar ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar") :
|
||
NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
|
||
menuItem.title = title;
|
||
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
//enable toggle filter bar
|
||
if (action == @selector(toggleFilterBar:))
|
||
{
|
||
NSString* title = !self.fFilterBar ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar") :
|
||
NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
|
||
menuItem.title = title;
|
||
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
// enable toggle toolbar
|
||
if (action == @selector(toggleToolbarShown:))
|
||
{
|
||
NSString* title = !self.fWindow.toolbar.isVisible ? NSLocalizedString(@"Show Toolbar", "View menu -> Toolbar") :
|
||
NSLocalizedString(@"Hide Toolbar", "View menu -> Toolbar");
|
||
menuItem.title = title;
|
||
|
||
return self.fWindow.visible;
|
||
}
|
||
|
||
//enable prev/next filter button
|
||
if (action == @selector(switchFilter:))
|
||
{
|
||
return self.fWindow.visible && self.fFilterBar;
|
||
}
|
||
|
||
//enable reveal in finder
|
||
if (action == @selector(revealFile:))
|
||
{
|
||
return canUseTable && self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
//enable renaming file/folder
|
||
if (action == @selector(renameSelected:))
|
||
{
|
||
return canUseTable && self.fTableView.numberOfSelectedRows == 1;
|
||
}
|
||
|
||
//enable remove items
|
||
if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:))
|
||
{
|
||
BOOL warning = NO;
|
||
|
||
if (self.fFilterBar.isFocused == YES)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (torrent.active)
|
||
{
|
||
if ([self.fDefaults boolForKey:@"CheckRemoveDownloading"] ? !torrent.seeding : YES)
|
||
{
|
||
warning = YES;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
//append or remove ellipsis when needed
|
||
NSString *title = menuItem.title, *ellipsis = NSString.ellipsis;
|
||
if (warning && [self.fDefaults boolForKey:@"CheckRemove"])
|
||
{
|
||
if (![title hasSuffix:ellipsis])
|
||
{
|
||
menuItem.title = [title stringByAppendingEllipsis];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if ([title hasSuffix:ellipsis])
|
||
{
|
||
menuItem.title = [title substringToIndex:[title rangeOfString:ellipsis].location];
|
||
}
|
||
}
|
||
|
||
return canUseTable && self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
//remove all completed transfers item
|
||
if (action == @selector(clearCompleted:))
|
||
{
|
||
//append or remove ellipsis when needed
|
||
NSString *title = menuItem.title, *ellipsis = NSString.ellipsis;
|
||
if ([self.fDefaults boolForKey:@"WarningRemoveCompleted"])
|
||
{
|
||
if (![title hasSuffix:ellipsis])
|
||
{
|
||
menuItem.title = [title stringByAppendingEllipsis];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if ([title hasSuffix:ellipsis])
|
||
{
|
||
menuItem.title = [title substringToIndex:[title rangeOfString:ellipsis].location];
|
||
}
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.finishedSeeding)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable pause all item
|
||
if (action == @selector(stopAllTorrents:))
|
||
{
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.active || torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable resume all item
|
||
if (action == @selector(resumeAllTorrents:))
|
||
{
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (!torrent.active && !torrent.waitingToStart && !torrent.finishedSeeding)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable resume all waiting item
|
||
if (action == @selector(resumeWaitingTorrents:))
|
||
{
|
||
if (![self.fDefaults boolForKey:@"Queue"] && ![self.fDefaults boolForKey:@"QueueSeed"])
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable resume selected waiting item
|
||
if (action == @selector(resumeSelectedTorrentsNoWait:))
|
||
{
|
||
if (!canUseTable)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (!torrent.active)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable pause item
|
||
if (action == @selector(stopSelectedTorrents:))
|
||
{
|
||
if (!canUseTable)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (torrent.active || torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable resume item
|
||
if (action == @selector(resumeSelectedTorrents:))
|
||
{
|
||
if (!canUseTable)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (!torrent.active && !torrent.waitingToStart)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable manual announce item
|
||
if (action == @selector(announceSelectedTorrents:))
|
||
{
|
||
if (!canUseTable)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (torrent.canManualAnnounce)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable reset cache item
|
||
if (action == @selector(verifySelectedTorrents:))
|
||
{
|
||
if (!canUseTable)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (!torrent.magnet)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable move torrent file item
|
||
if (action == @selector(moveDataFilesSelected:))
|
||
{
|
||
return canUseTable && self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
//enable copy torrent file item
|
||
if (action == @selector(copyTorrentFiles:))
|
||
{
|
||
if (!canUseTable)
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
for (Torrent* torrent in self.fTableView.selectedTorrents)
|
||
{
|
||
if (!torrent.magnet)
|
||
{
|
||
return YES;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
//enable copy torrent file item
|
||
if (action == @selector(copyMagnetLinks:))
|
||
{
|
||
return canUseTable && self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
//enable reverse sort item
|
||
if (action == @selector(setSortReverse:))
|
||
{
|
||
BOOL const isReverse = menuItem.tag == SortOrderTagDescending;
|
||
menuItem.state = (isReverse == [self.fDefaults boolForKey:@"SortReverse"]) ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return ![[self.fDefaults stringForKey:@"Sort"] isEqualToString:SortTypeOrder];
|
||
}
|
||
|
||
//enable group sort item
|
||
if (action == @selector(setSortByGroup:))
|
||
{
|
||
menuItem.state = [self.fDefaults boolForKey:@"SortByGroup"] ? NSControlStateValueOn : NSControlStateValueOff;
|
||
return YES;
|
||
}
|
||
|
||
if (action == @selector(toggleQuickLook:))
|
||
{
|
||
BOOL const visible = [QLPreviewPanel sharedPreviewPanelExists] && [QLPreviewPanel sharedPreviewPanel].visible;
|
||
//text consistent with Finder
|
||
NSString* title = !visible ? NSLocalizedString(@"Quick Look", "View menu -> Quick Look") :
|
||
NSLocalizedString(@"Close Quick Look", "View menu -> Quick Look");
|
||
menuItem.title = title;
|
||
|
||
return self.fTableView.numberOfSelectedRows > 0;
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (void)sleepCallback:(natural_t)messageType argument:(void*)messageArgument
|
||
{
|
||
switch (messageType)
|
||
{
|
||
case kIOMessageSystemWillSleep:
|
||
{
|
||
//stop all transfers (since some are active) before going to sleep and remember to resume when we wake up
|
||
BOOL anyActive = NO;
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.active)
|
||
{
|
||
anyActive = YES;
|
||
}
|
||
[torrent sleep]; //have to call on all, regardless if they are active
|
||
}
|
||
|
||
//if there are any running transfers, wait 15 seconds for them to stop
|
||
if (anyActive)
|
||
{
|
||
sleep(15);
|
||
}
|
||
|
||
IOAllowPowerChange(self.fRootPort, (long)messageArgument);
|
||
break;
|
||
}
|
||
|
||
case kIOMessageCanSystemSleep:
|
||
if ([self.fDefaults boolForKey:@"SleepPrevent"])
|
||
{
|
||
//prevent idle sleep unless no torrents are active
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.active && !torrent.stalled && !torrent.error)
|
||
{
|
||
IOCancelPowerChange(self.fRootPort, (long)messageArgument);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
IOAllowPowerChange(self.fRootPort, (long)messageArgument);
|
||
break;
|
||
|
||
case kIOMessageSystemHasPoweredOn:
|
||
//resume sleeping transfers after we wake up
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
[torrent wakeUp];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
- (NSMenu*)applicationDockMenu:(NSApplication*)sender
|
||
{
|
||
if (self.fQuitting)
|
||
{
|
||
return nil;
|
||
}
|
||
|
||
NSUInteger seeding = 0, downloading = 0;
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
if (torrent.seeding)
|
||
{
|
||
seeding++;
|
||
}
|
||
else if (torrent.active)
|
||
{
|
||
downloading++;
|
||
}
|
||
}
|
||
|
||
NSMenu* menu = [[NSMenu alloc] init];
|
||
|
||
if (seeding > 0)
|
||
{
|
||
NSString* title = [NSString localizedStringWithFormat:NSLocalizedString(@"%lu Seeding", "Dock item - Seeding"), seeding];
|
||
[menu addItemWithTitle:title action:nil keyEquivalent:@""];
|
||
}
|
||
|
||
if (downloading > 0)
|
||
{
|
||
NSString* title = [NSString localizedStringWithFormat:NSLocalizedString(@"%lu Downloading", "Dock item - Downloading"), downloading];
|
||
[menu addItemWithTitle:title action:nil keyEquivalent:@""];
|
||
}
|
||
|
||
if (seeding > 0 || downloading > 0)
|
||
{
|
||
[menu addItem:[NSMenuItem separatorItem]];
|
||
}
|
||
|
||
[menu addItemWithTitle:NSLocalizedString(@"Pause All", "Dock item") action:@selector(stopAllTorrents:) keyEquivalent:@""];
|
||
[menu addItemWithTitle:NSLocalizedString(@"Resume All", "Dock item") action:@selector(resumeAllTorrents:) keyEquivalent:@""];
|
||
[menu addItem:[NSMenuItem separatorItem]];
|
||
[menu addItemWithTitle:NSLocalizedString(@"Speed Limit", "Dock item") action:@selector(toggleSpeedLimit:) keyEquivalent:@""];
|
||
|
||
return menu;
|
||
}
|
||
|
||
- (void)updateMainWindow
|
||
{
|
||
NSArray* subViews = self.fStackView.arrangedSubviews;
|
||
NSUInteger idx = 0;
|
||
|
||
//update layout
|
||
if ([self.fDefaults boolForKey:@"StatusBar"])
|
||
{
|
||
if (self.fStatusBar == nil)
|
||
{
|
||
self.fStatusBar = [[StatusBarController alloc] initWithLib:self.fLib];
|
||
}
|
||
|
||
[self.fStackView insertArrangedSubview:self.fStatusBar.view atIndex:idx];
|
||
|
||
NSDictionary* views = @{ @"fStatusBar" : self.fStatusBar.view };
|
||
[self.fStackView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[fStatusBar(==21)]" options:0
|
||
metrics:nil
|
||
views:views]];
|
||
idx = 1;
|
||
}
|
||
else
|
||
{
|
||
if ([subViews containsObject:self.fStatusBar.view])
|
||
{
|
||
[self.fStackView removeView:self.fStatusBar.view];
|
||
self.fStatusBar = nil;
|
||
}
|
||
}
|
||
|
||
if ([self.fDefaults boolForKey:@"FilterBar"])
|
||
{
|
||
if (self.fFilterBar == nil)
|
||
{
|
||
self.fFilterBar = [[FilterBarController alloc] init];
|
||
}
|
||
|
||
[self.fStackView insertArrangedSubview:self.fFilterBar.view atIndex:idx];
|
||
|
||
NSDictionary* views = @{ @"fFilterBar" : self.fFilterBar.view };
|
||
[self.fStackView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[fFilterBar(==23)]" options:0
|
||
metrics:nil
|
||
views:views]];
|
||
|
||
[self focusFilterField];
|
||
}
|
||
else
|
||
{
|
||
if ([subViews containsObject:self.fFilterBar.view])
|
||
{
|
||
[self.fStackView removeView:self.fFilterBar.view];
|
||
self.fFilterBar = nil;
|
||
|
||
[self.fWindow makeFirstResponder:self.fTableView];
|
||
}
|
||
}
|
||
|
||
[self fullUpdateUI];
|
||
[self updateForAutoSize];
|
||
}
|
||
|
||
- (void)setWindowSizeToFit
|
||
{
|
||
if (!self.isFullScreen)
|
||
{
|
||
NSScrollView* scrollView = self.fTableView.enclosingScrollView;
|
||
|
||
scrollView.hasVerticalScroller = NO;
|
||
|
||
[self removeStackViewHeightConstraints];
|
||
|
||
NSDictionary* views = @{ @"scrollView" : scrollView };
|
||
|
||
if (![self.fDefaults boolForKey:@"AutoSize"])
|
||
{
|
||
// Only set a minimum height constraint
|
||
CGFloat height = self.minScrollViewHeightAllowed;
|
||
NSString* constraintsString = [NSString stringWithFormat:@"V:[scrollView(>=%f)]", height];
|
||
self.fStackViewHeightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintsString options:0
|
||
metrics:nil
|
||
views:views];
|
||
}
|
||
else
|
||
{
|
||
// Set a fixed height constraint
|
||
CGFloat height = [self calculateScrollViewHeightWithDockAdjustment];
|
||
NSString* constraintsString = [NSString stringWithFormat:@"V:[scrollView(==%f)]", height];
|
||
self.fStackViewHeightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintsString options:0
|
||
metrics:nil
|
||
views:views];
|
||
|
||
// Redraw table to avoid empty cells
|
||
[self.fTableView reloadData];
|
||
}
|
||
|
||
// Add height constraint to fStackView
|
||
[self.fStackView addConstraints:self.fStackViewHeightConstraints];
|
||
|
||
scrollView.hasVerticalScroller = YES;
|
||
}
|
||
else
|
||
{
|
||
[self removeStackViewHeightConstraints];
|
||
}
|
||
}
|
||
|
||
- (CGFloat)calculateScrollViewHeightWithDockAdjustment
|
||
{
|
||
CGFloat height = self.scrollViewHeight;
|
||
|
||
// Get the main screen's visible frame
|
||
NSScreen* screen = self.fWindow.screen;
|
||
if (screen)
|
||
{
|
||
// This frame respects the Dock and menu bar
|
||
NSRect visibleFrame = screen.visibleFrame;
|
||
height = MIN(height, visibleFrame.size.height - [self toolbarHeight] - [self mainWindowComponentHeight]);
|
||
}
|
||
|
||
return height;
|
||
}
|
||
|
||
- (void)updateForAutoSize
|
||
{
|
||
if (!self.isFullScreen)
|
||
{
|
||
[self setWindowSizeToFit];
|
||
}
|
||
else
|
||
{
|
||
[self removeStackViewHeightConstraints];
|
||
}
|
||
}
|
||
|
||
- (void)updateWindowAfterToolbarChange
|
||
{
|
||
//Hacky way of fixing an issue with showing the Toolbar
|
||
if (!self.isFullScreen)
|
||
{
|
||
//macOS shows the unified toolbar by default
|
||
//and we only need to "fix" the layout when showing the toolbar
|
||
if (!self.fWindow.toolbar.isVisible)
|
||
{
|
||
[self removeStackViewHeightConstraints];
|
||
}
|
||
|
||
//this fixes a macOS bug where on toggling the toolbar item bezels will show
|
||
[self hideToolBarBezels:YES];
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
[self updateForAutoSize];
|
||
[self hideToolBarBezels:NO];
|
||
});
|
||
}
|
||
}
|
||
|
||
- (void)hideToolBarBezels:(BOOL)hide
|
||
{
|
||
for (NSToolbarItem* item in self.fWindow.toolbar.items)
|
||
{
|
||
item.view.hidden = hide;
|
||
}
|
||
}
|
||
|
||
- (void)removeStackViewHeightConstraints
|
||
{
|
||
if (self.fStackViewHeightConstraints)
|
||
{
|
||
[self.fStackView removeConstraints:self.fStackViewHeightConstraints];
|
||
}
|
||
}
|
||
|
||
- (CGFloat)minScrollViewHeightAllowed
|
||
{
|
||
CGFloat contentMinHeight = self.fTableView.rowHeight + self.fTableView.intercellSpacing.height;
|
||
return contentMinHeight;
|
||
}
|
||
|
||
- (CGFloat)toolbarHeight
|
||
{
|
||
return self.fWindow.frame.size.height - [self.fWindow contentRectForFrameRect:self.fWindow.frame].size.height;
|
||
}
|
||
|
||
- (CGFloat)mainWindowComponentHeight
|
||
{
|
||
CGFloat height = kBottomBarHeight;
|
||
if (self.fStatusBar)
|
||
{
|
||
height += kStatusBarHeight;
|
||
}
|
||
|
||
if (self.fFilterBar)
|
||
{
|
||
height += kFilterBarHeight;
|
||
}
|
||
|
||
return height;
|
||
}
|
||
|
||
- (CGFloat)scrollViewHeight
|
||
{
|
||
CGFloat height;
|
||
CGFloat minHeight = self.minScrollViewHeightAllowed;
|
||
|
||
if ([self.fDefaults boolForKey:@"AutoSize"])
|
||
{
|
||
NSUInteger groups = ![self.fDisplayedTorrents.firstObject isKindOfClass:[Torrent class]] ? self.fDisplayedTorrents.count : 0;
|
||
|
||
height = (kGroupSeparatorHeight + self.fTableView.intercellSpacing.height) * groups +
|
||
(self.fTableView.rowHeight + self.fTableView.intercellSpacing.height) * (self.fTableView.numberOfRows - groups);
|
||
|
||
//account for group padding...
|
||
if (groups > 1)
|
||
{
|
||
height += (groups - 1) * 20;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
height = NSHeight(self.fTableView.enclosingScrollView.frame);
|
||
}
|
||
|
||
//make sure we don't go bigger than the screen height
|
||
NSScreen* screen = self.fWindow.screen;
|
||
if (screen)
|
||
{
|
||
NSSize maxSize = screen.visibleFrame.size;
|
||
maxSize.height -= self.toolbarHeight;
|
||
maxSize.height -= self.mainWindowComponentHeight;
|
||
|
||
if (height > maxSize.height)
|
||
{
|
||
height = maxSize.height;
|
||
}
|
||
}
|
||
|
||
//make sure we don't have zero height
|
||
if (height < minHeight)
|
||
{
|
||
height = minHeight;
|
||
}
|
||
|
||
return height;
|
||
}
|
||
|
||
- (BOOL)isFullScreen
|
||
{
|
||
return (self.fWindow.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen;
|
||
}
|
||
|
||
- (void)windowWillEnterFullScreen:(NSNotification*)notification
|
||
{
|
||
[self removeStackViewHeightConstraints];
|
||
}
|
||
|
||
- (void)windowDidExitFullScreen:(NSNotification*)notification
|
||
{
|
||
[self updateForAutoSize];
|
||
}
|
||
|
||
- (void)updateForExpandCollapse
|
||
{
|
||
[self setWindowSizeToFit];
|
||
[self setBottomCountText:YES];
|
||
}
|
||
|
||
- (void)showMainWindow:(id)sender
|
||
{
|
||
[self.fWindow makeKeyAndOrderFront:nil];
|
||
}
|
||
|
||
- (void)windowDidBecomeMain:(NSNotification*)notification
|
||
{
|
||
[self.fBadger clearCompleted];
|
||
[self updateUI];
|
||
}
|
||
|
||
- (void)applicationWillUnhide:(NSNotification*)notification
|
||
{
|
||
[self updateUI];
|
||
}
|
||
|
||
- (void)toggleQuickLook:(id)sender
|
||
{
|
||
if ([QLPreviewPanel sharedPreviewPanel].visible)
|
||
{
|
||
[[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
|
||
}
|
||
else
|
||
{
|
||
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
|
||
}
|
||
}
|
||
|
||
- (void)linkHomepage:(id)sender
|
||
{
|
||
[NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kWebsiteURL]];
|
||
}
|
||
|
||
- (void)linkForums:(id)sender
|
||
{
|
||
[NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kForumURL]];
|
||
}
|
||
|
||
- (void)linkGitHub:(id)sender
|
||
{
|
||
[NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kGithubURL]];
|
||
}
|
||
|
||
- (void)linkDonate:(id)sender
|
||
{
|
||
[NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kDonateURL]];
|
||
}
|
||
|
||
- (void)rpcCallback:(tr_rpc_callback_type)type forTorrentStruct:(struct tr_torrent*)torrentStruct
|
||
{
|
||
@autoreleasepool
|
||
{
|
||
//get the torrent
|
||
__block Torrent* torrent = nil;
|
||
if (torrentStruct != NULL && (type != TR_RPC_TORRENT_ADDED && type != TR_RPC_SESSION_CHANGED && type != TR_RPC_SESSION_CLOSE))
|
||
{
|
||
[self.fTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent
|
||
usingBlock:^(Torrent* checkTorrent, NSUInteger /*idx*/, BOOL* stop) {
|
||
if (torrentStruct == checkTorrent.torrentStruct)
|
||
{
|
||
torrent = checkTorrent;
|
||
*stop = YES;
|
||
}
|
||
}];
|
||
|
||
if (!torrent)
|
||
{
|
||
NSLog(@"No torrent found matching the given torrent struct from the RPC callback!");
|
||
return;
|
||
}
|
||
}
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
switch (type)
|
||
{
|
||
case TR_RPC_TORRENT_ADDED:
|
||
[self rpcAddTorrentStruct:torrentStruct];
|
||
break;
|
||
|
||
case TR_RPC_TORRENT_STARTED:
|
||
case TR_RPC_TORRENT_STOPPED:
|
||
[self rpcStartedStoppedTorrent:torrent];
|
||
break;
|
||
|
||
case TR_RPC_TORRENT_REMOVING:
|
||
[self rpcRemoveTorrent:torrent deleteData:NO];
|
||
break;
|
||
|
||
case TR_RPC_TORRENT_TRASHING:
|
||
[self rpcRemoveTorrent:torrent deleteData:YES];
|
||
break;
|
||
|
||
case TR_RPC_TORRENT_CHANGED:
|
||
[self rpcChangedTorrent:torrent];
|
||
break;
|
||
|
||
case TR_RPC_TORRENT_MOVED:
|
||
[self rpcMovedTorrent:torrent];
|
||
break;
|
||
|
||
case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED:
|
||
[self rpcUpdateQueue];
|
||
break;
|
||
|
||
case TR_RPC_SESSION_CHANGED:
|
||
[self.prefsController rpcUpdatePrefs];
|
||
break;
|
||
|
||
case TR_RPC_SESSION_CLOSE:
|
||
self.fQuitRequested = YES;
|
||
[NSApp terminate:self];
|
||
break;
|
||
|
||
default:
|
||
NSAssert1(NO, @"Unknown RPC command received: %d", type);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
- (void)rpcAddTorrentStruct:(struct tr_torrent*)torrentStruct
|
||
{
|
||
NSString* location = nil;
|
||
if (tr_torrentGetDownloadDir(torrentStruct) != NULL)
|
||
{
|
||
location = @(tr_torrentGetDownloadDir(torrentStruct));
|
||
}
|
||
|
||
Torrent* torrent = [[Torrent alloc] initWithTorrentStruct:torrentStruct location:location lib:self.fLib];
|
||
|
||
//change the location if the group calls for it (this has to wait until after the torrent is created)
|
||
if ([GroupsController.groups usesCustomDownloadLocationForIndex:torrent.groupValue])
|
||
{
|
||
location = [GroupsController.groups customDownloadLocationForIndex:torrent.groupValue];
|
||
[torrent changeDownloadFolderBeforeUsing:location determinationType:TorrentDeterminationAutomatic];
|
||
}
|
||
|
||
[torrent update];
|
||
[self.fTorrents addObject:torrent];
|
||
|
||
if (!self.fAddingTransfers)
|
||
{
|
||
self.fAddingTransfers = [[NSMutableSet alloc] init];
|
||
}
|
||
[self.fAddingTransfers addObject:torrent];
|
||
|
||
[self fullUpdateUI];
|
||
}
|
||
|
||
- (void)rpcRemoveTorrent:(Torrent*)torrent deleteData:(BOOL)deleteData
|
||
{
|
||
[self confirmRemoveTorrents:@[ torrent ] deleteData:deleteData];
|
||
}
|
||
|
||
- (void)rpcStartedStoppedTorrent:(Torrent*)torrent
|
||
{
|
||
[torrent update];
|
||
|
||
[self updateUI];
|
||
[self applyFilter];
|
||
[self updateTorrentHistory];
|
||
}
|
||
|
||
- (void)rpcChangedTorrent:(Torrent*)torrent
|
||
{
|
||
[torrent update];
|
||
|
||
if ([self.fTableView.selectedTorrents containsObject:torrent])
|
||
{
|
||
[self.fInfoController updateInfoStats]; //this will reload the file table
|
||
[self.fInfoController updateOptions];
|
||
}
|
||
}
|
||
|
||
- (void)rpcMovedTorrent:(Torrent*)torrent
|
||
{
|
||
[torrent update];
|
||
[torrent updateTimeMachineExclude];
|
||
|
||
if ([self.fTableView.selectedTorrents containsObject:torrent])
|
||
{
|
||
[self.fInfoController updateInfoStats];
|
||
}
|
||
}
|
||
|
||
- (void)rpcUpdateQueue
|
||
{
|
||
for (Torrent* torrent in self.fTorrents)
|
||
{
|
||
[torrent update];
|
||
}
|
||
|
||
NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:@"queuePosition" ascending:YES];
|
||
NSArray* descriptors = @[ descriptor ];
|
||
[self.fTorrents sortUsingDescriptors:descriptors];
|
||
|
||
[self sortTorrentsAndIncludeQueueOrder:YES];
|
||
}
|
||
|
||
@end
|
||
|
||
@implementation Controller (SUUpdaterDelegate)
|
||
|
||
- (void)updaterWillRelaunchApplication:(SUUpdater*)updater
|
||
{
|
||
self.fQuitRequested = YES;
|
||
}
|
||
|
||
- (nullable id<SUVersionComparison>)versionComparatorForUpdater:(SUUpdater*)updater
|
||
{
|
||
return [VersionComparator new];
|
||
}
|
||
|
||
@end
|