mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'master' of https://github.com/brentsimmons/NetNewsWire
This commit is contained in:
115
Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift
Normal file
115
Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) we’re not displaying featured images anywhere,
|
||||
// so let’s skip downloading them even if we find them.
|
||||
//
|
||||
// We’ll 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 there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user