This commit is contained in:
Maurice Parker
2019-09-17 11:09:23 -05:00
6 changed files with 180 additions and 186 deletions

View File

@@ -0,0 +1,115 @@
//
// TimelineAvatarView.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/15/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import AppKit
final class TimelineAvatarView: NSView {
var image: NSImage? = nil {
didSet {
if image !== oldValue {
imageView.image = image
needsDisplay = true
needsLayout = true
}
}
}
override var isFlipped: Bool {
return true
}
private let imageView: NSImageView = {
let imageView = NSImageView(frame: NSRect.zero)
imageView.animates = false
imageView.imageAlignment = .alignCenter
imageView.imageScaling = .scaleProportionallyUpOrDown
return imageView
}()
private var hasExposedVerticalBackground: Bool {
return imageView.frame.size.height < bounds.size.height
}
private static var lightBackgroundColor = AppAssets.avatarLightBackgroundColor
private static var darkBackgroundColor = AppAssets.avatarDarkBackgroundColor
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
convenience init() {
self.init(frame: NSRect.zero)
}
override func viewDidMoveToSuperview() {
needsLayout = true
needsDisplay = true
}
override func layout() {
resizeSubviews(withOldSize: NSZeroSize)
}
override func resizeSubviews(withOldSize oldSize: NSSize) {
imageView.rs_setFrameIfNotEqual(rectForImageView())
}
override func draw(_ dirtyRect: NSRect) {
guard hasExposedVerticalBackground else {
return
}
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? TimelineAvatarView.darkBackgroundColor : TimelineAvatarView.lightBackgroundColor
color.set()
dirtyRect.fill()
}
}
private extension TimelineAvatarView {
func commonInit() {
addSubview(imageView)
wantsLayer = true
}
func rectForImageView() -> NSRect {
guard let image = image else {
return NSRect.zero
}
let imageSize = image.size
let viewSize = bounds.size
if imageSize.height == imageSize.width {
if imageSize.height >= viewSize.height * 0.75 {
// Close enough to viewSize to scale up the image.
return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height)
}
let offset = floor((viewSize.height - imageSize.height) / 2.0)
return NSMakeRect(offset, offset, imageSize.width, imageSize.height)
}
else if imageSize.height > imageSize.width {
let factor = viewSize.height / imageSize.height
let width = imageSize.width * factor
let originX = floor((viewSize.width - width) / 2.0)
return NSMakeRect(originX, 0.0, width, viewSize.height)
}
// Wider than tall: imageSize.width > imageSize.height
let factor = viewSize.width / imageSize.width
let height = imageSize.height * factor
let originY = floor((viewSize.height - height) / 2.0)
return NSMakeRect(0.0, originY, viewSize.width, height)
}
}

View File

@@ -18,13 +18,7 @@ class TimelineTableCellView: NSTableCellView {
private let dateView = TimelineTableCellView.singleLineTextField()
private let feedNameView = TimelineTableCellView.singleLineTextField()
private lazy var avatarImageView: NSImageView = {
let imageView = TimelineTableCellView.imageView(with: AppAssets.genericFeedImage, scaling: .scaleNone)
imageView.imageAlignment = .alignTop
imageView.imageScaling = .scaleProportionallyDown
imageView.wantsLayer = true
return imageView
}()
private lazy var avatarView = TimelineAvatarView()
private let starView = TimelineTableCellView.imageView(with: AppAssets.timelineStar, scaling: .scaleNone)
private let separatorView = TimelineTableCellView.separatorView()
@@ -43,7 +37,7 @@ class TimelineTableCellView: NSTableCellView {
didSet {
if cellAppearance != oldValue {
updateTextFieldFonts()
avatarImageView.layer?.cornerRadius = cellAppearance.avatarCornerRadius
avatarView.layer?.cornerRadius = cellAppearance.avatarCornerRadius
needsLayout = true
}
}
@@ -125,7 +119,7 @@ class TimelineTableCellView: NSTableCellView {
dateView.rs_setFrameIfNotEqual(layoutRects.dateRect)
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect)
avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect)
avatarView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect)
starView.rs_setFrameIfNotEqual(layoutRects.starRect)
separatorView.rs_setFrameIfNotEqual(layoutRects.separatorRect)
}
@@ -213,7 +207,7 @@ private extension TimelineTableCellView {
addSubviewAtInit(unreadIndicatorView, hidden: true)
addSubviewAtInit(dateView, hidden: false)
addSubviewAtInit(feedNameView, hidden: true)
addSubviewAtInit(avatarImageView, hidden: true)
addSubviewAtInit(avatarView, hidden: true)
addSubviewAtInit(starView, hidden: true)
addSubviewAtInit(separatorView, hidden: !AppDefaults.timelineShowsSeparators)
@@ -222,7 +216,7 @@ private extension TimelineTableCellView {
func updatedLayoutRects() -> TimelineCellLayout {
return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasAvatar: avatarImageView.image != nil)
return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasAvatar: avatarView.image != nil)
}
func updateTitleView() {
@@ -277,19 +271,19 @@ private extension TimelineTableCellView {
return
}
showView(avatarImageView)
if avatarImageView.image !== image {
avatarImageView.image = image
showView(avatarView)
if avatarView.image !== image {
avatarView.image = image
needsLayout = true
}
}
func makeAvatarEmpty() {
if avatarImageView.image != nil {
avatarImageView.image = nil
if avatarView.image != nil {
avatarView.image = nil
needsLayout = true
}
hideView(avatarImageView)
hideView(avatarView)
}
func hideView(_ view: NSView) {

View File

@@ -11,10 +11,6 @@ import RSCore
import Articles
import Account
extension Notification.Name {
static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification")
}
protocol TimelineDelegate: class {
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
}
@@ -159,7 +155,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
override func viewDidLoad() {
cellAppearance = TimelineCellAppearance(showAvatar: false, fontSize: fontSize)
cellAppearanceWithAvatar = TimelineCellAppearance(showAvatar: true, fontSize: fontSize)
@@ -171,7 +166,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
tableView.keyboardDelegate = keyboardDelegate
if !didRegisterForNotifications {
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
@@ -182,9 +176,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
DistributedNotificationCenter.default.addObserver(self, selector: #selector(appleInterfaceThemeChanged), name: .AppleInterfaceThemeChangedNotification, object: nil)
didRegisterForNotifications = true
didRegisterForNotifications = true
}
}
@@ -192,42 +185,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
sharingServiceDelegate = SharingServiceDelegate(self.view.window)
}
// MARK: State Restoration
// private static let stateRestorationSelectedArticles = "selectedArticles"
//
// override func encodeRestorableState(with coder: NSCoder) {
//
// super.encodeRestorableState(with: coder)
//
// coder.encode(self.selectedArticleIDs(), forKey: TimelineViewController.stateRestorationSelectedArticles)
// }
//
// override func restoreState(with coder: NSCoder) {
//
// super.restoreState(with: coder)
//
// if let restoredArticleIDs = (try? coder.decodeTopLevelObject(forKey: TimelineViewController.stateRestorationSelectedArticles)) as? [String] {
// self.restoreSelection(restoredArticleIDs)
// }
// }
// MARK: Appearance Change
private func fontSizeDidChange() {
cellAppearance = TimelineCellAppearance(showAvatar: false, fontSize: fontSize)
cellAppearanceWithAvatar = TimelineCellAppearance(showAvatar: true, fontSize: fontSize)
updateRowHeights()
performBlockAndRestoreSelection {
tableView.reloadData()
}
}
// MARK: - API
func markAllAsRead() {
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
return
}
@@ -235,12 +195,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
func canMarkAllAsRead() -> Bool {
return articles.canMarkAllAsRead()
}
func canMarkSelectedArticlesAsRead() -> Bool {
return selectedArticles.canMarkAllAsRead()
}
@@ -257,14 +215,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Actions
@objc func openArticleInBrowser(_ sender: Any?) {
if let link = oneSelectedArticle?.preferredLink {
Browser.open(link)
}
}
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
guard !selectedArticles.isEmpty else {
return
}
@@ -281,7 +237,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@IBAction func markSelectedArticlesAsRead(_ sender: Any?) {
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: true, undoManager: undoManager) else {
return
}
@@ -289,7 +244,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else {
return
}
@@ -297,12 +251,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@IBAction func copy(_ sender: Any?) {
NSPasteboard.general.copyObjects(selectedArticles)
}
@IBAction func selectNextUp(_ sender: Any?) {
guard let lastSelectedRow = tableView.selectedRowIndexes.last else {
return
}
@@ -324,7 +276,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@IBAction func selectNextDown(_ sender: Any?) {
guard let firstSelectedRow = tableView.selectedRowIndexes.first else {
return
}
@@ -347,7 +298,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
func toggleReadStatusForSelectedArticles() {
// If any one of the selected articles is unread, then mark them as read.
// If all articles are read, then mark them as unread them.
@@ -392,12 +342,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
func markStarredCommandStatus() -> MarkCommandValidationStatus {
return MarkCommandValidationStatus.statusFor(selectedArticles) { $0.anyArticleIsUnstarred() }
}
func markReadCommandStatus() -> MarkCommandValidationStatus {
return MarkCommandValidationStatus.statusFor(selectedArticles) { $0.anyArticleIsUnread() }
}
@@ -439,7 +387,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Navigation
func goToNextUnread() {
guard let ix = indexOfNextUnreadArticle() else {
return
}
@@ -449,7 +396,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
func canGoToNextUnread() -> Bool {
guard let _ = indexOfNextUnreadArticle() else {
return false
}
@@ -457,12 +403,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
func indexOfNextUnreadArticle() -> Int? {
return articles.rowOfNextUnreadArticle(tableView.selectedRow)
}
func focus() {
guard let window = tableView.window else {
return
}
@@ -476,7 +420,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Notifications
@objc func statusesDidChange(_ note: Notification) {
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
return
}
@@ -485,7 +428,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
guard showAvatars, let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
return
}
@@ -501,7 +443,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@objc func avatarDidBecomeAvailable(_ note: Notification) {
guard showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
return
}
@@ -529,7 +470,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@objc func accountDidDownloadArticles(_ note: Notification) {
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
return
}
@@ -559,25 +499,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
@objc func userDefaultsDidChange(_ note: Notification) {
self.fontSize = AppDefaults.timelineFontSize
self.sortDirection = AppDefaults.timelineSortDirection
self.groupByFeed = AppDefaults.timelineGroupByFeed
}
@objc func appleInterfaceThemeChanged(_ note: Notification) {
appDelegate.authorAvatarDownloader.resetCache()
appDelegate.feedIconDownloader.resetCache()
appDelegate.faviconDownloader.resetCache()
performBlockAndRestoreSelection {
tableView.reloadData()
}
}
// MARK: - Reloading Data
private func cellForRowView(_ rowView: NSView) -> NSView? {
for oneView in rowView.subviews where oneView is TimelineTableCellView {
return oneView
}
@@ -626,7 +555,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
// MARK: - Cell Configuring
private func calculateRowHeight(showingFeedNames: Bool) -> CGFloat {
let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
let prototypeID = "prototype"
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, userDeleted: false, dateArrived: Date())
@@ -638,7 +566,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
}
private func updateRowHeights() {
rowHeightWithFeedName = calculateRowHeight(showingFeedNames: true)
rowHeightWithoutFeedName = calculateRowHeight(showingFeedNames: false)
updateTableViewRowHeight()
@@ -686,7 +613,6 @@ extension TimelineViewController: NSMenuDelegate {
extension TimelineViewController: NSUserInterfaceValidations {
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(openArticleInBrowser(_:)) {
let currentLink = oneSelectedArticle?.preferredLink
return currentLink != nil
@@ -703,7 +629,6 @@ extension TimelineViewController: NSUserInterfaceValidations {
// MARK: - NSTableViewDataSource
extension TimelineViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return articles.count
}
@@ -723,7 +648,6 @@ extension TimelineViewController: NSTableViewDataSource {
// MARK: - NSTableViewDelegate
extension TimelineViewController: NSTableViewDelegate {
private static let rowViewIdentifier = NSUserInterfaceItemIdentifier(rawValue: "timelineRow")
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
@@ -774,8 +698,6 @@ extension TimelineViewController: NSTableViewDelegate {
}
selectionDidChange(selectedArticles)
// self.invalidateRestorableState()
}
private func selectionDidChange(_ selectedArticles: ArticleArray?) {
@@ -784,27 +706,40 @@ extension TimelineViewController: NSTableViewDelegate {
private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
cell.objectValue = article
let avatar = article.avatarImage()
let featuredImage = featuredImageFor(article)
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: featuredImage)
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: nil)
}
private func featuredImageFor(_ article: Article) -> NSImage? {
// At this writing (17 June 2019) were not displaying featured images anywhere,
// so lets skip downloading them even if we find them.
//
// Well revisit this later.
private func avatarFor(_ article: Article) -> NSImage? {
if !showAvatars {
return nil
}
// if let url = article.imageURL {
// if let imageData = appDelegate.imageDownloader.image(for: url) {
// return NSImage(data: imageData)
// }
// }
if let authors = article.authors {
for author in authors {
if let image = avatarForAuthor(author) {
return image
}
}
}
return nil
guard let feed = article.feed else {
return nil
}
if let feedIcon = appDelegate.feedIconDownloader.icon(for: feed) {
return feedIcon
}
if let favicon = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
return favicon
}
return FaviconGenerator.favicon(feed)
}
private func avatarForAuthor(_ author: Author) -> NSImage? {
return appDelegate.authorAvatarDownloader.image(for: author)
}
private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
@@ -818,7 +753,6 @@ extension TimelineViewController: NSTableViewDelegate {
private extension TimelineViewController {
func startObservingUserDefaults() {
assert(timelineShowsSeparatorsObserver == nil)
timelineShowsSeparatorsObserver = UserDefaults.standard.observe(\UserDefaults.CorreiaSeparators) { [weak self] (_, _) in
guard let self = self, self.isViewLoaded else { return }
@@ -831,7 +765,6 @@ private extension TimelineViewController {
}
@objc func reloadAvailableCells() {
if let indexesToReload = tableView.indexesOfAvailableRows() {
reloadCells(for: indexesToReload)
}
@@ -848,17 +781,14 @@ private extension TimelineViewController {
}
func queueReloadAvailableCells() {
CoalescingQueue.standard.add(self, #selector(reloadAvailableCells))
}
func updateTableViewRowHeight() {
tableView.rowHeight = currentRowHeight
}
func updateShowAvatars() {
if showFeedNames {
self.showAvatars = true
return
@@ -892,12 +822,10 @@ private extension TimelineViewController {
}
func selectedArticleIDs() -> [String] {
return selectedArticles.articleIDs()
}
func restoreSelection(_ articleIDs: [String]) {
selectArticles(articleIDs)
if tableView.selectedRow != -1 {
tableView.scrollRowToVisible(tableView.selectedRow)
@@ -905,7 +833,6 @@ private extension TimelineViewController {
}
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
let savedSelection = selectedArticleIDs()
block()
restoreSelection(savedSelection)
@@ -937,7 +864,6 @@ private extension TimelineViewController {
}
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet()
articleIDs.forEach { (articleID) in
@@ -952,7 +878,18 @@ private extension TimelineViewController {
return indexes
}
// MARK: Fetching Articles
// MARK: - Appearance Change
private func fontSizeDidChange() {
cellAppearance = TimelineCellAppearance(showAvatar: false, fontSize: fontSize)
cellAppearanceWithAvatar = TimelineCellAppearance(showAvatar: true, fontSize: fontSize)
updateRowHeights()
performBlockAndRestoreSelection {
tableView.reloadData()
}
}
// MARK: - Fetching Articles
func fetchAndReplaceArticlesSync() {
// To be called when the user has made a change of selection in the sidebar.
@@ -1020,7 +957,6 @@ private extension TimelineViewController {
}
func selectArticles(_ articleIDs: [String]) {
let indexesToSelect = indexesForArticleIDs(Set(articleIDs))
if indexesToSelect.isEmpty {
tableView.deselectAll(self)
@@ -1030,12 +966,10 @@ private extension TimelineViewController {
}
func queueFetchAndMergeArticles() {
TimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
}
func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool {
if objects1 == nil && objects2 == nil {
return true
}
@@ -1069,7 +1003,6 @@ private extension TimelineViewController {
}
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
// Return true if theres a match or if a folder contains (recursively) one of feeds
guard let representedObjects = representedObjects else {

View File

@@ -241,6 +241,7 @@
84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; };
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* TimelineAvatarView.swift */; };
847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; };
848362FD2262A30800DA1D35 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; };
848362FF2262A30E00DA1D35 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
@@ -917,6 +918,7 @@
8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = "<group>"; };
847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = "<group>"; };
847CD6C9232F4CBF00FAC46D /* TimelineAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineAvatarView.swift; sourceTree = "<group>"; };
847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = "<group>"; };
848362FC2262A30800DA1D35 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
848362FE2262A30E00DA1D35 /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = template.html; sourceTree = "<group>"; };
@@ -1623,6 +1625,7 @@
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
847CD6C9232F4CBF00FAC46D /* TimelineAvatarView.swift */,
);
path = Cell;
sourceTree = "<group>";
@@ -2188,12 +2191,12 @@
ProvisioningStyle = Automatic;
};
6581C73220CED60000F4AD34 = {
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual;
};
840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
@@ -2203,8 +2206,8 @@
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual;
SystemCapabilities = {
com.apple.HardenedRuntime = {
enabled = 1;
@@ -2213,7 +2216,7 @@
};
849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = 9C84TZ7Q6Z;
ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0;
};
@@ -2724,6 +2727,7 @@
files = (
84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */,
848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */,
847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
51EF0F7A22771B890050506E /* ColorHash.swift in Sources */,
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */,

View File

@@ -27,64 +27,12 @@ extension RSImage {
guard var cgImage = RSImage.scaleImage(data, maxPixelSize: scaledMaxPixelSize) else {
return nil
}
if cgImage.width < avatarSize || cgImage.height < avatarSize {
cgImage = RSImage.compositeAvatar(cgImage)
}
#if os(iOS)
return RSImage(cgImage: cgImage)
#else
let size = NSSize(width: cgImage.width, height: cgImage.height)
return RSImage(cgImage: cgImage, size: size)
#endif
#endif
}
}
private extension RSImage {
#if os(iOS)
static func compositeAvatar(_ avatar: CGImage) -> CGImage {
let rect = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize)
UIGraphicsBeginImageContext(rect.size)
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(AppAssets.avatarBackgroundColor.cgColor)
context.fill(rect)
context.translateBy(x: 0.0, y: CGFloat(integerLiteral: avatarSize));
context.scaleBy(x: 1.0, y: -1.0)
let avatarRect = CGRect(x: (avatarSize - avatar.width) / 2, y: (avatarSize - avatar.height) / 2, width: avatar.width, height: avatar.height)
context.draw(avatar, in: avatarRect)
}
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!.cgImage!
}
#else
static func compositeAvatar(_ avatar: CGImage) -> CGImage {
var resultRect = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize)
let resultImage = NSImage(size: resultRect.size)
resultImage.lockFocus()
if let context = NSGraphicsContext.current?.cgContext {
if NSApplication.shared.effectiveAppearance.isDarkMode {
context.setFillColor(AppAssets.avatarDarkBackgroundColor.cgColor)
} else {
context.setFillColor(AppAssets.avatarLightBackgroundColor.cgColor)
}
context.fill(resultRect)
let avatarRect = CGRect(x: (avatarSize - avatar.width) / 2, y: (avatarSize - avatar.height) / 2, width: avatar.width, height: avatar.height)
context.draw(avatar, in: avatarRect)
}
resultImage.unlockFocus()
return resultImage.cgImage(forProposedRect: &resultRect, context: nil, hints: nil)!
}
#endif
}