mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'main' into bsc-662-catch-up
This commit is contained in:
@@ -128,14 +128,17 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
|
||||
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?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
|
||||
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func applyThemeProperties() {
|
||||
super.applyThemeProperties()
|
||||
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||
backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell().updated(for: state)
|
||||
if state.isSelected {
|
||||
backgroundConfiguration?.backgroundColor = AppAssets.secondaryAccentColor
|
||||
}
|
||||
}
|
||||
|
||||
override func willTransition(to state: UITableViewCell.StateMask) {
|
||||
@@ -171,8 +174,10 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
|
||||
|
||||
let iconTintColor: UIColor
|
||||
if isHighlighted || isSelected {
|
||||
disclosureButton?.tintColor = AppAssets.vibrantTextColor
|
||||
iconTintColor = AppAssets.vibrantTextColor
|
||||
} else {
|
||||
disclosureButton?.tintColor = AppAssets.secondaryAccentColor
|
||||
if let preferredColor = iconImage?.preferredColor {
|
||||
iconTintColor = UIColor(cgColor: preferredColor)
|
||||
} else {
|
||||
|
||||
@@ -18,33 +18,63 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
|
||||
guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else {
|
||||
guard tableView.hasActiveDrag else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
guard let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
||||
let sourceWebFeed = sourceNode.representedObject as? WebFeed else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed,
|
||||
let destAccount = destFeed.account,
|
||||
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
var successOperation = UIDropOperation.move
|
||||
|
||||
if let destinationIndexPath = destinationIndexPath,
|
||||
let sourceIndexPath = coordinator.indexPathFor(sourceNode),
|
||||
destinationIndexPath.section != sourceIndexPath.section {
|
||||
successOperation = .copy
|
||||
}
|
||||
|
||||
guard let correctedIndexPath = correctDestinationIndexPath(session: session) else {
|
||||
// We didn't hit the corrected indexPath, but this at least it gets the section right
|
||||
guard let section = destinationIndexPath?.section,
|
||||
let account = coordinator.nodeFor(section)?.representedObject as? Account,
|
||||
!account.hasChildWebFeed(withURL: sourceWebFeed.url) else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
|
||||
guard correctedIndexPath.section > 0 else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
guard let correctDestNode = coordinator.nodeFor(correctedIndexPath),
|
||||
let correctDestFeed = correctDestNode.representedObject as? Feed,
|
||||
let correctDestAccount = correctDestFeed.account else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
// Validate account specific behaviors...
|
||||
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
||||
let sourceWebFeed = sourceNode.representedObject as? WebFeed,
|
||||
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
|
||||
if correctDestAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||
sourceWebFeed.account?.accountID != correctDestAccount.accountID && correctDestAccount.hasWebFeed(withURL: sourceWebFeed.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)
|
||||
if let correctFolder = correctDestFeed as? Folder {
|
||||
if correctFolder.hasChildWebFeed(withURL: sourceWebFeed.url) {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .move, intent: .unspecified)
|
||||
return UITableViewDropProposal(operation: successOperation, intent: .insertIntoDestinationIndexPath)
|
||||
}
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
||||
if let parentContainer = correctDestNode.parent?.representedObject as? Container, !parentContainer.hasChildWebFeed(withURL: sourceWebFeed.url) {
|
||||
return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath)
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -52,33 +82,23 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
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
|
||||
}()
|
||||
let source = dragNode.parent?.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
// Based on the drop we have to determine a node to start looking for a parent container.
|
||||
let destNode: Node? = {
|
||||
guard let destIndexPath = correctDestinationIndexPath(session: dropCoordinator.session) else { return nil }
|
||||
|
||||
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))
|
||||
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder {
|
||||
if dropCoordinator.proposal.intent == .insertAtDestinationIndexPath {
|
||||
return coordinator.nodeFor(destIndexPath.section)
|
||||
} else {
|
||||
return nil
|
||||
return coordinator.nodeFor(destIndexPath)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
// Now we start looking for the parent container
|
||||
@@ -86,8 +106,11 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) {
|
||||
return container
|
||||
} else {
|
||||
// We didn't hit the corrected indexPath, but this at least gets the section right
|
||||
guard let section = dropCoordinator.destinationIndexPath?.section else { return nil }
|
||||
|
||||
// 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
|
||||
return coordinator.nodeFor(section)?.representedObject as? Account
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -96,10 +119,25 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
if source.account == destination.account {
|
||||
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
} else {
|
||||
moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
copyWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension MasterFeedViewController {
|
||||
|
||||
func correctDestinationIndexPath(session: UIDropSession) -> IndexPath? {
|
||||
let location = session.location(in: tableView)
|
||||
|
||||
var correctDestination: IndexPath?
|
||||
tableView.performUsingPresentationValues {
|
||||
correctDestination = tableView.indexPathForRow(at: location)
|
||||
}
|
||||
|
||||
return correctDestination
|
||||
}
|
||||
|
||||
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
|
||||
guard sourceContainer !== destinationContainer else { return }
|
||||
|
||||
@@ -115,7 +153,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
|
||||
func copyWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
|
||||
|
||||
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
|
||||
|
||||
@@ -123,15 +161,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
BatchUpdate.shared.end()
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
self.presentError(error)
|
||||
@@ -144,15 +174,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
BatchUpdate.shared.end()
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
self.presentError(error)
|
||||
@@ -164,3 +186,11 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension Container {
|
||||
|
||||
func hasChildWebFeed(withURL url: String) -> Bool {
|
||||
return topLevelWebFeeds.contains(where: { $0.url == url })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Account
|
||||
import Articles
|
||||
import RSCore
|
||||
@@ -16,13 +17,15 @@ import SafariServices
|
||||
class MasterFeedViewController: UITableViewController, UndoableCommandRunner, MainControllerIdentifiable {
|
||||
|
||||
@IBOutlet weak var filterButton: UIBarButtonItem!
|
||||
private var refreshProgressView: RefreshProgressView?
|
||||
@IBOutlet weak var addNewItemButton: UIBarButtonItem! {
|
||||
didSet {
|
||||
addNewItemButton.primaryAction = nil
|
||||
}
|
||||
}
|
||||
|
||||
let refreshProgressModel = RefreshProgressModel()
|
||||
lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
|
||||
|
||||
var mainControllerIdentifer = MainControllerIdentifier.masterFeed
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
@@ -71,11 +74,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(configureContextMenu(_:)), name: .ActiveExtensionPointsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
||||
refreshControl!.tintColor = .clear
|
||||
|
||||
progressBarViewController.view.backgroundColor = .clear
|
||||
progressBarViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: progressBarViewController.view)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
|
||||
configureToolbar()
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
@@ -139,6 +148,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
}
|
||||
}
|
||||
|
||||
@objc func displayNameDidChange(_ note: Notification) {
|
||||
guard let object = note.object as? AnyObject else {
|
||||
return
|
||||
}
|
||||
reloadCell(for: object)
|
||||
}
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
resetEstimatedRowHeight()
|
||||
tableView.reloadData()
|
||||
@@ -515,7 +531,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
|
||||
func updateFeedSelection(animations: Animations) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath {
|
||||
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
|
||||
if indexPath != tableView.indexPathForSelectedRow {
|
||||
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
|
||||
}
|
||||
} else {
|
||||
if let indexPath = tableView.indexPathForSelectedRow {
|
||||
if animations.contains(.select) {
|
||||
@@ -587,7 +605,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
} else {
|
||||
setFilterButtonToInactive()
|
||||
}
|
||||
refreshProgressView?.update()
|
||||
refreshProgressModel.update()
|
||||
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
|
||||
|
||||
configureContextMenu()
|
||||
@@ -720,16 +738,6 @@ extension MasterFeedViewController: MasterFeedTableViewCellDelegate {
|
||||
|
||||
private extension MasterFeedViewController {
|
||||
|
||||
func configureToolbar() {
|
||||
guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshProgressView = refreshProgressView
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
}
|
||||
|
||||
func setFilterButtonToActive() {
|
||||
filterButton?.image = AppAssets.filterActiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
|
||||
@@ -831,6 +839,12 @@ private extension MasterFeedViewController {
|
||||
completion(cell as! MasterFeedTableViewCell, indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadCell(for object: AnyObject) {
|
||||
guard let indexPath = coordinator.indexPathFor(object) else { return }
|
||||
tableView.reloadRows(at: [indexPath], with: .none)
|
||||
restoreSelectionIfNecessary(adjustScroll: false)
|
||||
}
|
||||
|
||||
private func reloadAllVisibleCells(completion: (() -> Void)? = nil) {
|
||||
guard let indexPaths = tableView.indexPathsForVisibleRows else { return }
|
||||
@@ -1050,8 +1064,7 @@ private extension MasterFeedViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
@@ -1131,8 +1144,7 @@ private extension MasterFeedViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
||||
if let articles = try? feed.fetchUnreadArticles() {
|
||||
@@ -1233,8 +1245,7 @@ private extension MasterFeedViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
|
||||
@@ -1,27 +1,82 @@
|
||||
//
|
||||
// RefeshProgressView.swift
|
||||
// NetNewsWire-iOS
|
||||
// ProgressBarView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 10/24/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
// Created by Maurice Parker on 11/11/22.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
// IndetermineProgressView inspired by https://daringsnowball.net/articles/indeterminate-linear-progress-view/
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
class RefreshProgressView: UIView {
|
||||
struct RefreshProgressView: View {
|
||||
|
||||
@IBOutlet weak var progressView: UIProgressView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
static let width: CGFloat = 100
|
||||
static let height: CGFloat = 5
|
||||
|
||||
override func awakeFromNib() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
update()
|
||||
scheduleUpdateRefreshLabel()
|
||||
@ObservedObject var refreshProgressModel: RefreshProgressModel
|
||||
@State private var offset: CGFloat = 0
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = [.updatesFrequently, .notEnabled]
|
||||
init(progressBarMode: RefreshProgressModel) {
|
||||
self.refreshProgressModel = progressBarMode
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if refreshProgressModel.isRefreshing {
|
||||
if refreshProgressModel.isIndeterminate {
|
||||
indeterminateProgressView
|
||||
} else {
|
||||
ProgressView(value: refreshProgressModel.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.frame(width: Self.width, height: Self.height)
|
||||
}
|
||||
} else {
|
||||
Text(refreshProgressModel.label)
|
||||
.accessibilityLabel(refreshProgressModel.label)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 200, height: 44)
|
||||
}
|
||||
|
||||
var indeterminateProgressView: some View {
|
||||
Rectangle()
|
||||
.foregroundColor(.gray.opacity(0.15))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.foregroundColor(Color.accentColor)
|
||||
.frame(width: Self.width * 0.26, height: Self.height)
|
||||
.clipShape(Capsule())
|
||||
.offset(x: -Self.width * 0.6, y: 0)
|
||||
.offset(x: Self.width * 1.2 * self.offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.265), value: self.offset)
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
self.offset = 1
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
self.offset = 0
|
||||
}
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
.frame(width: Self.width, height: Self.height)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RefreshProgressModel: ObservableObject {
|
||||
|
||||
@Published var isRefreshing = false
|
||||
@Published var isIndeterminate = false
|
||||
@Published var progress = 0.0
|
||||
@Published var label = String()
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
}
|
||||
|
||||
func update() {
|
||||
@@ -31,52 +86,32 @@ class RefreshProgressView: UIView {
|
||||
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 {
|
||||
|
||||
private extension RefreshProgressModel {
|
||||
|
||||
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)
|
||||
}
|
||||
let combinedRefreshProgress = AccountManager.shared.combinedRefreshProgress
|
||||
isIndeterminate = combinedRefreshProgress.isIndeterminate
|
||||
|
||||
if combinedRefreshProgress.isComplete {
|
||||
isRefreshing = false
|
||||
progress = 1
|
||||
|
||||
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)
|
||||
}
|
||||
updateRefreshLabel()
|
||||
progress = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,19 +123,16 @@ private extension RefreshProgressView {
|
||||
completeLabel()
|
||||
}
|
||||
} else {
|
||||
label.isHidden = true
|
||||
progressView.isHidden = false
|
||||
if isInViewHierarchy {
|
||||
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
|
||||
isRefreshing = true
|
||||
let percent = Double(combinedRefreshProgress.numberCompleted) / Double(combinedRefreshProgress.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)
|
||||
}
|
||||
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
||||
if percent > progress || (progress - percent) > 0.25 {
|
||||
progress = percent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateRefreshLabel() {
|
||||
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
||||
|
||||
@@ -111,17 +143,15 @@ private extension RefreshProgressView {
|
||||
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
|
||||
label = refreshText
|
||||
|
||||
} else {
|
||||
label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
|
||||
label = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
|
||||
}
|
||||
|
||||
} else {
|
||||
label.text = ""
|
||||
label = ""
|
||||
}
|
||||
|
||||
accessibilityLabel = label.text
|
||||
}
|
||||
|
||||
func scheduleUpdateRefreshLabel() {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user