mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Rename MainFeed and MainTimeline folders to Sidebar and Timeline.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// MainTimelineAccessibilityCellLayout.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/29/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
|
||||
|
||||
let height: CGFloat
|
||||
let unreadIndicatorRect: CGRect
|
||||
let starRect: CGRect
|
||||
let iconImageRect: CGRect
|
||||
let titleRect: CGRect
|
||||
let summaryRect: CGRect
|
||||
let feedNameRect: CGRect
|
||||
let dateRect: CGRect
|
||||
|
||||
init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) {
|
||||
|
||||
var currentPoint = CGPoint.zero
|
||||
currentPoint.x = MainTimelineDefaultCellLayout.cellPadding.left + insets.left + MainTimelineDefaultCellLayout.unreadCircleMarginLeft
|
||||
currentPoint.y = MainTimelineDefaultCellLayout.cellPadding.top
|
||||
|
||||
// Unread Indicator and Star
|
||||
self.unreadIndicatorRect = MainTimelineAccessibilityCellLayout.rectForUnreadIndicator(currentPoint)
|
||||
self.starRect = MainTimelineAccessibilityCellLayout.rectForStar(currentPoint)
|
||||
|
||||
// Start the point at the beginning position of the main block
|
||||
currentPoint.x += MainTimelineDefaultCellLayout.unreadCircleDimension + MainTimelineDefaultCellLayout.unreadCircleMarginRight
|
||||
|
||||
// Icon Image
|
||||
if cellData.showIcon {
|
||||
self.iconImageRect = MainTimelineAccessibilityCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
|
||||
currentPoint.y = self.iconImageRect.maxY
|
||||
} else {
|
||||
self.iconImageRect = CGRect.zero
|
||||
}
|
||||
|
||||
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
|
||||
|
||||
// Title Text Block
|
||||
let (titleRect, numberOfLinesForTitle) = MainTimelineAccessibilityCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
|
||||
self.titleRect = titleRect
|
||||
|
||||
// Summary Text Block
|
||||
if self.titleRect != CGRect.zero {
|
||||
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
|
||||
}
|
||||
self.summaryRect = MainTimelineAccessibilityCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
|
||||
|
||||
currentPoint.y = [self.titleRect, self.summaryRect].maxY()
|
||||
|
||||
if cellData.showFeedName != .none {
|
||||
self.feedNameRect = MainTimelineAccessibilityCellLayout.rectForFeedName(cellData, currentPoint, textAreaWidth)
|
||||
currentPoint.y = self.feedNameRect.maxY
|
||||
} else {
|
||||
self.feedNameRect = CGRect.zero
|
||||
}
|
||||
|
||||
// Feed Name and Pub Date
|
||||
self.dateRect = MainTimelineAccessibilityCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
|
||||
|
||||
self.height = self.dateRect.maxY + MainTimelineDefaultCellLayout.cellPadding.bottom
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Calculate Rects
|
||||
|
||||
private extension MainTimelineAccessibilityCellLayout {
|
||||
|
||||
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
|
||||
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
|
||||
r.size = size
|
||||
r.origin = point
|
||||
|
||||
return r
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
84
iOS/MainWindow/Timeline/Cell/MainTimelineCellData.swift
Normal file
84
iOS/MainWindow/Timeline/Cell/MainTimelineCellData.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// MainTimelineCellData.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Articles
|
||||
|
||||
struct MainTimelineCellData {
|
||||
|
||||
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
||||
|
||||
let title: String
|
||||
let attributedTitle: NSAttributedString
|
||||
let summary: String
|
||||
let dateString: String
|
||||
let feedName: String
|
||||
let byline: String
|
||||
let showFeedName: ShowFeedName
|
||||
let iconImage: IconImage? // feed icon, user avatar, or favicon
|
||||
let showIcon: Bool // Make space even when icon is nil
|
||||
let read: Bool
|
||||
let starred: Bool
|
||||
let numberOfLines: Int
|
||||
let iconSize: IconSize
|
||||
|
||||
init(article: Article, showFeedName: ShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, numberOfLines: Int, iconSize: IconSize) {
|
||||
|
||||
self.title = ArticleStringFormatter.truncatedTitle(article)
|
||||
self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article)
|
||||
|
||||
let truncatedSummary = ArticleStringFormatter.truncatedSummary(article)
|
||||
if self.title.isEmpty && truncatedSummary.isEmpty {
|
||||
self.summary = Self.noText
|
||||
} else {
|
||||
self.summary = truncatedSummary
|
||||
}
|
||||
|
||||
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||
|
||||
if let feedName = feedName {
|
||||
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
|
||||
} else {
|
||||
self.feedName = ""
|
||||
}
|
||||
|
||||
if let byline = byline {
|
||||
self.byline = byline
|
||||
} else {
|
||||
self.byline = ""
|
||||
}
|
||||
|
||||
self.showFeedName = showFeedName
|
||||
|
||||
self.showIcon = showIcon
|
||||
self.iconImage = iconImage
|
||||
|
||||
self.read = article.status.read
|
||||
self.starred = article.status.starred
|
||||
self.numberOfLines = numberOfLines
|
||||
self.iconSize = iconSize
|
||||
|
||||
}
|
||||
|
||||
init() { // Empty
|
||||
self.title = ""
|
||||
self.attributedTitle = NSAttributedString()
|
||||
self.summary = ""
|
||||
self.dateString = ""
|
||||
self.feedName = ""
|
||||
self.byline = ""
|
||||
self.showFeedName = .none
|
||||
self.showIcon = false
|
||||
self.iconImage = nil
|
||||
self.read = true
|
||||
self.starred = false
|
||||
self.numberOfLines = 0
|
||||
self.iconSize = .medium
|
||||
}
|
||||
|
||||
}
|
||||
112
iOS/MainWindow/Timeline/Cell/MainTimelineCellLayout.swift
Normal file
112
iOS/MainWindow/Timeline/Cell/MainTimelineCellLayout.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// MainTimelineCellLayout.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/29/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol MainTimelineCellLayout {
|
||||
|
||||
var height: CGFloat {get}
|
||||
var unreadIndicatorRect: CGRect {get}
|
||||
var starRect: CGRect {get}
|
||||
var iconImageRect: CGRect {get}
|
||||
var titleRect: CGRect {get}
|
||||
var summaryRect: CGRect {get}
|
||||
var feedNameRect: CGRect {get}
|
||||
var dateRect: CGRect {get}
|
||||
|
||||
}
|
||||
|
||||
extension MainTimelineCellLayout {
|
||||
|
||||
static func rectForUnreadIndicator(_ point: CGPoint) -> CGRect {
|
||||
var r = CGRect.zero
|
||||
r.size = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension)
|
||||
r.origin.x = point.x
|
||||
r.origin.y = point.y + 5
|
||||
return r
|
||||
}
|
||||
|
||||
static func rectForStar(_ point: CGPoint) -> CGRect {
|
||||
var r = CGRect.zero
|
||||
r.size.width = MainTimelineDefaultCellLayout.starDimension
|
||||
r.size.height = MainTimelineDefaultCellLayout.starDimension
|
||||
r.origin.x = floor(point.x - ((MainTimelineDefaultCellLayout.starDimension - MainTimelineDefaultCellLayout.unreadCircleDimension) / 2.0))
|
||||
r.origin.y = point.y + 3
|
||||
return r
|
||||
}
|
||||
|
||||
static func rectForIconView(_ point: CGPoint, iconSize: IconSize) -> CGRect {
|
||||
var r = CGRect.zero
|
||||
r.size = iconSize.size
|
||||
r.origin.x = point.x
|
||||
r.origin.y = point.y + 4
|
||||
return r
|
||||
}
|
||||
|
||||
static func rectForTitle(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) {
|
||||
|
||||
var r = CGRect.zero
|
||||
if cellData.title.isEmpty {
|
||||
return (r, 0)
|
||||
}
|
||||
|
||||
r.origin = point
|
||||
|
||||
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MainTimelineDefaultCellLayout.titleFont, numberOfLines: cellData.numberOfLines, width: Int(textAreaWidth))
|
||||
|
||||
r.size.width = textAreaWidth
|
||||
r.size.height = sizeInfo.size.height
|
||||
if sizeInfo.numberOfLinesUsed < 1 {
|
||||
r.size.height = 0
|
||||
}
|
||||
|
||||
return (r, sizeInfo.numberOfLinesUsed)
|
||||
|
||||
}
|
||||
|
||||
static func rectForSummary(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect {
|
||||
|
||||
let linesLeft = cellData.numberOfLines - linesUsed
|
||||
|
||||
var r = CGRect.zero
|
||||
if cellData.summary.isEmpty || linesLeft < 1 {
|
||||
return r
|
||||
}
|
||||
|
||||
r.origin = point
|
||||
|
||||
let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MainTimelineDefaultCellLayout.summaryFont, numberOfLines: linesLeft, width: Int(textAreaWidth))
|
||||
|
||||
r.size.width = textAreaWidth
|
||||
r.size.height = sizeInfo.size.height
|
||||
if sizeInfo.numberOfLinesUsed < 1 {
|
||||
r.size.height = 0
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
}
|
||||
|
||||
static func rectForFeedName(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
r.origin = point
|
||||
|
||||
let feedName = cellData.showFeedName == .feed ? cellData.feedName : cellData.byline
|
||||
let size = SingleLineUILabelSizer.size(for: feedName, font: MainTimelineDefaultCellLayout.feedNameFont)
|
||||
r.size = size
|
||||
|
||||
if r.size.width > textAreaWidth {
|
||||
r.size.width = textAreaWidth
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
126
iOS/MainWindow/Timeline/Cell/MainTimelineDefaultCellLayout.swift
Normal file
126
iOS/MainWindow/Timeline/Cell/MainTimelineDefaultCellLayout.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// MainTimelineDefaultCellLayout.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
|
||||
|
||||
static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20)
|
||||
|
||||
static let unreadCircleMarginLeft = CGFloat(integerLiteral: 0)
|
||||
static let unreadCircleDimension = CGFloat(integerLiteral: 12)
|
||||
static let unreadCircleSize = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension)
|
||||
static let unreadCircleMarginRight = CGFloat(integerLiteral: 8)
|
||||
|
||||
static let starDimension = CGFloat(integerLiteral: 16)
|
||||
static let starSize = CGSize(width: MainTimelineDefaultCellLayout.starDimension, height: MainTimelineDefaultCellLayout.starDimension)
|
||||
|
||||
static let iconMarginRight = CGFloat(integerLiteral: 8)
|
||||
static let iconCornerRadius = CGFloat(integerLiteral: 4)
|
||||
|
||||
static var titleFont: UIFont {
|
||||
return UIFont.preferredFont(forTextStyle: .headline)
|
||||
}
|
||||
static let titleBottomMargin = CGFloat(integerLiteral: 1)
|
||||
|
||||
static var feedNameFont: UIFont {
|
||||
return UIFont.preferredFont(forTextStyle: .footnote)
|
||||
}
|
||||
static let feedRightMargin = CGFloat(integerLiteral: 8)
|
||||
|
||||
static var dateFont: UIFont {
|
||||
return UIFont.preferredFont(forTextStyle: .footnote)
|
||||
}
|
||||
static let dateMarginBottom = CGFloat(integerLiteral: 1)
|
||||
|
||||
static var summaryFont: UIFont {
|
||||
return UIFont.preferredFont(forTextStyle: .body)
|
||||
}
|
||||
|
||||
let height: CGFloat
|
||||
let unreadIndicatorRect: CGRect
|
||||
let starRect: CGRect
|
||||
let iconImageRect: CGRect
|
||||
let titleRect: CGRect
|
||||
let summaryRect: CGRect
|
||||
let feedNameRect: CGRect
|
||||
let dateRect: CGRect
|
||||
|
||||
init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) {
|
||||
|
||||
var currentPoint = CGPoint.zero
|
||||
currentPoint.x = MainTimelineDefaultCellLayout.cellPadding.left + insets.left + MainTimelineDefaultCellLayout.unreadCircleMarginLeft
|
||||
currentPoint.y = MainTimelineDefaultCellLayout.cellPadding.top
|
||||
|
||||
// Unread Indicator and Star
|
||||
self.unreadIndicatorRect = MainTimelineDefaultCellLayout.rectForUnreadIndicator(currentPoint)
|
||||
self.starRect = MainTimelineDefaultCellLayout.rectForStar(currentPoint)
|
||||
|
||||
// Start the point at the beginning position of the main block
|
||||
currentPoint.x += MainTimelineDefaultCellLayout.unreadCircleDimension + MainTimelineDefaultCellLayout.unreadCircleMarginRight
|
||||
|
||||
// Icon Image
|
||||
if cellData.showIcon {
|
||||
self.iconImageRect = MainTimelineDefaultCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
|
||||
currentPoint.x = self.iconImageRect.maxX + MainTimelineDefaultCellLayout.iconMarginRight
|
||||
} else {
|
||||
self.iconImageRect = CGRect.zero
|
||||
}
|
||||
|
||||
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
|
||||
|
||||
// Title Text Block
|
||||
let (titleRect, numberOfLinesForTitle) = MainTimelineDefaultCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
|
||||
self.titleRect = titleRect
|
||||
|
||||
// Summary Text Block
|
||||
if self.titleRect != CGRect.zero {
|
||||
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
|
||||
}
|
||||
self.summaryRect = MainTimelineDefaultCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
|
||||
|
||||
var y = [self.titleRect, self.summaryRect].maxY()
|
||||
if y == 0 {
|
||||
y = iconImageRect.origin.y + iconImageRect.height
|
||||
// Necessary calculation of either feed name or date since we are working with dynamic font-sizes
|
||||
let tmp = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
|
||||
y -= tmp.height
|
||||
}
|
||||
currentPoint.y = y
|
||||
|
||||
// Feed Name and Pub Date
|
||||
self.dateRect = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
|
||||
|
||||
let feedNameWidth = textAreaWidth - (MainTimelineDefaultCellLayout.feedRightMargin + self.dateRect.size.width)
|
||||
self.feedNameRect = MainTimelineDefaultCellLayout.rectForFeedName(cellData, currentPoint, feedNameWidth)
|
||||
|
||||
self.height = [self.iconImageRect, self.feedNameRect].maxY() + MainTimelineDefaultCellLayout.cellPadding.bottom
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Calculate Rects
|
||||
|
||||
extension MainTimelineDefaultCellLayout {
|
||||
|
||||
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
|
||||
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
|
||||
r.size = size
|
||||
r.origin.x = (point.x + textAreaWidth) - size.width
|
||||
r.origin.y = point.y
|
||||
|
||||
return r
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
314
iOS/MainWindow/Timeline/Cell/MainTimelineTableViewCell.swift
Normal file
314
iOS/MainWindow/Timeline/Cell/MainTimelineTableViewCell.swift
Normal file
@@ -0,0 +1,314 @@
|
||||
//
|
||||
// MainTimelineTableViewCell.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 8/31/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
final class MainTimelineTableViewCell: VibrantTableViewCell {
|
||||
|
||||
private let titleView = MainTimelineTableViewCell.multiLineUILabel()
|
||||
private let summaryView = MainTimelineTableViewCell.multiLineUILabel()
|
||||
private let unreadIndicatorView = MainUnreadIndicatorView(frame: CGRect.zero)
|
||||
private let dateView = MainTimelineTableViewCell.singleLineUILabel()
|
||||
private let feedNameView = MainTimelineTableViewCell.singleLineUILabel()
|
||||
|
||||
private lazy var iconView = IconView()
|
||||
|
||||
private lazy var starView = {
|
||||
return NonIntrinsicImageView(image: AppImage.timelineStar)
|
||||
}()
|
||||
|
||||
private var unreadIndicatorPropertyAnimator: UIViewPropertyAnimator?
|
||||
private var starViewPropertyAnimator: UIViewPropertyAnimator?
|
||||
|
||||
var cellData: MainTimelineCellData! {
|
||||
didSet {
|
||||
updateSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
unreadIndicatorPropertyAnimator?.stopAnimation(true)
|
||||
unreadIndicatorPropertyAnimator = nil
|
||||
unreadIndicatorView.isHidden = true
|
||||
|
||||
starViewPropertyAnimator?.stopAnimation(true)
|
||||
starViewPropertyAnimator = nil
|
||||
starView.isHidden = true
|
||||
}
|
||||
|
||||
override var frame: CGRect {
|
||||
didSet {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
updateLabelVibrancy(titleView, color: labelColor, animated: animated)
|
||||
updateLabelVibrancy(summaryView, color: labelColor, animated: animated)
|
||||
updateLabelVibrancy(dateView, color: secondaryLabelColor, animated: animated)
|
||||
updateLabelVibrancy(feedNameView, color: secondaryLabelColor, animated: animated)
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
if self.isHighlighted || self.isSelected {
|
||||
self.unreadIndicatorView.backgroundColor = AppColor.vibrantText
|
||||
} else {
|
||||
self.unreadIndicatorView.backgroundColor = AppColor.secondaryAccent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.isHighlighted || self.isSelected {
|
||||
self.unreadIndicatorView.backgroundColor = AppColor.vibrantText
|
||||
} else {
|
||||
self.unreadIndicatorView.backgroundColor = AppColor.secondaryAccent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
let layout = updatedLayout(width: size.width)
|
||||
return CGSize(width: size.width, height: layout.height)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
||||
super.layoutSubviews()
|
||||
|
||||
let layout = updatedLayout(width: bounds.width)
|
||||
|
||||
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
|
||||
starView.setFrameIfNotEqual(layout.starRect)
|
||||
iconView.setFrameIfNotEqual(layout.iconImageRect)
|
||||
setFrame(for: titleView, rect: layout.titleRect)
|
||||
setFrame(for: summaryView, rect: layout.summaryRect)
|
||||
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
|
||||
dateView.setFrameIfNotEqual(layout.dateRect)
|
||||
|
||||
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
|
||||
func setIconImage(_ image: IconImage) {
|
||||
iconView.iconImage = image
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension MainTimelineTableViewCell {
|
||||
|
||||
static func singleLineUILabel() -> UILabel {
|
||||
let label = NonIntrinsicLabel()
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.allowsDefaultTighteningForTruncation = false
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}
|
||||
|
||||
static func multiLineUILabel() -> UILabel {
|
||||
let label = NonIntrinsicLabel()
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.allowsDefaultTighteningForTruncation = false
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}
|
||||
|
||||
func setFrame(for label: UILabel, rect: CGRect) {
|
||||
|
||||
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
|
||||
hideView(label)
|
||||
} else {
|
||||
showView(label)
|
||||
label.setFrameIfNotEqual(rect)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func addSubviewAtInit(_ view: UIView, hidden: Bool) {
|
||||
addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isHidden = hidden
|
||||
}
|
||||
|
||||
func commonInit() {
|
||||
|
||||
addSubviewAtInit(titleView, hidden: false)
|
||||
addSubviewAtInit(summaryView, hidden: true)
|
||||
addSubviewAtInit(unreadIndicatorView, hidden: true)
|
||||
addSubviewAtInit(dateView, hidden: false)
|
||||
addSubviewAtInit(feedNameView, hidden: true)
|
||||
addSubviewAtInit(iconView, hidden: true)
|
||||
addSubviewAtInit(starView, hidden: true)
|
||||
}
|
||||
|
||||
func updatedLayout(width: CGFloat) -> MainTimelineCellLayout {
|
||||
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
|
||||
return MainTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
|
||||
} else {
|
||||
return MainTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTitleView() {
|
||||
titleView.font = MainTimelineDefaultCellLayout.titleFont
|
||||
titleView.textColor = labelColor
|
||||
updateTextFieldAttributedText(titleView, cellData?.attributedTitle)
|
||||
}
|
||||
|
||||
func updateSummaryView() {
|
||||
summaryView.font = MainTimelineDefaultCellLayout.summaryFont
|
||||
summaryView.textColor = labelColor
|
||||
updateTextFieldText(summaryView, cellData?.summary)
|
||||
}
|
||||
|
||||
func updateDateView() {
|
||||
dateView.font = MainTimelineDefaultCellLayout.dateFont
|
||||
dateView.textColor = secondaryLabelColor
|
||||
updateTextFieldText(dateView, cellData.dateString)
|
||||
}
|
||||
|
||||
func updateTextFieldText(_ label: UILabel, _ text: String?) {
|
||||
let s = text ?? ""
|
||||
if label.text != s {
|
||||
label.text = s
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func updateTextFieldAttributedText(_ label: UILabel, _ text: NSAttributedString?) {
|
||||
var s = text ?? NSAttributedString(string: "")
|
||||
|
||||
if let fieldFont = label.font {
|
||||
s = s.adding(font: fieldFont)
|
||||
}
|
||||
|
||||
if label.attributedText != s {
|
||||
label.attributedText = s
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeedNameView() {
|
||||
switch cellData.showFeedName {
|
||||
case .feed:
|
||||
showView(feedNameView)
|
||||
feedNameView.font = MainTimelineDefaultCellLayout.feedNameFont
|
||||
feedNameView.textColor = secondaryLabelColor
|
||||
updateTextFieldText(feedNameView, cellData.feedName)
|
||||
case .byline:
|
||||
showView(feedNameView)
|
||||
feedNameView.font = MainTimelineDefaultCellLayout.feedNameFont
|
||||
feedNameView.textColor = secondaryLabelColor
|
||||
updateTextFieldText(feedNameView, cellData.byline)
|
||||
case .none:
|
||||
hideView(feedNameView)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUnreadIndicator() {
|
||||
if !unreadIndicatorView.isHidden && cellData.read && !cellData.starred {
|
||||
unreadIndicatorPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
|
||||
self?.unreadIndicatorView.alpha = 0
|
||||
}
|
||||
unreadIndicatorPropertyAnimator?.addCompletion { [weak self] _ in
|
||||
self?.unreadIndicatorView.isHidden = true
|
||||
self?.unreadIndicatorView.alpha = 1
|
||||
self?.unreadIndicatorPropertyAnimator = nil
|
||||
}
|
||||
unreadIndicatorPropertyAnimator?.startAnimation()
|
||||
} else {
|
||||
unreadIndicatorView.alpha = 1
|
||||
showOrHideView(unreadIndicatorView, cellData.read || cellData.starred)
|
||||
}
|
||||
}
|
||||
|
||||
func updateStarView() {
|
||||
if !starView.isHidden && cellData.read && !cellData.starred {
|
||||
starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
|
||||
self?.starView.alpha = 0
|
||||
}
|
||||
starViewPropertyAnimator?.addCompletion { [weak self] _ in
|
||||
self?.starView.isHidden = true
|
||||
self?.starView.alpha = 1
|
||||
self?.starViewPropertyAnimator = nil
|
||||
}
|
||||
starViewPropertyAnimator?.startAnimation()
|
||||
} else {
|
||||
starView.alpha = 1
|
||||
showOrHideView(starView, !cellData.starred)
|
||||
}
|
||||
}
|
||||
|
||||
func updateIconImage() {
|
||||
guard let image = cellData.iconImage, cellData.showIcon else {
|
||||
makeIconEmpty()
|
||||
return
|
||||
}
|
||||
|
||||
showView(iconView)
|
||||
|
||||
if iconView.iconImage !== cellData.iconImage {
|
||||
iconView.iconImage = image
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func updateAccessiblityLabel() {
|
||||
let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : ""
|
||||
let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), "
|
||||
let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)"
|
||||
accessibilityLabel = label
|
||||
}
|
||||
|
||||
func makeIconEmpty() {
|
||||
if iconView.iconImage != nil {
|
||||
iconView.iconImage = nil
|
||||
setNeedsLayout()
|
||||
}
|
||||
hideView(iconView)
|
||||
}
|
||||
|
||||
func hideView(_ view: UIView) {
|
||||
if !view.isHidden {
|
||||
view.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func showView(_ view: UIView) {
|
||||
if view.isHidden {
|
||||
view.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func showOrHideView(_ view: UIView, _ shouldHide: Bool) {
|
||||
if shouldHide {
|
||||
hideView(view)
|
||||
} else {
|
||||
showView(view)
|
||||
}
|
||||
}
|
||||
|
||||
func updateSubviews() {
|
||||
updateTitleView()
|
||||
updateSummaryView()
|
||||
updateDateView()
|
||||
updateFeedNameView()
|
||||
updateUnreadIndicator()
|
||||
updateStarView()
|
||||
updateIconImage()
|
||||
updateAccessiblityLabel()
|
||||
}
|
||||
|
||||
}
|
||||
19
iOS/MainWindow/Timeline/Cell/MainUnreadIndicatorView.swift
Normal file
19
iOS/MainWindow/Timeline/Cell/MainUnreadIndicatorView.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// MainUnreadIndicatorView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/16/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class MainUnreadIndicatorView: UIView {
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layer.cornerRadius = frame.size.width / 2.0
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
}
|
||||
181
iOS/MainWindow/Timeline/Cell/MultilineUILabelSizer.swift
Normal file
181
iOS/MainWindow/Timeline/Cell/MultilineUILabelSizer.swift
Normal file
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// UILabelSizerSpecifier.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/19/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// Get the height of an NSTextField given a string, font, and width.
|
||||
// Uses a cache. Avoids actually measuring text as much as possible.
|
||||
// Main thread only.
|
||||
|
||||
typealias WidthHeightCache = [Int: Int] // width: height
|
||||
|
||||
private struct UILabelSizerSpecifier: Hashable {
|
||||
|
||||
let numberOfLines: Int
|
||||
let font: UIFont
|
||||
}
|
||||
|
||||
struct TextFieldSizeInfo {
|
||||
|
||||
let size: CGSize // Integral size (ceiled)
|
||||
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
|
||||
}
|
||||
|
||||
final class MultilineUILabelSizer {
|
||||
|
||||
private let numberOfLines: Int
|
||||
private let font: UIFont
|
||||
private let singleLineHeightEstimate: Int
|
||||
private let doubleLineHeightEstimate: Int
|
||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||
private static var sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
|
||||
|
||||
private init(numberOfLines: Int, font: UIFont) {
|
||||
|
||||
self.numberOfLines = numberOfLines
|
||||
self.font = font
|
||||
|
||||
self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y", 200, font)
|
||||
self.doubleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font)
|
||||
|
||||
}
|
||||
|
||||
static func size(for string: String, font: UIFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo {
|
||||
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension MultilineUILabelSizer {
|
||||
|
||||
static func sizer(numberOfLines: Int, font: UIFont) -> MultilineUILabelSizer {
|
||||
|
||||
let specifier = UILabelSizerSpecifier(numberOfLines: numberOfLines, font: font)
|
||||
if let cachedSizer = sizers[specifier] {
|
||||
return cachedSizer
|
||||
}
|
||||
|
||||
let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font)
|
||||
sizers[specifier] = newSizer
|
||||
return newSizer
|
||||
|
||||
}
|
||||
|
||||
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
|
||||
|
||||
let textFieldHeight = height(for: string, width: width)
|
||||
let numberOfLinesUsed = numberOfLines(for: textFieldHeight)
|
||||
|
||||
let size = CGSize(width: width, height: textFieldHeight)
|
||||
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
|
||||
return sizeInfo
|
||||
|
||||
}
|
||||
|
||||
func height(for string: String, width: Int) -> Int {
|
||||
|
||||
if cache[string] == nil {
|
||||
cache[string] = WidthHeightCache()
|
||||
}
|
||||
|
||||
if let height = cache[string]![width] {
|
||||
return height
|
||||
}
|
||||
|
||||
if let height = heightConsideringNeighbors(cache[string]!, width) {
|
||||
return height
|
||||
}
|
||||
|
||||
var height = MultilineUILabelSizer.calculateHeight(string, width, font)
|
||||
|
||||
if numberOfLines != 0 {
|
||||
let maxHeight = singleLineHeightEstimate * numberOfLines
|
||||
if height > maxHeight {
|
||||
height = maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
cache[string]![width] = height
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
static func calculateHeight(_ string: String, _ width: Int, _ font: UIFont) -> Int {
|
||||
let height = string.height(withConstrainedWidth: CGFloat(width), font: font)
|
||||
return Int(ceil(height))
|
||||
}
|
||||
|
||||
func numberOfLines(for height: Int) -> Int {
|
||||
|
||||
// We’ll have to see if this really works reliably.
|
||||
|
||||
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
|
||||
let lines = Int(round(CGFloat(height) / averageHeight))
|
||||
return lines
|
||||
|
||||
}
|
||||
|
||||
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
|
||||
return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate)
|
||||
}
|
||||
|
||||
func heightIsProbablyDoubleLineHeight(_ height: Int) -> Bool {
|
||||
return heightIsProbablyEqualToEstimate(height, doubleLineHeightEstimate)
|
||||
}
|
||||
|
||||
func heightIsProbablyEqualToEstimate(_ height: Int, _ estimate: Int) -> Bool {
|
||||
|
||||
let slop = 4
|
||||
let minimum = estimate - slop
|
||||
let maximum = estimate + slop
|
||||
return height >= minimum && height <= maximum
|
||||
|
||||
}
|
||||
|
||||
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
|
||||
|
||||
// Given width, if the height at width - something and width + something is equal,
|
||||
// then that height must be correct for the given width.
|
||||
// Also:
|
||||
// If a narrower neighbor’s height is single line height, then this wider width must also be single-line height.
|
||||
// If a wider neighbor’s height is double line height, and numberOfLines == 2, then this narrower width must able be double-line height.
|
||||
|
||||
var smallNeighbor = (width: 0, height: 0)
|
||||
var largeNeighbor = (width: 0, height: 0)
|
||||
|
||||
for (oneWidth, oneHeight) in heightCache {
|
||||
|
||||
if oneWidth < width && heightIsProbablySingleLineHeight(oneHeight) {
|
||||
return oneHeight
|
||||
}
|
||||
if numberOfLines == 2 && oneWidth > width && heightIsProbablyDoubleLineHeight(oneHeight) {
|
||||
return oneHeight
|
||||
}
|
||||
|
||||
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
|
||||
smallNeighbor = (oneWidth, oneHeight)
|
||||
} else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
|
||||
largeNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
|
||||
if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height {
|
||||
return smallNeighbor.height
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
64
iOS/MainWindow/Timeline/Cell/SingleLineUILabelSizer.swift
Normal file
64
iOS/MainWindow/Timeline/Cell/SingleLineUILabelSizer.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// SingleLineUILabelSizer.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/19/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// Get the size of an UILabel configured with a specific font with a specific size.
|
||||
// Uses a cache.
|
||||
// Main thready only.
|
||||
|
||||
final class SingleLineUILabelSizer {
|
||||
|
||||
let font: UIFont
|
||||
private var cache = [String: CGSize]()
|
||||
|
||||
init(font: UIFont) {
|
||||
self.font = font
|
||||
}
|
||||
|
||||
func size(for text: String) -> CGSize {
|
||||
|
||||
if let cachedSize = cache[text] {
|
||||
return cachedSize
|
||||
}
|
||||
|
||||
let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font)
|
||||
let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font)
|
||||
let calculatedSize = CGSize(width: ceil(width), height: ceil(height))
|
||||
|
||||
cache[text] = calculatedSize
|
||||
return calculatedSize
|
||||
|
||||
}
|
||||
|
||||
static private var sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
|
||||
static func sizer(for font: UIFont) -> SingleLineUILabelSizer {
|
||||
|
||||
if let cachedSizer = sizers[font] {
|
||||
return cachedSizer
|
||||
}
|
||||
|
||||
let newSizer = SingleLineUILabelSizer(font: font)
|
||||
sizers[font] = newSizer
|
||||
|
||||
return newSizer
|
||||
|
||||
}
|
||||
|
||||
// Use this call. It’s easiest.
|
||||
|
||||
static func size(for text: String, font: UIFont) -> CGSize {
|
||||
return sizer(for: font).size(for: text)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
}
|
||||
|
||||
}
|
||||
16
iOS/MainWindow/Timeline/MainTimelineDataSource.swift
Normal file
16
iOS/MainWindow/Timeline/MainTimelineDataSource.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// MainTimelineDataSource.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 8/30/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class MainTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
56
iOS/MainWindow/Timeline/MainTimelineTitleView.swift
Normal file
56
iOS/MainWindow/Timeline/MainTimelineTitleView.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// MainTimelineTitleView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 9/21/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class MainTimelineTitleView: UIView {
|
||||
|
||||
@IBOutlet weak var iconView: IconView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
@IBOutlet weak var unreadCountView: MainTimelineUnreadCountView!
|
||||
|
||||
private lazy var pointerInteraction: UIPointerInteraction = {
|
||||
UIPointerInteraction(delegate: self)
|
||||
}()
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
if let name = label.text {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)"
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
func buttonize() {
|
||||
heightAnchor.constraint(equalToConstant: 40.0).isActive = true
|
||||
accessibilityTraits = .button
|
||||
addInteraction(pointerInteraction)
|
||||
}
|
||||
|
||||
func debuttonize() {
|
||||
heightAnchor.constraint(equalToConstant: 40.0).isActive = true
|
||||
accessibilityTraits.remove(.button)
|
||||
removeInteraction(pointerInteraction)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTimelineTitleView: UIPointerInteractionDelegate {
|
||||
|
||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||
var rect = self.frame
|
||||
rect.origin.x -= 10
|
||||
rect.size.width += 20
|
||||
|
||||
return UIPointerStyle(effect: .automatic(UITargetedPreview(view: self)), shape: .roundedRect(rect))
|
||||
}
|
||||
}
|
||||
57
iOS/MainWindow/Timeline/MainTimelineTitleView.xib
Normal file
57
iOS/MainWindow/Timeline/MainTimelineTitleView.xib
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="MainTimelineTitleView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="190" height="38"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5F6-2v-qSS">
|
||||
<rect key="frame" x="28" y="9" width="43.5" height="20"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="z9o-XA-3t4" customClass="MainTimelineUnreadCountView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="79.5" y="9" width="110.5" height="20"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5gI-Wl-lnK" customClass="IconView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="9" width="20" height="20"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="20" id="TgA-1l-WQJ"/>
|
||||
<constraint firstAttribute="height" constant="20" id="VUB-ip-zXU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="5gI-Wl-lnK" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="1Yh-ha-Pu8"/>
|
||||
<constraint firstItem="5F6-2v-qSS" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="CeZ-D5-NOV"/>
|
||||
<constraint firstItem="z9o-XA-3t4" firstAttribute="leading" secondItem="5F6-2v-qSS" secondAttribute="trailing" constant="8" symbolic="YES" id="CiV-5P-T1S"/>
|
||||
<constraint firstAttribute="trailing" secondItem="z9o-XA-3t4" secondAttribute="trailing" id="OVL-Ac-Rtt"/>
|
||||
<constraint firstItem="z9o-XA-3t4" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ZkY-jG-eZO"/>
|
||||
<constraint firstItem="5gI-Wl-lnK" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="eQU-mX-qmd"/>
|
||||
<constraint firstItem="5F6-2v-qSS" firstAttribute="leading" secondItem="5gI-Wl-lnK" secondAttribute="trailing" constant="8" id="fVr-vW-alg"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="iconView" destination="5gI-Wl-lnK" id="IiR-qS-d22"/>
|
||||
<outlet property="label" destination="5F6-2v-qSS" id="ec7-8Y-PRv"/>
|
||||
<outlet property="unreadCountView" destination="z9o-XA-3t4" id="JBy-aa-feL"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="14.492753623188406" y="-224.33035714285714"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
39
iOS/MainWindow/Timeline/MainTimelineUnreadCountView.swift
Normal file
39
iOS/MainWindow/Timeline/MainTimelineUnreadCountView.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// MainTimelineUnreadCountView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 9/30/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class MainTimelineUnreadCountView: MainFeedUnreadCountView {
|
||||
|
||||
override var padding: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 2.0, left: 9.0, bottom: 2.0, right: 9.0)
|
||||
}
|
||||
|
||||
override var textColor: UIColor {
|
||||
return UIColor.systemBackground
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return contentSize
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: CGRect) {
|
||||
|
||||
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
|
||||
let rect = CGRect(x: 1, y: 1, width: bounds.width - 2, height: bounds.height - 2)
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: cornerRadii)
|
||||
AppColor.accent.setFill()
|
||||
path.fill()
|
||||
|
||||
if unreadCount > 0 {
|
||||
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
85
iOS/MainWindow/Timeline/MarkAsReadAlertController.swift
Normal file
85
iOS/MainWindow/Timeline/MarkAsReadAlertController.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// UndoAvailableAlertController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Phil Viso on 9/29/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol MarkAsReadAlertControllerSourceType {}
|
||||
extension CGRect: MarkAsReadAlertControllerSourceType {}
|
||||
extension UIView: MarkAsReadAlertControllerSourceType {}
|
||||
extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {}
|
||||
|
||||
struct MarkAsReadAlertController {
|
||||
|
||||
static func confirm<T>(
|
||||
_ controller: UIViewController?,
|
||||
coordinator: SceneCoordinator?,
|
||||
confirmTitle: String,
|
||||
sourceType: T,
|
||||
cancelCompletion: (() -> Void)? = nil,
|
||||
completion: @escaping () -> Void
|
||||
) where T: MarkAsReadAlertControllerSourceType {
|
||||
|
||||
guard let controller = controller, let coordinator = coordinator else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
if AppDefaults.confirmMarkAllAsRead {
|
||||
let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in
|
||||
completion()
|
||||
}
|
||||
controller.present(alertController, animated: true)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private static func alert<T>(
|
||||
coordinator: SceneCoordinator,
|
||||
confirmTitle: String,
|
||||
cancelCompletion: (() -> Void)?,
|
||||
sourceType: T,
|
||||
completion: @escaping (UIAlertAction) -> Void
|
||||
) -> UIAlertController where T: MarkAsReadAlertControllerSourceType {
|
||||
|
||||
let title = NSLocalizedString("Mark As Read", comment: "Mark As Read")
|
||||
let message = NSLocalizedString("You can turn this confirmation off in Settings.",
|
||||
comment: "You can turn this confirmation off in Settings.")
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings")
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
|
||||
cancelCompletion?()
|
||||
}
|
||||
let settingsAction = UIAlertAction(title: settingsTitle, style: .default) { _ in
|
||||
coordinator.showSettings(scrollToArticlesSection: true)
|
||||
}
|
||||
let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion)
|
||||
|
||||
alertController.addAction(markAction)
|
||||
alertController.addAction(settingsAction)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
if let barButtonItem = sourceType as? UIBarButtonItem {
|
||||
alertController.popoverPresentationController?.barButtonItem = barButtonItem
|
||||
}
|
||||
|
||||
if let rect = sourceType as? CGRect {
|
||||
alertController.popoverPresentationController?.sourceRect = rect
|
||||
}
|
||||
|
||||
if let view = sourceType as? UIView {
|
||||
alertController.popoverPresentationController?.sourceView = view
|
||||
}
|
||||
|
||||
return alertController
|
||||
}
|
||||
|
||||
}
|
||||
1024
iOS/MainWindow/Timeline/TimelineViewController.swift
Normal file
1024
iOS/MainWindow/Timeline/TimelineViewController.swift
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user