transmission/macosx/TrackerCell.mm

324 lines
12 KiB
Plaintext

// This file Copyright © 2009-2022 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#include <libtransmission/transmission.h>
#include <libtransmission/web-utils.h> //tr_addressIsIP()
#import "TrackerCell.h"
#import "TrackerNode.h"
#define PADDING_HORIZONAL 3.0
#define PADDING_STATUS_HORIZONAL 3.0
#define ICON_SIZE 16.0
#define PADDING_BETWEEN_ICON_AND_NAME 4.0
#define PADDING_ABOVE_ICON 1.0
#define PADDING_ABOVE_NAME 1.0
#define PADDING_BETWEEN_LINES 1.0
#define PADDING_BETWEEN_LINES_ON_SAME_LINE 4.0
#define COUNT_WIDTH 40.0
@interface TrackerCell ()
@property(nonatomic, readonly) NSImage* favIcon;
- (void)loadTrackerIcon:(NSString*)baseAddress;
- (NSRect)imageRectForBounds:(NSRect)bounds;
- (NSRect)rectForNameWithString:(NSAttributedString*)string inBounds:(NSRect)bounds;
- (NSRect)rectForCountWithString:(NSAttributedString*)string withAboveRect:(NSRect)aboveRect inBounds:(NSRect)bounds;
- (NSRect)rectForCountLabelWithString:(NSAttributedString*)string withRightRect:(NSRect)rightRect inBounds:(NSRect)bounds;
- (NSRect)rectForStatusWithString:(NSAttributedString*)string
withAboveRect:(NSRect)aboveRect
withRightRect:(NSRect)rightRect
inBounds:(NSRect)bounds;
@property(nonatomic, readonly) NSAttributedString* attributedName;
- (NSAttributedString*)attributedStatusWithString:(NSString*)statusString;
- (NSAttributedString*)attributedCount:(NSInteger)count;
@end
@implementation TrackerCell
//make the favicons accessible to all tracker cells
NSCache* fTrackerIconCache;
NSMutableSet* fTrackerIconLoading;
+ (void)initialize
{
fTrackerIconCache = [[NSCache alloc] init];
fTrackerIconLoading = [[NSMutableSet alloc] init];
}
- (instancetype)init
{
if ((self = [super init]))
{
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
fNameAttributes = [[NSMutableDictionary alloc]
initWithObjectsAndKeys:[NSFont messageFontOfSize:12.0], NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil];
fStatusAttributes = [[NSMutableDictionary alloc]
initWithObjectsAndKeys:[NSFont messageFontOfSize:9.0], NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil];
}
return self;
}
- (id)copyWithZone:(NSZone*)zone
{
TrackerCell* copy = [super copyWithZone:zone];
copy->fNameAttributes = fNameAttributes;
copy->fStatusAttributes = fStatusAttributes;
return copy;
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
//icon
[self.favIcon drawInRect:[self imageRectForBounds:cellFrame] fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
//set table colors
NSColor *nameColor, *statusColor;
if (self.backgroundStyle == NSBackgroundStyleEmphasized)
{
nameColor = statusColor = NSColor.whiteColor;
}
else
{
nameColor = NSColor.labelColor;
statusColor = NSColor.secondaryLabelColor;
}
fNameAttributes[NSForegroundColorAttributeName] = nameColor;
fStatusAttributes[NSForegroundColorAttributeName] = statusColor;
TrackerNode* node = (TrackerNode*)self.objectValue;
//name
NSAttributedString* nameString = self.attributedName;
NSRect const nameRect = [self rectForNameWithString:nameString inBounds:cellFrame];
[nameString drawInRect:nameRect];
//count strings
NSAttributedString* seederString = [self attributedCount:node.totalSeeders];
NSRect const seederRect = [self rectForCountWithString:seederString withAboveRect:nameRect inBounds:cellFrame];
[seederString drawInRect:seederRect];
NSAttributedString* leecherString = [self attributedCount:node.totalLeechers];
NSRect const leecherRect = [self rectForCountWithString:leecherString withAboveRect:seederRect inBounds:cellFrame];
[leecherString drawInRect:leecherRect];
NSAttributedString* downloadedString = [self attributedCount:node.totalDownloaded];
NSRect const downloadedRect = [self rectForCountWithString:downloadedString withAboveRect:leecherRect inBounds:cellFrame];
[downloadedString drawInRect:downloadedRect];
//count label strings
NSString* seederLabelBaseString = [NSLocalizedString(@"Seeders", "tracker peer stat") stringByAppendingFormat:@": "];
NSAttributedString* seederLabelString = [self attributedStatusWithString:seederLabelBaseString];
NSRect const seederLabelRect = [self rectForCountLabelWithString:seederLabelString withRightRect:seederRect inBounds:cellFrame];
[seederLabelString drawInRect:seederLabelRect];
NSString* leecherLabelBaseString = [NSLocalizedString(@"Leechers", "tracker peer stat") stringByAppendingFormat:@": "];
NSAttributedString* leecherLabelString = [self attributedStatusWithString:leecherLabelBaseString];
NSRect const leecherLabelRect = [self rectForCountLabelWithString:leecherLabelString withRightRect:leecherRect
inBounds:cellFrame];
[leecherLabelString drawInRect:leecherLabelRect];
NSString* downloadedLabelBaseString = [NSLocalizedString(@"Downloaded", "tracker peer stat") stringByAppendingFormat:@": "];
NSAttributedString* downloadedLabelString = [self attributedStatusWithString:downloadedLabelBaseString];
NSRect const downloadedLabelRect = [self rectForCountLabelWithString:downloadedLabelString withRightRect:downloadedRect
inBounds:cellFrame];
[downloadedLabelString drawInRect:downloadedLabelRect];
//status strings
NSAttributedString* lastAnnounceString = [self attributedStatusWithString:node.lastAnnounceStatusString];
NSRect const lastAnnounceRect = [self rectForStatusWithString:lastAnnounceString withAboveRect:nameRect
withRightRect:seederLabelRect
inBounds:cellFrame];
[lastAnnounceString drawInRect:lastAnnounceRect];
NSAttributedString* nextAnnounceString = [self attributedStatusWithString:node.nextAnnounceStatusString];
NSRect const nextAnnounceRect = [self rectForStatusWithString:nextAnnounceString withAboveRect:lastAnnounceRect
withRightRect:leecherLabelRect
inBounds:cellFrame];
[nextAnnounceString drawInRect:nextAnnounceRect];
NSAttributedString* lastScrapeString = [self attributedStatusWithString:node.lastScrapeStatusString];
NSRect const lastScrapeRect = [self rectForStatusWithString:lastScrapeString withAboveRect:nextAnnounceRect
withRightRect:downloadedLabelRect
inBounds:cellFrame];
[lastScrapeString drawInRect:lastScrapeRect];
}
- (NSImage*)favIcon
{
id icon = nil;
NSURL* address = [NSURL URLWithString:((TrackerNode*)self.objectValue).fullAnnounceAddress];
NSString* host;
if ((host = address.host))
{
//don't try to parse ip address
BOOL const separable = !tr_addressIsIP(host.UTF8String);
NSArray* hostComponents = separable ? [host componentsSeparatedByString:@"."] : nil;
//let's try getting the tracker address without using any subdomains
NSString* baseAddress;
if (separable && hostComponents.count > 1)
{
baseAddress = [NSString stringWithFormat:@"http://%@.%@", hostComponents[hostComponents.count - 2], hostComponents.lastObject];
}
else
{
baseAddress = [NSString stringWithFormat:@"http://%@", host];
}
icon = [fTrackerIconCache objectForKey:baseAddress];
if (!icon)
{
[self loadTrackerIcon:baseAddress];
}
}
if ((icon && icon != [NSNull null]))
{
return icon;
}
if (@available(macOS 11.0, *)) {
NSImage* result = [NSImage imageWithSystemSymbolName:@"globe" accessibilityDescription:nil];
[result lockFocus];
[NSColor.textColor set];
NSRect imageRect = {NSZeroPoint, [result size]};
NSRectFillUsingOperation(imageRect, NSCompositingOperationSourceIn);
[result unlockFocus];
return result;
}
return [NSImage imageNamed:@"FavIcon"];
}
#warning better favicon detection
- (void)loadTrackerIcon:(NSString*)baseAddress
{
if ([fTrackerIconLoading containsObject:baseAddress])
{
return;
}
[fTrackerIconLoading addObject:baseAddress];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSImage* icon = nil;
NSArray<NSString*>* filenamesToTry = @[ @"favicon.png", @"favicon.ico" ];
for (NSString* filename in filenamesToTry)
{
NSURL* favIconUrl = [NSURL URLWithString:[baseAddress stringByAppendingPathComponent:filename]];
NSURLRequest* request = [NSURLRequest requestWithURL:favIconUrl cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30.0];
NSData* iconData = [NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:NULL];
if (iconData)
{
icon = [[NSImage alloc] initWithData:iconData];
if (icon)
{
break;
}
}
}
dispatch_async(dispatch_get_main_queue(), ^{
if (icon)
{
[fTrackerIconCache setObject:icon forKey:baseAddress];
[[self controlView] setNeedsDisplay:YES];
}
else
{
[fTrackerIconCache setObject:[NSNull null] forKey:baseAddress];
}
[fTrackerIconLoading removeObject:baseAddress];
});
});
}
- (NSRect)imageRectForBounds:(NSRect)bounds
{
return NSMakeRect(NSMinX(bounds) + PADDING_HORIZONAL, NSMinY(bounds) + PADDING_ABOVE_ICON, ICON_SIZE, ICON_SIZE);
}
- (NSRect)rectForNameWithString:(NSAttributedString*)string inBounds:(NSRect)bounds
{
NSRect result;
result.origin.x = NSMinX(bounds) + PADDING_HORIZONAL + ICON_SIZE + PADDING_BETWEEN_ICON_AND_NAME;
result.origin.y = NSMinY(bounds) + PADDING_ABOVE_NAME;
result.size.height = [string size].height;
result.size.width = NSMaxX(bounds) - NSMinX(result) - PADDING_HORIZONAL;
return result;
}
- (NSRect)rectForCountWithString:(NSAttributedString*)string withAboveRect:(NSRect)aboveRect inBounds:(NSRect)bounds
{
return NSMakeRect(
NSMaxX(bounds) - PADDING_HORIZONAL - COUNT_WIDTH,
NSMaxY(aboveRect) + PADDING_BETWEEN_LINES,
COUNT_WIDTH,
[string size].height);
}
- (NSRect)rectForCountLabelWithString:(NSAttributedString*)string withRightRect:(NSRect)rightRect inBounds:(NSRect)bounds
{
NSRect result = rightRect;
result.size.width = [string size].width;
result.origin.x -= NSWidth(result);
return result;
}
- (NSRect)rectForStatusWithString:(NSAttributedString*)string
withAboveRect:(NSRect)aboveRect
withRightRect:(NSRect)rightRect
inBounds:(NSRect)bounds
{
NSRect result;
result.origin.x = NSMinX(bounds) + PADDING_STATUS_HORIZONAL;
result.origin.y = NSMaxY(aboveRect) + PADDING_BETWEEN_LINES;
result.size.height = [string size].height;
result.size.width = NSMinX(rightRect) - PADDING_BETWEEN_LINES_ON_SAME_LINE - NSMinX(result);
return result;
}
- (NSAttributedString*)attributedName
{
NSString* name = ((TrackerNode*)self.objectValue).host;
return [[NSAttributedString alloc] initWithString:name attributes:fNameAttributes];
}
- (NSAttributedString*)attributedStatusWithString:(NSString*)statusString
{
return [[NSAttributedString alloc] initWithString:statusString attributes:fStatusAttributes];
}
- (NSAttributedString*)attributedCount:(NSInteger)count
{
NSString* countString = count != -1 ? [NSString stringWithFormat:@"%ld", count] : NSLocalizedString(@"N/A", "tracker peer stat");
return [[NSAttributedString alloc] initWithString:countString attributes:fStatusAttributes];
}
@end