305 lines
12 KiB
Plaintext
305 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 "CocoaCompatibility.h"
|
|
|
|
#import "TrackerCell.h"
|
|
#import "TrackerNode.h"
|
|
|
|
static CGFloat const kPaddingHorizontal = 3.0;
|
|
static CGFloat const kPaddingStatusHorizontal = 3.0;
|
|
static CGFloat const kIconSize = 16.0;
|
|
static CGFloat const kPaddingBetweenIconAndName = 4.0;
|
|
static CGFloat const kPaddingAboveIcon = 1.0;
|
|
static CGFloat const kPaddingAboveName = 1.0;
|
|
static CGFloat const kPaddingBetweenLines = 1.0;
|
|
static CGFloat const kPaddingBetweenLinesOnSameLine = 4.0;
|
|
static CGFloat const kCountWidth = 60.0;
|
|
|
|
@interface TrackerCell ()
|
|
|
|
@property(nonatomic, readonly) NSImage* favIcon;
|
|
@property(nonatomic, readonly) NSAttributedString* attributedName;
|
|
@property(nonatomic, readonly) NSMutableDictionary* fNameAttributes;
|
|
@property(nonatomic, readonly) NSMutableDictionary* fStatusAttributes;
|
|
|
|
@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.5], 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;
|
|
}
|
|
|
|
self.fNameAttributes[NSForegroundColorAttributeName] = nameColor;
|
|
self.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];
|
|
}
|
|
|
|
#pragma mark - Private
|
|
|
|
- (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 isIP = tr_addressIsIP(host.UTF8String);
|
|
NSArray* hostComponents = !isIP ? [host componentsSeparatedByString:@"."] : nil;
|
|
|
|
if (!isIP && hostComponents.count >= 2)
|
|
{
|
|
NSString* domain = hostComponents[hostComponents.count - 2];
|
|
NSString* tld = hostComponents[hostComponents.count - 1];
|
|
NSString* baseAddress = [NSString stringWithFormat:@"%@.%@", domain, tld];
|
|
|
|
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"];
|
|
}
|
|
|
|
- (void)loadTrackerIcon:(NSString*)baseAddress
|
|
{
|
|
if ([fTrackerIconLoading containsObject:baseAddress])
|
|
{
|
|
return;
|
|
}
|
|
[fTrackerIconLoading addObject:baseAddress];
|
|
|
|
NSString* favIconUrl = [NSString stringWithFormat:@"https://icons.duckduckgo.com/ip3/%@.ico", baseAddress];
|
|
|
|
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:favIconUrl] cachePolicy:NSURLRequestUseProtocolCachePolicy
|
|
timeoutInterval:30.0];
|
|
|
|
NSURLSessionDataTask* task = [NSURLSession.sharedSession
|
|
dataTaskWithRequest:request completionHandler:^(NSData* iconData, NSURLResponse* response, NSError* error) {
|
|
BOOL ok = ((NSHTTPURLResponse*)response).statusCode == 200 ? YES : NO;
|
|
if (!ok)
|
|
{
|
|
return;
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSImage* icon = [[NSImage alloc] initWithData:iconData];
|
|
if (icon)
|
|
{
|
|
[fTrackerIconCache setObject:icon forKey:baseAddress];
|
|
|
|
[self.controlView setNeedsDisplay:YES];
|
|
}
|
|
else
|
|
{
|
|
[fTrackerIconCache setObject:[NSNull null] forKey:baseAddress];
|
|
}
|
|
|
|
[fTrackerIconLoading removeObject:baseAddress];
|
|
});
|
|
}];
|
|
[task resume];
|
|
}
|
|
|
|
- (NSRect)imageRectForBounds:(NSRect)bounds
|
|
{
|
|
return NSMakeRect(NSMinX(bounds) + kPaddingHorizontal, NSMinY(bounds) + kPaddingAboveIcon, kIconSize, kIconSize);
|
|
}
|
|
|
|
- (NSRect)rectForNameWithString:(NSAttributedString*)string inBounds:(NSRect)bounds
|
|
{
|
|
NSRect result;
|
|
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kIconSize + kPaddingBetweenIconAndName;
|
|
result.origin.y = NSMinY(bounds) + kPaddingAboveName;
|
|
|
|
result.size.height = [string size].height;
|
|
result.size.width = NSMaxX(bounds) - NSMinX(result) - kPaddingHorizontal;
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSRect)rectForCountWithString:(NSAttributedString*)string withAboveRect:(NSRect)aboveRect inBounds:(NSRect)bounds
|
|
{
|
|
return NSMakeRect(
|
|
NSMaxX(bounds) - kPaddingHorizontal - kCountWidth,
|
|
NSMaxY(aboveRect) + kPaddingBetweenLines,
|
|
kCountWidth,
|
|
[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) + kPaddingStatusHorizontal;
|
|
result.origin.y = NSMaxY(aboveRect) + kPaddingBetweenLines;
|
|
|
|
result.size.height = [string size].height;
|
|
result.size.width = NSMinX(rightRect) - kPaddingBetweenLinesOnSameLine - NSMinX(result);
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSAttributedString*)attributedName
|
|
{
|
|
NSString* name = ((TrackerNode*)self.objectValue).host;
|
|
return [[NSAttributedString alloc] initWithString:name attributes:self.fNameAttributes];
|
|
}
|
|
|
|
- (NSAttributedString*)attributedStatusWithString:(NSString*)statusString
|
|
{
|
|
return [[NSAttributedString alloc] initWithString:statusString attributes:self.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:self.fStatusAttributes];
|
|
}
|
|
|
|
@end
|