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:
23
iOS/MainWindow/Sidebar/Cell/MainFeedRowIdentifier.swift
Normal file
23
iOS/MainWindow/Sidebar/Cell/MainFeedRowIdentifier.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// MainFeedRowIdentifier.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 10/20/21.
|
||||
// Copyright © 2021 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class MainFeedRowIdentifier: NSObject, NSCopying {
|
||||
|
||||
var indexPath: IndexPath
|
||||
|
||||
init(indexPath: IndexPath) {
|
||||
self.indexPath = indexPath
|
||||
}
|
||||
|
||||
func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
243
iOS/MainWindow/Sidebar/Cell/MainFeedTableViewCell.swift
Normal file
243
iOS/MainWindow/Sidebar/Cell/MainFeedTableViewCell.swift
Normal file
@@ -0,0 +1,243 @@
|
||||
//
|
||||
// MainFeedTableViewCell.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 8/1/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
import Account
|
||||
import RSTree
|
||||
|
||||
protocol MainFeedTableViewCellDelegate: AnyObject {
|
||||
func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool)
|
||||
}
|
||||
|
||||
final class MainFeedTableViewCell: VibrantTableViewCell {
|
||||
|
||||
weak var delegate: MainFeedTableViewCellDelegate?
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
if unreadCount > 0 {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCount) \(unreadLabel)"
|
||||
} else {
|
||||
return name
|
||||
}
|
||||
}
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
var iconImage: IconImage? {
|
||||
didSet {
|
||||
iconView.iconImage = iconImage
|
||||
}
|
||||
}
|
||||
|
||||
var isDisclosureAvailable = false {
|
||||
didSet {
|
||||
if isDisclosureAvailable != oldValue {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isSeparatorShown = true {
|
||||
didSet {
|
||||
if isSeparatorShown != oldValue {
|
||||
if isSeparatorShown {
|
||||
showView(bottomSeparatorView)
|
||||
} else {
|
||||
hideView(bottomSeparatorView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unreadCount: Int {
|
||||
get {
|
||||
return unreadCountView.unreadCount
|
||||
}
|
||||
set {
|
||||
if unreadCountView.unreadCount != newValue {
|
||||
unreadCountView.unreadCount = newValue
|
||||
unreadCountView.isHidden = (newValue < 1)
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name: String {
|
||||
get {
|
||||
return titleView.text ?? ""
|
||||
}
|
||||
set {
|
||||
if titleView.text != newValue {
|
||||
titleView.text = newValue
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleView: UILabel = {
|
||||
let label = NonIntrinsicLabel()
|
||||
label.numberOfLines = 0
|
||||
label.allowsDefaultTighteningForTruncation = false
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
return label
|
||||
}()
|
||||
|
||||
private let iconView = IconView()
|
||||
|
||||
private let bottomSeparatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.separator
|
||||
view.alpha = 0.5
|
||||
return view
|
||||
}()
|
||||
|
||||
private var isDisclosureExpanded = false
|
||||
private var disclosureButton: UIButton?
|
||||
private var unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
|
||||
private var isShowingEditControl = false
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
func setDisclosure(isExpanded: Bool, animated: Bool) {
|
||||
isDisclosureExpanded = isExpanded
|
||||
let duration = animated ? 0.3 : 0.0
|
||||
|
||||
UIView.animate(withDuration: duration) {
|
||||
if self.isDisclosureExpanded {
|
||||
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder")
|
||||
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796)
|
||||
} else {
|
||||
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
|
||||
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func willTransition(to state: UITableViewCell.StateMask) {
|
||||
super.willTransition(to: state)
|
||||
isShowingEditControl = state.contains(.showingEditControl)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
let layout = MainFeedTableViewCellLayout(
|
||||
cellWidth: bounds.size.width,
|
||||
insets: safeAreaInsets,
|
||||
label: titleView,
|
||||
unreadCountView: unreadCountView,
|
||||
showingEditingControl: isShowingEditControl,
|
||||
indent: indentationLevel == 1,
|
||||
shouldShowDisclosure: isDisclosureAvailable
|
||||
)
|
||||
return CGSize(width: bounds.width, height: layout.height)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let layout = MainFeedTableViewCellLayout(
|
||||
cellWidth: bounds.size.width,
|
||||
insets: safeAreaInsets,
|
||||
label: titleView,
|
||||
unreadCountView: unreadCountView,
|
||||
showingEditingControl: isShowingEditControl,
|
||||
indent: indentationLevel == 1,
|
||||
shouldShowDisclosure: isDisclosureAvailable
|
||||
)
|
||||
layoutWith(layout)
|
||||
}
|
||||
|
||||
@objc func buttonPressed(_ sender: UIButton) {
|
||||
if isDisclosureAvailable {
|
||||
setDisclosure(isExpanded: !isDisclosureExpanded, animated: true)
|
||||
delegate?.mainFeedTableViewCellDisclosureDidToggle(self, expanding: isDisclosureExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
|
||||
let iconTintColor: UIColor
|
||||
if isHighlighted || isSelected {
|
||||
iconTintColor = AppColor.vibrantText
|
||||
} else {
|
||||
if let preferredColor = iconImage?.preferredColor {
|
||||
iconTintColor = UIColor(cgColor: preferredColor)
|
||||
} else {
|
||||
iconTintColor = AppColor.secondaryAccent
|
||||
}
|
||||
}
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
self.iconView.tintColor = iconTintColor
|
||||
}
|
||||
} else {
|
||||
self.iconView.tintColor = iconTintColor
|
||||
}
|
||||
|
||||
updateLabelVibrancy(titleView, color: labelColor, animated: animated)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension MainFeedTableViewCell {
|
||||
|
||||
func commonInit() {
|
||||
addSubviewAtInit(unreadCountView)
|
||||
addSubviewAtInit(iconView)
|
||||
addSubviewAtInit(titleView)
|
||||
addDisclosureView()
|
||||
addSubviewAtInit(bottomSeparatorView)
|
||||
}
|
||||
|
||||
func addDisclosureView() {
|
||||
disclosureButton = NonIntrinsicButton(type: .roundedRect)
|
||||
disclosureButton!.addTarget(self, action: #selector(buttonPressed(_:)), for: UIControl.Event.touchUpInside)
|
||||
disclosureButton?.setImage(AppImage.disclosure, for: .normal)
|
||||
disclosureButton?.tintColor = AppColor.controlBackground
|
||||
disclosureButton?.imageView?.contentMode = .center
|
||||
disclosureButton?.imageView?.clipsToBounds = false
|
||||
disclosureButton?.addInteraction(UIPointerInteraction())
|
||||
addSubviewAtInit(disclosureButton!)
|
||||
}
|
||||
|
||||
func addSubviewAtInit(_ view: UIView) {
|
||||
addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
func layoutWith(_ layout: MainFeedTableViewCellLayout) {
|
||||
iconView.setFrameIfNotEqual(layout.faviconRect)
|
||||
titleView.setFrameIfNotEqual(layout.titleRect)
|
||||
unreadCountView.setFrameIfNotEqual(layout.unreadCountRect)
|
||||
disclosureButton?.setFrameIfNotEqual(layout.disclosureButtonRect)
|
||||
disclosureButton?.isHidden = !isDisclosureAvailable
|
||||
bottomSeparatorView.setFrameIfNotEqual(layout.separatorRect)
|
||||
}
|
||||
|
||||
func hideView(_ view: UIView) {
|
||||
if !view.isHidden {
|
||||
view.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func showView(_ view: UIView) {
|
||||
if view.isHidden {
|
||||
view.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
148
iOS/MainWindow/Sidebar/Cell/MainFeedTableViewCellLayout.swift
Normal file
148
iOS/MainWindow/Sidebar/Cell/MainFeedTableViewCellLayout.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// MainFeedTableViewCellLayout.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 11/24/17.
|
||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
struct MainFeedTableViewCellLayout {
|
||||
|
||||
private static let indentWidth = CGFloat(integerLiteral: 42)
|
||||
private static let editingControlIndent = CGFloat(integerLiteral: 40)
|
||||
private static let imageSize = CGSize(width: 24, height: 24)
|
||||
private static let imageMarginRight = CGFloat(integerLiteral: 11)
|
||||
private static let labelMarginRight = CGFloat(integerLiteral: 8)
|
||||
private static let unreadCountMarginRight = CGFloat(integerLiteral: 16)
|
||||
private static let disclosureButtonSize = CGSize(width: 44, height: 44)
|
||||
private static let verticalPadding = CGFloat(integerLiteral: 11)
|
||||
|
||||
private static let minRowHeight = CGFloat(integerLiteral: 44)
|
||||
|
||||
static let faviconCornerRadius = CGFloat(integerLiteral: 2)
|
||||
|
||||
let faviconRect: CGRect
|
||||
let titleRect: CGRect
|
||||
let unreadCountRect: CGRect
|
||||
let disclosureButtonRect: CGRect
|
||||
let separatorRect: CGRect
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView, showingEditingControl: Bool, indent: Bool, shouldShowDisclosure: Bool) {
|
||||
|
||||
var initialIndent = insets.left
|
||||
if indent {
|
||||
initialIndent += MainFeedTableViewCellLayout.indentWidth
|
||||
}
|
||||
let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0)
|
||||
|
||||
// Disclosure Button
|
||||
var rDisclosure = CGRect.zero
|
||||
if shouldShowDisclosure {
|
||||
rDisclosure.size = MainFeedTableViewCellLayout.disclosureButtonSize
|
||||
rDisclosure.origin.x = bounds.origin.x
|
||||
}
|
||||
|
||||
// Favicon
|
||||
var rFavicon = CGRect.zero
|
||||
if !shouldShowDisclosure {
|
||||
let x = bounds.origin.x
|
||||
let y = UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding) +
|
||||
label.font.lineHeight / 2.0 -
|
||||
MainFeedTableViewCellLayout.imageSize.height / 2.0
|
||||
rFavicon = CGRect(x: x, y: y, width: MainFeedTableViewCellLayout.imageSize.width, height: MainFeedTableViewCellLayout.imageSize.height)
|
||||
}
|
||||
|
||||
// Unread Count
|
||||
let unreadCountSize = unreadCountView.contentSize
|
||||
let unreadCountIsHidden = unreadCountView.unreadCount < 1
|
||||
|
||||
var rUnread = CGRect.zero
|
||||
if !unreadCountIsHidden {
|
||||
rUnread.size = unreadCountSize
|
||||
rUnread.origin.x = bounds.maxX - (MainFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width)
|
||||
}
|
||||
|
||||
// Title
|
||||
var rLabelx = insets.left + MainFeedTableViewCellLayout.disclosureButtonSize.width
|
||||
if !shouldShowDisclosure {
|
||||
rLabelx = rLabelx + MainFeedTableViewCellLayout.imageSize.width + MainFeedTableViewCellLayout.imageMarginRight
|
||||
}
|
||||
let rLabely = UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding)
|
||||
|
||||
var labelWidth = CGFloat.zero
|
||||
if !unreadCountIsHidden {
|
||||
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight + (cellWidth - rUnread.minX))
|
||||
} else {
|
||||
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight)
|
||||
}
|
||||
|
||||
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
|
||||
|
||||
// Now that we've got everything (especially the label) computed without the editing controls, update for them.
|
||||
// We do this because we don't want the row height to change when the editing controls are brought out. We will
|
||||
// handle the missing space, but removing it from the label and truncating.
|
||||
if showingEditingControl {
|
||||
rDisclosure.origin.x += MainFeedTableViewCellLayout.editingControlIndent
|
||||
rFavicon.origin.x += MainFeedTableViewCellLayout.editingControlIndent
|
||||
rLabelx += MainFeedTableViewCellLayout.editingControlIndent
|
||||
if !unreadCountIsHidden {
|
||||
rUnread.origin.x -= MainFeedTableViewCellLayout.editingControlIndent
|
||||
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight + (cellWidth - rUnread.minX))
|
||||
} else {
|
||||
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight + MainFeedTableViewCellLayout.editingControlIndent)
|
||||
}
|
||||
}
|
||||
|
||||
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
|
||||
|
||||
// Determine cell height
|
||||
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding)
|
||||
let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY()
|
||||
var cellHeight = max(paddedLabelHeight, maxGraphicsHeight)
|
||||
if cellHeight < MainFeedTableViewCellLayout.minRowHeight {
|
||||
cellHeight = MainFeedTableViewCellLayout.minRowHeight
|
||||
}
|
||||
|
||||
// Center in Cell
|
||||
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
|
||||
if !unreadCountIsHidden {
|
||||
rUnread = MainFeedTableViewCellLayout.centerVertically(rUnread, newBounds)
|
||||
}
|
||||
if shouldShowDisclosure {
|
||||
rDisclosure = MainFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds)
|
||||
}
|
||||
|
||||
// Small fonts and the Favicon need centered if we hit the minimum row height
|
||||
if cellHeight == MainFeedTableViewCellLayout.minRowHeight {
|
||||
rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
|
||||
rFavicon = MainFeedTableViewCellLayout.centerVertically(rFavicon, newBounds)
|
||||
}
|
||||
|
||||
// Separator Insets
|
||||
let separatorInset = MainFeedTableViewCellLayout.disclosureButtonSize.width
|
||||
separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5)
|
||||
|
||||
// Assign the properties
|
||||
self.height = cellHeight
|
||||
self.faviconRect = rFavicon
|
||||
self.unreadCountRect = rUnread
|
||||
self.disclosureButtonRect = rDisclosure
|
||||
self.titleRect = rLabel
|
||||
|
||||
}
|
||||
|
||||
// Ideally this will be implemented in RSCore (see RSGeometry)
|
||||
static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect {
|
||||
var result = originalRect
|
||||
result.origin.y = containerRect.midY - (result.height / 2.0)
|
||||
result = result.integral
|
||||
result.size = originalRect.size
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
209
iOS/MainWindow/Sidebar/Cell/MainFeedTableViewSectionHeader.swift
Normal file
209
iOS/MainWindow/Sidebar/Cell/MainFeedTableViewSectionHeader.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// MainFeedTableViewSectionHeader.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol MainFeedTableViewSectionHeaderDelegate: AnyObject {
|
||||
func mainFeedTableViewSectionHeaderDisclosureDidToggle(_ sender: MainFeedTableViewSectionHeader)
|
||||
}
|
||||
|
||||
final class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
|
||||
weak var delegate: MainFeedTableViewSectionHeaderDelegate?
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
if unreadCount > 0 {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCount) \(unreadLabel) \(expandedStateMessage) "
|
||||
} else {
|
||||
return "\(name) \(expandedStateMessage) "
|
||||
}
|
||||
}
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
private var expandedStateMessage: String {
|
||||
if disclosureExpanded {
|
||||
return NSLocalizedString("Expanded", comment: "Disclosure button expanded state for accessibility")
|
||||
}
|
||||
return NSLocalizedString("Collapsed", comment: "Disclosure button collapsed state for accessibility")
|
||||
}
|
||||
|
||||
var unreadCount: Int {
|
||||
get {
|
||||
return unreadCountView.unreadCount
|
||||
}
|
||||
set {
|
||||
if unreadCountView.unreadCount != newValue {
|
||||
unreadCountView.unreadCount = newValue
|
||||
updateUnreadCountView()
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name: String {
|
||||
get {
|
||||
return titleView.text ?? ""
|
||||
}
|
||||
set {
|
||||
if titleView.text != newValue {
|
||||
titleView.text = newValue
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var disclosureExpanded = false {
|
||||
didSet {
|
||||
updateExpandedState(animate: true)
|
||||
updateUnreadCountView()
|
||||
}
|
||||
}
|
||||
|
||||
var isLastSection = false
|
||||
|
||||
private let titleView: UILabel = {
|
||||
let label = NonIntrinsicLabel()
|
||||
label.numberOfLines = 0
|
||||
label.allowsDefaultTighteningForTruncation = false
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
return label
|
||||
}()
|
||||
|
||||
private let unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
|
||||
|
||||
private lazy var disclosureButton: UIButton = {
|
||||
let button = NonIntrinsicButton()
|
||||
button.tintColor = UIColor.tertiaryLabel
|
||||
button.setImage(AppImage.disclosure, for: .normal)
|
||||
button.contentMode = .center
|
||||
button.addInteraction(UIPointerInteraction())
|
||||
button.addTarget(self, action: #selector(toggleDisclosure), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private let topSeparatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.separator
|
||||
return view
|
||||
}()
|
||||
|
||||
private let bottomSeparatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.separator
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView)
|
||||
return CGSize(width: bounds.width, height: layout.height)
|
||||
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: contentView.bounds.size.width,
|
||||
insets: contentView.safeAreaInsets,
|
||||
label: titleView,
|
||||
unreadCountView: unreadCountView)
|
||||
layoutWith(layout)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension MainFeedTableViewSectionHeader {
|
||||
|
||||
@objc func toggleDisclosure() {
|
||||
delegate?.mainFeedTableViewSectionHeaderDisclosureDidToggle(self)
|
||||
}
|
||||
|
||||
func commonInit() {
|
||||
addSubviewAtInit(unreadCountView)
|
||||
addSubviewAtInit(titleView)
|
||||
addSubviewAtInit(disclosureButton)
|
||||
updateExpandedState(animate: false)
|
||||
addBackgroundView()
|
||||
addSubviewAtInit(topSeparatorView)
|
||||
addSubviewAtInit(bottomSeparatorView)
|
||||
}
|
||||
|
||||
func updateExpandedState(animate: Bool) {
|
||||
if !isLastSection && self.disclosureExpanded {
|
||||
self.bottomSeparatorView.isHidden = false
|
||||
}
|
||||
|
||||
let duration = animate ? 0.3 : 0.0
|
||||
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
animations: {
|
||||
if self.disclosureExpanded {
|
||||
self.disclosureButton.transform = CGAffineTransform(rotationAngle: 1.570796)
|
||||
} else {
|
||||
self.disclosureButton.transform = CGAffineTransform(rotationAngle: 0)
|
||||
}
|
||||
}, completion: { _ in
|
||||
if !self.isLastSection && !self.disclosureExpanded {
|
||||
self.bottomSeparatorView.isHidden = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func updateUnreadCountView() {
|
||||
if !disclosureExpanded && unreadCount > 0 {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.unreadCountView.alpha = 1
|
||||
}
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.unreadCountView.alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addSubviewAtInit(_ view: UIView) {
|
||||
contentView.addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
func layoutWith(_ layout: MainFeedTableViewSectionHeaderLayout) {
|
||||
titleView.setFrameIfNotEqual(layout.titleRect)
|
||||
unreadCountView.setFrameIfNotEqual(layout.unreadCountRect)
|
||||
disclosureButton.setFrameIfNotEqual(layout.disclosureButtonRect)
|
||||
|
||||
let x = -safeAreaInsets.left
|
||||
let width = safeAreaInsets.left + safeAreaInsets.right + frame.width
|
||||
let height = 0.33
|
||||
|
||||
let top = CGRect(x: x, y: 0, width: width, height: height)
|
||||
topSeparatorView.setFrameIfNotEqual(top)
|
||||
|
||||
let bottom = CGRect(x: x, y: frame.height - height, width: width, height: height)
|
||||
bottomSeparatorView.setFrameIfNotEqual(bottom)
|
||||
}
|
||||
|
||||
func addBackgroundView() {
|
||||
self.backgroundView = UIView(frame: self.bounds)
|
||||
self.backgroundView?.backgroundColor = AppColor.sectionHeader
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// MainFeedTableViewSectionHeaderLayout.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/5/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
struct MainFeedTableViewSectionHeaderLayout {
|
||||
|
||||
private static let labelMarginRight = CGFloat(integerLiteral: 8)
|
||||
private static let unreadCountMarginRight = CGFloat(integerLiteral: 16)
|
||||
private static let disclosureButtonSize = CGSize(width: 44, height: 44)
|
||||
private static let verticalPadding = CGFloat(integerLiteral: 11)
|
||||
|
||||
private static let minRowHeight = CGFloat(integerLiteral: 44)
|
||||
|
||||
let titleRect: CGRect
|
||||
let unreadCountRect: CGRect
|
||||
let disclosureButtonRect: CGRect
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView) {
|
||||
|
||||
let bounds = CGRect(x: insets.left, y: 0.0, width: floor(cellWidth - insets.right), height: 0.0)
|
||||
|
||||
// Disclosure Button
|
||||
var rDisclosure = CGRect.zero
|
||||
rDisclosure.size = MainFeedTableViewSectionHeaderLayout.disclosureButtonSize
|
||||
rDisclosure.origin.x = bounds.origin.x
|
||||
|
||||
// Unread Count
|
||||
let unreadCountSize = unreadCountView.contentSize
|
||||
let unreadCountIsHidden = unreadCountView.unreadCount < 1
|
||||
|
||||
var rUnread = CGRect.zero
|
||||
if !unreadCountIsHidden {
|
||||
rUnread.size = unreadCountSize
|
||||
rUnread.origin.x = bounds.maxX - (MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight + unreadCountSize.width)
|
||||
}
|
||||
|
||||
// Max Unread Count
|
||||
// We can't reload Section Headers so we don't let the title extend into the (probably) worse case Unread Count area.
|
||||
let maxUnreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
|
||||
maxUnreadCountView.unreadCount = 888
|
||||
let maxUnreadCountSize = maxUnreadCountView.contentSize
|
||||
|
||||
// Title
|
||||
let rLabelx = insets.left + MainFeedTableViewSectionHeaderLayout.disclosureButtonSize.width
|
||||
let rLabely = UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding)
|
||||
|
||||
var labelWidth = CGFloat.zero
|
||||
labelWidth = cellWidth - (rLabelx + MainFeedTableViewSectionHeaderLayout.labelMarginRight + maxUnreadCountSize.width + MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight)
|
||||
|
||||
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
|
||||
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
|
||||
|
||||
// Determine cell height
|
||||
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding)
|
||||
let maxGraphicsHeight = [rUnread, rDisclosure].maxY()
|
||||
var cellHeight = max(paddedLabelHeight, maxGraphicsHeight)
|
||||
if cellHeight < MainFeedTableViewSectionHeaderLayout.minRowHeight {
|
||||
cellHeight = MainFeedTableViewSectionHeaderLayout.minRowHeight
|
||||
}
|
||||
|
||||
// Center in Cell
|
||||
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
|
||||
if !unreadCountIsHidden {
|
||||
rUnread = MainFeedTableViewCellLayout.centerVertically(rUnread, newBounds)
|
||||
}
|
||||
rDisclosure = MainFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds)
|
||||
|
||||
// Small fonts need centered if we hit the minimum row height
|
||||
if cellHeight == MainFeedTableViewSectionHeaderLayout.minRowHeight {
|
||||
rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
|
||||
}
|
||||
|
||||
// Assign the properties
|
||||
self.height = cellHeight
|
||||
self.unreadCountRect = rUnread
|
||||
self.disclosureButtonRect = rDisclosure
|
||||
self.titleRect = rLabel
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
122
iOS/MainWindow/Sidebar/Cell/MainFeedUnreadCountView.swift
Normal file
122
iOS/MainWindow/Sidebar/Cell/MainFeedUnreadCountView.swift
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// MainFeedUnreadCountView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 11/22/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class MainFeedUnreadCountView: UIView {
|
||||
|
||||
var padding: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0)
|
||||
}
|
||||
|
||||
let cornerRadius = 8.0
|
||||
let bgColor = AppColor.controlBackground
|
||||
var textColor: UIColor {
|
||||
return UIColor.white
|
||||
}
|
||||
|
||||
var textAttributes: [NSAttributedString.Key: AnyObject] {
|
||||
let textFont = UIFont.preferredFont(forTextStyle: .caption1).bold()
|
||||
return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()]
|
||||
}
|
||||
var textSizeCache = [Int: CGSize]()
|
||||
|
||||
var unreadCount = 0 {
|
||||
didSet {
|
||||
contentSizeIsValid = false
|
||||
invalidateIntrinsicContentSize()
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
var unreadCountString: String {
|
||||
return unreadCount < 1 ? "" : "\(unreadCount)"
|
||||
}
|
||||
|
||||
private var contentSizeIsValid = false
|
||||
private var _contentSize = CGSize.zero
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
textSizeCache = [Int: CGSize]()
|
||||
contentSizeIsValid = false
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
var contentSize: CGSize {
|
||||
if !contentSizeIsValid {
|
||||
var size = CGSize.zero
|
||||
if unreadCount > 0 {
|
||||
size = textSize()
|
||||
size.width += (padding.left + padding.right)
|
||||
size.height += (padding.top + padding.bottom)
|
||||
}
|
||||
_contentSize = size
|
||||
contentSizeIsValid = true
|
||||
}
|
||||
return _contentSize
|
||||
}
|
||||
|
||||
// Prevent autolayout from messing around with our frame settings
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
||||
}
|
||||
|
||||
func textSize() -> CGSize {
|
||||
|
||||
if unreadCount < 1 {
|
||||
return CGSize.zero
|
||||
}
|
||||
|
||||
if let cachedSize = textSizeCache[unreadCount] {
|
||||
return cachedSize
|
||||
}
|
||||
|
||||
var size = unreadCountString.size(withAttributes: textAttributes)
|
||||
size.height = ceil(size.height)
|
||||
size.width = ceil(size.width)
|
||||
|
||||
textSizeCache[unreadCount] = size
|
||||
return size
|
||||
|
||||
}
|
||||
|
||||
func textRect() -> CGRect {
|
||||
|
||||
let size = textSize()
|
||||
var r = CGRect.zero
|
||||
r.size = size
|
||||
r.origin.x = (bounds.maxX - padding.right) - r.size.width
|
||||
r.origin.y = padding.top
|
||||
return r
|
||||
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: CGRect) {
|
||||
|
||||
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
|
||||
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: cornerRadii)
|
||||
bgColor.setFill()
|
||||
path.fill()
|
||||
|
||||
if unreadCount > 0 {
|
||||
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
36
iOS/MainWindow/Sidebar/MainFeedViewController+Drag.swift
Normal file
36
iOS/MainWindow/Sidebar/MainFeedViewController+Drag.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// MainFeedViewController+Drag.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/20/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
extension MainFeedViewController: UITableViewDragDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed else {
|
||||
return [UIDragItem]()
|
||||
}
|
||||
|
||||
let data = feed.url.data(using: .utf8)
|
||||
let itemProvider = NSItemProvider()
|
||||
|
||||
itemProvider.registerDataRepresentation(forTypeIdentifier: UTType.url.identifier, visibility: .ownProcess) { completion in
|
||||
Task { @MainActor in
|
||||
completion(data, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let dragItem = UIDragItem(itemProvider: itemProvider)
|
||||
dragItem.localObject = node
|
||||
return [dragItem]
|
||||
}
|
||||
|
||||
}
|
||||
165
iOS/MainWindow/Sidebar/MainFeedViewController+Drop.swift
Normal file
165
iOS/MainWindow/Sidebar/MainFeedViewController+Drop.swift
Normal file
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// MainFeedViewController+Drop.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 11/20/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
import Account
|
||||
import RSTree
|
||||
|
||||
extension MainFeedViewController: UITableViewDropDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
|
||||
return session.localDragSession != nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
|
||||
guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem,
|
||||
let destAccount = destFeed.account,
|
||||
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
// Validate account specific behaviors...
|
||||
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
||||
let sourceFeed = sourceNode.representedObject as? Feed,
|
||||
sourceFeed.account?.accountID != destAccount.accountID && destAccount.hasFeed(withURL: sourceFeed.url) {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
// Determine the correct drop proposal
|
||||
if destFeed is Folder {
|
||||
if session.location(in: destCell).y >= 0 {
|
||||
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .move, intent: .unspecified)
|
||||
}
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
|
||||
guard let dragItem = dropCoordinator.items.first?.dragItem,
|
||||
let dragNode = dragItem.localObject as? Node,
|
||||
let source = dragNode.parent?.representedObject as? Container,
|
||||
let destIndexPath = dropCoordinator.destinationIndexPath else {
|
||||
return
|
||||
}
|
||||
|
||||
let isFolderDrop: Bool = {
|
||||
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) {
|
||||
return dropCoordinator.session.location(in: propCell).y >= 0
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
// Based on the drop we have to determine a node to start looking for a parent container.
|
||||
let destNode: Node? = {
|
||||
|
||||
if isFolderDrop {
|
||||
return coordinator.nodeFor(destIndexPath)
|
||||
} else {
|
||||
if destIndexPath.row == 0 {
|
||||
return coordinator.nodeFor(IndexPath(row: 0, section: destIndexPath.section))
|
||||
} else if destIndexPath.row > 0 {
|
||||
return coordinator.nodeFor(IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
// Now we start looking for the parent container
|
||||
let destinationContainer: Container? = {
|
||||
if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) {
|
||||
return container
|
||||
} else {
|
||||
// If we got here, we are trying to drop on an empty section header. Go and find the Account for this section
|
||||
return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account
|
||||
}
|
||||
}()
|
||||
|
||||
guard let destination = destinationContainer, let feed = dragNode.representedObject as? Feed else { return }
|
||||
|
||||
if source.account == destination.account {
|
||||
moveFeedInAccount(feed: feed, sourceContainer: source, destinationContainer: destination)
|
||||
} else {
|
||||
moveFeedBetweenAccounts(feed: feed, sourceContainer: source, destinationContainer: destination)
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
guard sourceContainer !== destinationContainer else { return }
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
sourceContainer.account?.moveFeed(feed, from: sourceContainer, to: destinationContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
|
||||
if let existingFeed = destinationContainer.account?.existingFeed(withURL: feed.url) {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationContainer.account?.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceContainer.account?.removeFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationContainer.account?.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceContainer.account?.removeFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1295
iOS/MainWindow/Sidebar/MainFeedViewController.swift
Normal file
1295
iOS/MainWindow/Sidebar/MainFeedViewController.swift
Normal file
File diff suppressed because it is too large
Load Diff
135
iOS/MainWindow/Sidebar/RefreshProgressView.swift
Normal file
135
iOS/MainWindow/Sidebar/RefreshProgressView.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// RefeshProgressView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 10/24/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
final class RefreshProgressView: UIView {
|
||||
|
||||
@IBOutlet weak var progressView: UIProgressView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
update()
|
||||
scheduleUpdateRefreshLabel()
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = [.updatesFrequently, .notEnabled]
|
||||
}
|
||||
|
||||
func update() {
|
||||
if !AccountManager.shared.combinedRefreshProgress.isComplete {
|
||||
progressChanged(animated: false)
|
||||
} else {
|
||||
updateRefreshLabel()
|
||||
}
|
||||
}
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
progressChanged(animated: false)
|
||||
}
|
||||
|
||||
@objc func progressDidChange(_ note: Notification) {
|
||||
progressChanged(animated: true)
|
||||
}
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
// This hack is probably necessary because custom views in the toolbar don't get
|
||||
// notifications that the content size changed.
|
||||
label.font = UIFont.preferredFont(forTextStyle: .footnote)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension RefreshProgressView {
|
||||
|
||||
func progressChanged(animated: Bool) {
|
||||
// Layout may crash if not in the view hierarchy.
|
||||
// https://github.com/Ranchero-Software/NetNewsWire/issues/1764
|
||||
let isInViewHierarchy = self.superview != nil
|
||||
|
||||
let progress = AccountManager.shared.combinedRefreshProgress
|
||||
|
||||
if progress.isComplete {
|
||||
if isInViewHierarchy {
|
||||
progressView.setProgress(1, animated: animated)
|
||||
}
|
||||
|
||||
func completeLabel() {
|
||||
// Check that there are no pending downloads.
|
||||
if AccountManager.shared.combinedRefreshProgress.isComplete {
|
||||
self.updateRefreshLabel()
|
||||
self.label.isHidden = false
|
||||
self.progressView.isHidden = true
|
||||
if self.superview != nil {
|
||||
self.progressView.setProgress(0, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if animated {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
completeLabel()
|
||||
}
|
||||
} else {
|
||||
completeLabel()
|
||||
}
|
||||
} else {
|
||||
label.isHidden = true
|
||||
progressView.isHidden = false
|
||||
if isInViewHierarchy {
|
||||
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
|
||||
|
||||
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
||||
if percent > progressView.progress || progressView.progress - percent > 0.25 {
|
||||
progressView.setProgress(percent, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateRefreshLabel() {
|
||||
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
||||
|
||||
if Date() > accountLastArticleFetchEndTime.addingTimeInterval(60) {
|
||||
|
||||
let relativeDateTimeFormatter = RelativeDateTimeFormatter()
|
||||
relativeDateTimeFormatter.dateTimeStyle = .named
|
||||
let refreshed = relativeDateTimeFormatter.localizedString(for: accountLastArticleFetchEndTime, relativeTo: Date())
|
||||
let localizedRefreshText = NSLocalizedString("Updated %@", comment: "Updated")
|
||||
let refreshText = NSString.localizedStringWithFormat(localizedRefreshText as NSString, refreshed) as String
|
||||
label.text = refreshText
|
||||
|
||||
} else {
|
||||
label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
|
||||
}
|
||||
|
||||
} else {
|
||||
label.text = ""
|
||||
}
|
||||
|
||||
accessibilityLabel = label.text
|
||||
}
|
||||
|
||||
func scheduleUpdateRefreshLabel() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in
|
||||
self?.updateRefreshLabel()
|
||||
self?.scheduleUpdateRefreshLabel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
61
iOS/MainWindow/Sidebar/RefreshProgressView.xib
Normal file
61
iOS/MainWindow/Sidebar/RefreshProgressView.xib
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.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="ejl-zC-eNy" customClass="RefreshProgressView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="461" height="90"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Ds3-59-ooT" customClass="RoundedProgressView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="180.5" y="42.5" width="100" height="5"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="100" id="ReS-sT-7EN"/>
|
||||
<constraint firstAttribute="height" constant="5" id="oDX-bb-24H"/>
|
||||
</constraints>
|
||||
</progressView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7mJ-VZ-zqU">
|
||||
<rect key="frame" x="214" y="34" width="33" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="sNo-8i-tO3"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Ds3-59-ooT" firstAttribute="centerX" secondItem="ejl-zC-eNy" secondAttribute="centerX" id="5Rv-6l-HSL"/>
|
||||
<constraint firstItem="Ds3-59-ooT" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ejl-zC-eNy" secondAttribute="leading" id="Bck-uf-0G7"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="bottom" secondItem="sNo-8i-tO3" secondAttribute="bottom" id="DVn-hI-PhH"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="sNo-8i-tO3" secondAttribute="leading" id="Sbp-yf-ts9"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="centerY" secondItem="ejl-zC-eNy" secondAttribute="centerY" id="Shb-X2-Fwc"/>
|
||||
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="centerX" secondItem="ejl-zC-eNy" secondAttribute="centerX" id="lFg-fm-YmV"/>
|
||||
<constraint firstItem="sNo-8i-tO3" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="7mJ-VZ-zqU" secondAttribute="trailing" id="mZ2-XG-Kvg"/>
|
||||
<constraint firstItem="Ds3-59-ooT" firstAttribute="centerY" secondItem="ejl-zC-eNy" secondAttribute="centerY" id="tIh-lb-KbY"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Ds3-59-ooT" secondAttribute="trailing" id="vSU-N6-Sk5"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="label" destination="7mJ-VZ-zqU" id="MHr-r4-qop"/>
|
||||
<outlet property="progressView" destination="Ds3-59-ooT" id="TjM-db-LxM"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-75" y="-117"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
68
iOS/MainWindow/Sidebar/ShadowTableChanges.swift
Normal file
68
iOS/MainWindow/Sidebar/ShadowTableChanges.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// ShadowTableChanges.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 10/20/21.
|
||||
// Copyright © 2021 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ShadowTableChanges {
|
||||
|
||||
struct Move: Hashable {
|
||||
var from: Int
|
||||
var to: Int
|
||||
|
||||
init(_ from: Int, _ to: Int) {
|
||||
self.from = from
|
||||
self.to = to
|
||||
}
|
||||
}
|
||||
|
||||
struct RowChanges {
|
||||
|
||||
var section: Int
|
||||
var deletes: Set<Int>?
|
||||
var inserts: Set<Int>?
|
||||
var reloads: Set<Int>?
|
||||
var moves: Set<ShadowTableChanges.Move>?
|
||||
|
||||
var isEmpty: Bool {
|
||||
return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
var deleteIndexPaths: [IndexPath]? {
|
||||
guard let deletes = deletes else { return nil }
|
||||
return deletes.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
var insertIndexPaths: [IndexPath]? {
|
||||
guard let inserts = inserts else { return nil }
|
||||
return inserts.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
var reloadIndexPaths: [IndexPath]? {
|
||||
guard let reloads = reloads else { return nil }
|
||||
return reloads.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
var moveIndexPaths: [(IndexPath, IndexPath)]? {
|
||||
guard let moves = moves else { return nil }
|
||||
return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) }
|
||||
}
|
||||
|
||||
init(section: Int, deletes: Set<Int>?, inserts: Set<Int>?, reloads: Set<Int>?, moves: Set<Move>?) {
|
||||
self.section = section
|
||||
self.deletes = deletes
|
||||
self.inserts = inserts
|
||||
self.reloads = reloads
|
||||
self.moves = moves
|
||||
}
|
||||
}
|
||||
|
||||
var deletes: Set<Int>?
|
||||
var inserts: Set<Int>?
|
||||
var moves: Set<Move>?
|
||||
var rowChanges: [RowChanges]?
|
||||
}
|
||||
Reference in New Issue
Block a user