This commit is contained in:
Brent Simmons 2019-10-31 21:50:47 -07:00
commit e7337ed5ec
21 changed files with 422 additions and 187 deletions

View File

@ -14,7 +14,7 @@ extension RSImage {
static let avatarSize = 48
static func scaledForAvatar(_ data: Data, imageResultBlock: @escaping (RSImage?) -> Void) {
DispatchQueue.global().async {
DispatchQueue.global(qos: .default).async {
let image = RSImage.scaledForAvatar(data)
DispatchQueue.main.async {
imageResultBlock(image)

View File

@ -18,7 +18,9 @@ import AppKit
public class ColorHash {
public static let defaultLS = [CGFloat(0.45), CGFloat(0.6), CGFloat(0.75)]
public static let defaultSaturation = [CGFloat(0.35), CGFloat(0.5), CGFloat(0.65)]
public static let defaultBrightness = [CGFloat(0.5), CGFloat(0.65), CGFloat(0.80)]
let seed = CGFloat(131.0)
let seed2 = CGFloat(137.0)
let maxSafeInteger = 9007199254740991.0 / CGFloat(137.0)
@ -28,7 +30,7 @@ public class ColorHash {
public private(set) var brightness: [CGFloat]
public private(set) var saturation: [CGFloat]
public init(_ str: String, _ saturation: [CGFloat] = defaultLS, _ brightness: [CGFloat] = defaultLS) {
public init(_ str: String, _ saturation: [CGFloat] = defaultSaturation, _ brightness: [CGFloat] = defaultBrightness) {
self.str = str
self.saturation = saturation
self.brightness = brightness

View File

@ -18,11 +18,28 @@ extension Notification.Name {
final class FaviconDownloader {
private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0)
private let folder: String
private let diskCache: BinaryDiskCache
private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader
private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL
private var homePageURLsWithNoFaviconURL = Set<String>()
private var homePageToFaviconURLCachePath: String
private var homePageToFaviconURLCacheDirty = false {
didSet {
queueSaveHomePageToFaviconURLCacheIfNeeded()
}
}
private var homePageURLsWithNoFaviconURLCache = Set<String>()
private var homePageURLsWithNoFaviconURLCachePath: String
private var homePageURLsWithNoFaviconURLCacheDirty = false {
didSet {
queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded()
}
}
private let queue: DispatchQueue
private var cache = [Feed: RSImage]() // faviconURL: RSImage
@ -36,6 +53,11 @@ final class FaviconDownloader {
self.diskCache = BinaryDiskCache(folder: folder)
self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)")
self.homePageToFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToFaviconURLCache.plist")
self.homePageURLsWithNoFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist")
loadHomePageToFaviconURLCache()
loadHomePageURLsWithNoFaviconURLCache()
NotificationCenter.default.addObserver(self, selector: #selector(didLoadFavicon(_:)), name: .DidLoadFavicon, object: nil)
}
@ -92,7 +114,7 @@ final class FaviconDownloader {
func favicon(withHomePageURL homePageURL: String) -> RSImage? {
let url = homePageURL.rs_normalizedURL()
if homePageURLsWithNoFaviconURL.contains(url) {
if homePageURLsWithNoFaviconURLCache.contains(url) {
return nil
}
@ -103,10 +125,12 @@ final class FaviconDownloader {
findFaviconURL(with: url) { (faviconURL) in
if let faviconURL = faviconURL {
self.homePageToFaviconURLCache[url] = faviconURL
self.homePageToFaviconURLCacheDirty = true
let _ = self.favicon(with: faviconURL)
}
else {
self.homePageURLsWithNoFaviconURL.insert(url)
self.homePageURLsWithNoFaviconURLCache.insert(url)
self.homePageURLsWithNoFaviconURLCacheDirty = true
}
}
@ -126,6 +150,18 @@ final class FaviconDownloader {
postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL)
}
@objc func saveHomePageToFaviconURLCacheIfNeeded() {
if homePageToFaviconURLCacheDirty {
saveHomePageToFaviconURLCache()
}
}
@objc func saveHomePageURLsWithNoFaviconURLCacheIfNeeded() {
if homePageURLsWithNoFaviconURLCacheDirty {
saveHomePageURLsWithNoFaviconURLCache()
}
}
}
private extension FaviconDownloader {
@ -175,4 +211,60 @@ private extension FaviconDownloader {
NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo)
}
}
func loadHomePageToFaviconURLCache() {
let url = URL(fileURLWithPath: homePageToFaviconURLCachePath)
guard let data = try? Data(contentsOf: url) else {
return
}
let decoder = PropertyListDecoder()
homePageToFaviconURLCache = (try? decoder.decode([String: String].self, from: data)) ?? [String: String]()
}
func loadHomePageURLsWithNoFaviconURLCache() {
let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath)
guard let data = try? Data(contentsOf: url) else {
return
}
let decoder = PropertyListDecoder()
let decoded = (try? decoder.decode([String].self, from: data)) ?? [String]()
homePageURLsWithNoFaviconURLCache = Set(decoded)
}
func queueSaveHomePageToFaviconURLCacheIfNeeded() {
FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded))
}
func queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() {
FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded))
}
func saveHomePageToFaviconURLCache() {
homePageToFaviconURLCacheDirty = false
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let url = URL(fileURLWithPath: homePageToFaviconURLCachePath)
do {
let data = try encoder.encode(homePageToFaviconURLCache)
try data.write(to: url)
} catch {
assertionFailure(error.localizedDescription)
}
}
func saveHomePageURLsWithNoFaviconURLCache() {
homePageURLsWithNoFaviconURLCacheDirty = false
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath)
do {
let data = try encoder.encode(Array(homePageURLsWithNoFaviconURLCache))
try data.write(to: url)
} catch {
assertionFailure(error.localizedDescription)
}
}
}

View File

@ -32,7 +32,14 @@ public final class FeedIconDownloader {
}
}
private var homePagesWithNoIconURL = Set<String>()
private var homePagesWithNoIconURLCache = Set<String>()
private var homePagesWithNoIconURLCachePath: String
private var homePagesWithNoIconURLCacheDirty = false {
didSet {
queueHomePagesWithNoIconURLCacheIfNeeded()
}
}
private var urlsInProgress = Set<String>()
private var cache = [Feed: RSImage]()
private var waitingForFeedURLs = [String: Feed]()
@ -40,7 +47,9 @@ public final class FeedIconDownloader {
init(imageDownloader: ImageDownloader, folder: String) {
self.imageDownloader = imageDownloader
self.homePageToIconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToIconURLCache.plist")
self.homePagesWithNoIconURLCachePath = (folder as NSString).appendingPathComponent("HomePagesWithNoIconURLCache.plist")
loadHomePageToIconURLCache()
loadHomePagesWithNoIconURLCache()
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: imageDownloader)
}
@ -99,13 +108,19 @@ public final class FeedIconDownloader {
}
}
@objc func saveHomePagesWithNoIconURLCacheIfNeeded() {
if homePagesWithNoIconURLCacheDirty {
saveHomePagesWithNoIconURLCache()
}
}
}
private extension FeedIconDownloader {
func icon(forHomePageURL homePageURL: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
if homePagesWithNoIconURL.contains(homePageURL) {
if homePagesWithNoIconURLCache.contains(homePageURL) {
imageResultBlock(nil)
return
}
@ -141,7 +156,8 @@ private extension FeedIconDownloader {
}
func cacheIconURL(for homePageURL: String, _ iconURL: String) {
homePagesWithNoIconURL.remove(homePageURL)
homePagesWithNoIconURLCache.remove(homePageURL)
homePagesWithNoIconURLCacheDirty = true
homePageToIconURLCache[homePageURL] = iconURL
homePageToIconURLCacheDirty = true
}
@ -172,7 +188,8 @@ private extension FeedIconDownloader {
return
}
homePagesWithNoIconURL.insert(homePageURL)
homePagesWithNoIconURLCache.insert(homePageURL)
homePagesWithNoIconURLCacheDirty = true
}
func loadHomePageToIconURLCache() {
@ -184,10 +201,24 @@ private extension FeedIconDownloader {
homePageToIconURLCache = (try? decoder.decode([String: String].self, from: data)) ?? [String: String]()
}
func loadHomePagesWithNoIconURLCache() {
let url = URL(fileURLWithPath: homePagesWithNoIconURLCachePath)
guard let data = try? Data(contentsOf: url) else {
return
}
let decoder = PropertyListDecoder()
let decoded = (try? decoder.decode([String].self, from: data)) ?? [String]()
homePagesWithNoIconURLCache = Set(decoded)
}
func queueSaveHomePageToIconURLCacheIfNeeded() {
FeedIconDownloader.saveQueue.add(self, #selector(saveHomePageToIconURLCacheIfNeeded))
}
func queueHomePagesWithNoIconURLCacheIfNeeded() {
FeedIconDownloader.saveQueue.add(self, #selector(saveHomePagesWithNoIconURLCacheIfNeeded))
}
func saveHomePageToIconURLCache() {
homePageToIconURLCacheDirty = false
@ -202,4 +233,18 @@ private extension FeedIconDownloader {
}
}
func saveHomePagesWithNoIconURLCache() {
homePagesWithNoIconURLCacheDirty = false
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let url = URL(fileURLWithPath: homePagesWithNoIconURLCachePath)
do {
let data = try encoder.encode(Array(homePagesWithNoIconURLCache))
try data.write(to: url)
} catch {
assertionFailure(error.localizedDescription)
}
}
}

View File

@ -109,6 +109,7 @@ private extension AddContainerViewController {
return
}
navigationItem.title = NSLocalizedString("Add Feed", comment: "Add Feed")
resetUI()
hideCurrentController()
@ -126,6 +127,7 @@ private extension AddContainerViewController {
return
}
navigationItem.title = NSLocalizedString("Add Folder", comment: "Add Folder")
resetUI()
hideCurrentController()
displayContentController(UIStoryboard.add.instantiateController(ofType: AddFolderViewController.self))

View File

@ -44,7 +44,6 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
urlTextField.autocapitalizationType = .none
urlTextField.text = initialFeed
urlTextField.delegate = self
urlTextField.becomeFirstResponder()
if initialFeed != nil {
delegate?.readyToAdd(state: true)

View File

@ -31,7 +31,6 @@ class AddFolderViewController: UITableViewController, AddContainerViewController
accounts = AccountManager.shared.sortedActiveAccounts
nameTextField.delegate = self
nameTextField.becomeFirstResponder()
accountLabel.text = (accounts[0] as DisplayNameProvider).nameForDisplay

View File

@ -18,6 +18,7 @@ var appDelegate: AppDelegate!
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {
private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
var syncTimer: ArticleStatusSyncTimer?
@ -130,28 +131,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func prepareAccountsForBackground() {
syncTimer?.invalidate()
// Schedule background app refresh
scheduleBackgroundFeedRefresh()
// Sync article status
let completeProcessing = { [unowned self] in
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
}
DispatchQueue.global(qos: .background).async {
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
completeProcessing()
os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info)
}
DispatchQueue.main.async {
AccountManager.shared.syncArticleStatusAll() {
completeProcessing()
}
}
}
waitForProgressToFinish()
syncArticleStatus()
}
func prepareAccountsForForeground() {
@ -200,12 +182,15 @@ private extension AppDelegate {
let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
let imagesFolderURL = tempDir.appendingPathComponent("Images")
let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist")
let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist")
let homePageToFaviconURL = tempDir.appendingPathComponent("HomePageToFaviconURLCache.plist")
let homePageURLsWithNoFaviconURL = tempDir.appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist")
// If the image disk cache hasn't been flushed for 3 days and the network is available, delete it
if let flushDate = AppDefaults.lastImageCacheFlushDate, flushDate.addingTimeInterval(3600*24*3) < Date() {
if let reachability = try? Reachability(hostname: "apple.com") {
if reachability.connection != .unavailable {
for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL] {
for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL, homePageToFaviconURL, homePageURLsWithNoFaviconURL] {
do {
os_log(.info, log: self.log, "Removing cache file: %@", tempItem.absoluteString)
try FileManager.default.removeItem(at: tempItem)
@ -253,6 +238,68 @@ private extension AppDelegate {
}
// MARK: Go To Background
private extension AppDelegate {
func waitForProgressToFinish() {
let completeProcessing = { [unowned self] in
AccountManager.shared.saveAll()
UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask)
self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
}
self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
completeProcessing()
os_log("Accounts wait for progress terminated for running too long.", log: self.log, type: .info)
}
DispatchQueue.main.async { [weak self] in
self?.waitToComplete() {
completeProcessing()
}
}
}
func waitToComplete(completion: @escaping () -> Void) {
guard UIApplication.shared.applicationState != .active else {
os_log("App came back to forground, no longer waiting.", log: self.log, type: .info)
completion()
return
}
if AccountManager.shared.refreshInProgress {
os_log("Waiting for refresh progress to finish...", log: self.log, type: .info)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.waitToComplete() {
completion()
}
}
} else {
os_log("Refresh progress complete.", log: self.log, type: .info)
completion()
}
}
func syncArticleStatus() {
let completeProcessing = { [unowned self] in
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
}
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
completeProcessing()
os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info)
}
DispatchQueue.main.async {
AccountManager.shared.syncArticleStatusAll() {
completeProcessing()
}
}
}
}
// MARK: Background Tasks
private extension AppDelegate {

View File

@ -85,6 +85,9 @@ class ArticleViewController: UIViewController {
deinit {
if webView != nil {
webView?.evaluateJavaScript("cancelImageLoad();")
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked)
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown)
webView.removeFromSuperview()
ArticleViewControllerWebViewProvider.shared.enqueueWebView(webView)
webView = nil
@ -109,8 +112,6 @@ class ArticleViewController: UIViewController {
webView.navigationDelegate = self
webView.uiDelegate = self
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked)
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown)
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
@ -118,8 +119,6 @@ class ArticleViewController: UIViewController {
// to work around this bug: http://www.openradar.me/22855188
let url = Bundle.main.url(forResource: "page", withExtension: "html")!
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
// let request = URLRequest(url: url)
// webView.load(request)
}
@ -184,6 +183,7 @@ class ArticleViewController: UIViewController {
render = "render(\(json));"
}
webView?.scrollView.setZoomScale(1.0, animated: false)
webView?.evaluateJavaScript(render)
}

View File

@ -16,7 +16,7 @@ final class AvatarView: UIView {
imageView.image = image
if self.traitCollection.userInterfaceStyle == .dark {
DispatchQueue.global(qos: .background).async {
DispatchQueue.global(qos: .default).async {
if self.image?.isDark() ?? false {
DispatchQueue.main.async {
self.isDisconcernable = false
@ -74,7 +74,7 @@ final class AvatarView: UIView {
override func layoutSubviews() {
imageView.setFrameIfNotEqual(rectForImageView())
if (isVerticalBackgroundExposed && !isSymbolImage) || !isDisconcernable {
if (image != nil && isVerticalBackgroundExposed && !isSymbolImage) || !isDisconcernable {
backgroundColor = AppAssets.avatarBackgroundColor
} else {
backgroundColor = nil

View File

@ -69,7 +69,6 @@ struct FeedInspectorView : View {
.onDisappear { self.viewModel.save() }
.navigationBarTitle(Text(verbatim: self.viewModel.nameForDisplay), displayMode: .inline)
.navigationBarItems(leading: Button(action: {
self.viewModel.save()
self.viewController?.dismiss(animated: true)
}) { Text("Done") } )
}
@ -137,7 +136,8 @@ struct FeedInspectorView : View {
func save() {
if name != nameForDisplay {
feed.editedName = name.isEmpty ? nil : name
let newName = name.isEmpty ? (feed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : name
feed.rename(to: newName) { _ in }
}
}

View File

@ -87,6 +87,7 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
label.numberOfLines = 0
label.allowsDefaultTighteningForTruncation = false
label.adjustsFontForContentSizeCategory = true
label.lineBreakMode = .byTruncatingTail
label.font = .preferredFont(forTextStyle: .body)
return label
}()

View File

@ -37,10 +37,6 @@ struct MasterFeedTableViewCellLayout {
if indent {
initialIndent += MasterFeedTableViewCellLayout.disclosureButtonSize.width
}
if showingEditingControl {
initialIndent += MasterFeedTableViewCellLayout.editingControlIndent
}
let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0)
// Disclosure Button
@ -68,18 +64,12 @@ struct MasterFeedTableViewCellLayout {
if !unreadCountIsHidden {
rUnread.size = unreadCountSize
rUnread.origin.x = bounds.maxX - (MasterFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width)
if showingEditingControl {
rUnread.origin.x = rUnread.origin.x - MasterFeedTableViewCellLayout.editingControlIndent
}
}
// Title
var rLabelx = CGFloat.zero
if shouldShowDisclosure {
rLabelx = bounds.minX + MasterFeedTableViewCellLayout.disclosureButtonSize.width
} else {
rLabelx = rFavicon.maxX + MasterFeedTableViewCellLayout.imageMarginRight
var rLabelx = insets.left + MasterFeedTableViewCellLayout.disclosureButtonSize.width
if !shouldShowDisclosure {
rLabelx = rLabelx + MasterFeedTableViewCellLayout.imageSize.width + MasterFeedTableViewCellLayout.imageMarginRight
}
let rLabely = UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding)
@ -91,7 +81,23 @@ struct MasterFeedTableViewCellLayout {
}
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelSizeInfo.size.width, height: labelSizeInfo.size.height)
// 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 += MasterFeedTableViewCellLayout.editingControlIndent
rFavicon.origin.x += MasterFeedTableViewCellLayout.editingControlIndent
rLabelx += MasterFeedTableViewCellLayout.editingControlIndent
if !unreadCountIsHidden {
rUnread.origin.x -= MasterFeedTableViewCellLayout.editingControlIndent
labelWidth = cellWidth - (rLabelx + MasterFeedTableViewCellLayout.labelMarginRight + (cellWidth - rUnread.minX))
} else {
labelWidth = cellWidth - (rLabelx + MasterFeedTableViewCellLayout.labelMarginRight + MasterFeedTableViewCellLayout.editingControlIndent)
}
}
let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding)

View File

@ -14,7 +14,7 @@ import RSTree
class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
private var refreshProgressView: RefreshProgressView!
private var refreshProgressView: RefreshProgressView?
private var addNewItemButton: UIBarButtonItem!
private lazy var dataSource = makeDataSource()
@ -99,10 +99,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
}
if let node = node, let indexPath = dataSource.indexPath(for: node), let unreadCountProvider = node.representedObject as? UnreadCountProvider {
if let cell = tableView.cellForRow(at: indexPath) as? MasterFeedTableViewCell {
cell.unreadCount = unreadCountProvider.unreadCount
}
if let node = node, dataSource.indexPath(for: node) != nil {
reloadNode(node)
}
}
@ -131,7 +129,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
guard let feed = notification.userInfo?[UserInfoKey.feed] as? Feed else {
return
}
discloseFeed(feed)
discloseFeed(feed, animated: true)
}
@objc func contentSizeCategoryDidChange(_ note: Notification) {
@ -483,7 +481,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
}
func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) {
func discloseFeed(_ feed: Feed, animated: Bool, completion: (() -> Void)? = nil) {
guard let node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else {
completion?()
@ -492,7 +490,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
if let indexPath = dataSource.indexPath(for: node) {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true)
coordinator.selectFeed(indexPath)
coordinator.selectFeed(indexPath, animated: animated)
completion?()
return
}
@ -507,7 +505,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
self.applyChanges(animate: true, adjustScroll: true) { [weak self] in
if let indexPath = self?.dataSource.indexPath(for: node) {
self?.coordinator.selectFeed(indexPath)
self?.coordinator.selectFeed(indexPath, animated: animated)
completion?()
}
}
@ -572,8 +570,8 @@ private extension MasterFeedViewController {
}
func updateUI() {
refreshProgressView.updateRefreshLabel()
addNewItemButton.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
refreshProgressView?.updateRefreshLabel()
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
}
func reloadNode(_ node: Node) {
@ -656,7 +654,7 @@ private extension MasterFeedViewController {
return feedIconImage
}
if let faviconImage = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
if let faviconImage = appDelegate.faviconDownloader.favicon(for: feed) {
return faviconImage
}

View File

@ -57,7 +57,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
NSLocalizedString("Here", comment: "Here"),
NSLocalizedString("All Articles", comment: "All Articles")
]
navigationItem.searchController = searchController
definesPresentationContext = true
// Configure the table
@ -74,6 +73,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
super.viewWillAppear(animated)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// You have to assign the search controller here to avoid showing it by default
// https://stackoverflow.com/questions/57581557/how-to-initally-hide-searchbar-in-navigation-controller-on-ios-13
navigationItem.searchController = searchController
}
// MARK: Actions
@IBAction func markAllAsRead(_ sender: Any) {
@ -580,7 +586,8 @@ private extension MasterTimelineViewController {
let title = NSLocalizedString("Select Feed", comment: "Select Feed")
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in
self?.coordinator.discloseFeed(feed)
self?.coordinator.selectFeed(nil, animated: true)
self?.coordinator.discloseFeed(feed, animated: true)
}
return action
}
@ -590,7 +597,8 @@ private extension MasterTimelineViewController {
let title = NSLocalizedString("Select Feed", comment: "Select Feed")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.discloseFeed(feed)
self?.coordinator.selectFeed(nil, animated: true)
self?.coordinator.discloseFeed(feed, animated: true)
completionHandler(true)
}
return action

View File

@ -1,23 +1,37 @@
var imageIsLoading = false;
var controller = new AbortController()
// Cancel any pending image loads (there might be none) and reset the controller
function cancelImageLoad() {
controller.abort();
controller = new AbortController();
}
// Used to pop a resizable image view
async function imageWasClicked(img) {
img.classList.add("nnwClicked");
cancelImageLoad();
showNetworkLoading(img);
try {
showNetworkLoading(img);
const response = await fetch(img.src);
const signal = controller.signal;
const response = await fetch(img.src, { signal: signal });
if (!response.ok) {
throw new Error('Network response was not ok.');
}
const imgBlob = await response.blob();
if (signal.aborted) {
throw new Error('Network response was aborted.');
}
hideNetworkLoading(img);
var reader = new FileReader();
reader.readAsDataURL(imgBlob);
reader.onloadend = function() {
img.classList.add("nnwClicked");
const rect = img.getBoundingClientRect();
var message = {
x: rect.x,
@ -29,8 +43,9 @@ async function imageWasClicked(img) {
var jsonMessage = JSON.stringify(message);
window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage);
}
} catch (error) {
hideNetworkLoading(img);
console.log('There has been a problem with your fetch operation: ', error.message);
@ -39,7 +54,6 @@ async function imageWasClicked(img) {
}
function showNetworkLoading(img) {
imageIsLoading = true;
var wrapper = document.createElement("div");
wrapper.classList.add("activityIndicatorWrap");
@ -64,8 +78,6 @@ function hideNetworkLoading(img) {
var wrapperParent = wrapper.parentNode;
wrapperParent.insertBefore(img, wrapper);
wrapperParent.removeChild(wrapper);
imageIsLoading = false;
}
// Used to animate the transition to a fullscreen image
@ -85,7 +97,7 @@ function showClickedImage() {
// Add the click listener for images
function imageClicks() {
window.onclick = function(event) {
if (event.target.matches('img') && !imageIsLoading) {
if (event.target.matches('img')) {
imageWasClicked(event.target);
}
}
@ -101,8 +113,9 @@ function inlineVideos() {
}
function postRenderProcessing() {
imageClicks()
inlineVideos()
cancelImageLoad();
imageClicks();
inlineVideos();
}
const activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg==";

View File

@ -14,10 +14,10 @@ class RootSplitViewController: UISplitViewController {
var coordinator: SceneCoordinator!
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIApplication.shared.applicationState != .background {
self.coordinator.configureThreePanelMode(for: size)
}
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] context in
self?.coordinator.configureThreePanelMode(for: size)
})
}
// MARK: Keyboard Shortcuts

View File

@ -65,6 +65,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private var lastSearchScope: SearchScope? = nil
private var isSearching: Bool = false
private var searchArticleIds: Set<String>? = nil
private var isTimelineViewControllerPending = false
private var isArticleViewControllerPending = false
private(set) var sortDirection = AppDefaults.timelineSortDirection {
@ -782,8 +783,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
}
func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) {
masterFeedViewController.discloseFeed(feed) {
func discloseFeed(_ feed: Feed, animated: Bool, completion: (() -> Void)? = nil) {
masterFeedViewController.discloseFeed(feed, animated: animated) {
completion?()
}
}
@ -952,11 +953,15 @@ extension SceneCoordinator: UINavigationControllerDelegate {
}
// If we are showing the Feeds and only the feeds start clearing stuff
if viewController === masterFeedViewController && !isThreePanelMode {
if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending {
activityManager.invalidateCurrentActivities()
selectFeed(nil)
return
}
if viewController is MasterTimelineViewController {
isTimelineViewControllerPending = false
}
// If we are using a phone and navigate away from the detail, clear up the article resources (including activity).
// Don't clear it if we have pushed an ArticleViewController, but don't yet see it on the navigation stack.
@ -1473,6 +1478,9 @@ private extension SceneCoordinator {
// MARK: Double Split
func installTimelineControllerIfNecessary(animated: Bool) {
isTimelineViewControllerPending = true
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
@ -1654,7 +1662,7 @@ private extension SceneCoordinator {
return
}
if let feed = feedNode.representedObject as? Feed {
discloseFeed(feed)
discloseFeed(feed, animated: false)
}
}
@ -1663,7 +1671,7 @@ private extension SceneCoordinator {
return
}
discloseFeed(feedNode.representedObject as! Feed) {
discloseFeed(feedNode.representedObject as! Feed, animated: false) {
guard let articleID = userInfo?[DeepLinkKey.articleID.rawValue] as? String else { return }
if let article = self.articles.first(where: { $0.articleID == articleID }) {

View File

@ -33,8 +33,11 @@ class AboutViewController: UITableViewController {
buildLabel.numberOfLines = 0
buildLabel.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false
tableView.tableFooterView = buildLabel
let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
wrapperView.translatesAutoresizingMaskIntoConstraints = false
wrapperView.addSubview(buildLabel)
tableView.tableFooterView = wrapperView
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

View File

@ -57,10 +57,89 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Feeds" id="hAC-uA-RbS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="qur-cL-wrM" detailTextLabel="qIl-N6-6wQ" style="IBUITableViewCellStyleValue1" id="z1J-VF-St0" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="255.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="z1J-VF-St0" id="Y8U-Ka-GeZ">
<rect key="frame" x="0.0" y="0.0" width="383" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Refresh Interval" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="qur-cL-wrM">
<rect key="frame" x="20" y="12" width="119.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="qIl-N6-6wQ" customClass="VibrantLabel" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="331" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="4Hg-B3-zAE" style="IBUITableViewCellStyleDefault" id="glf-Pg-s3P" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="299.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="glf-Pg-s3P" id="bPA-43-Oqh">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Import Subscriptions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="4Hg-B3-zAE">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="25J-iX-3at" style="IBUITableViewCellStyleDefault" id="qke-Ha-PXl" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="343.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qke-Ha-PXl" id="pZi-ck-RV5">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Export Subscriptions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="25J-iX-3at">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dXN-Mw-yf2" style="IBUITableViewCellStyleDefault" id="F0L-Ut-reX" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="387.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F0L-Ut-reX" id="5SX-M2-2jR">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Add NetNewsWire News Feed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="dXN-Mw-yf2">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Timeline" id="9Pk-Y8-JVJ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="MpA-w1-Wwh" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="255.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="487.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MpA-w1-Wwh" id="GhU-ib-Mz8">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -90,7 +169,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="f7r-AZ-aDn" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="299.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="531.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="f7r-AZ-aDn" id="KHC-cc-tOC">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -120,7 +199,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5wo-fM-0l6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="343.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="575.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5wo-fM-0l6" id="XAn-lK-LoN">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -151,72 +230,10 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Database" id="hAC-uA-RbS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="qur-cL-wrM" detailTextLabel="qIl-N6-6wQ" style="IBUITableViewCellStyleValue1" id="z1J-VF-St0" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="443.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="z1J-VF-St0" id="Y8U-Ka-GeZ">
<rect key="frame" x="0.0" y="0.0" width="383" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Refresh Interval" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="qur-cL-wrM">
<rect key="frame" x="20" y="12" width="119.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="qIl-N6-6wQ" customClass="VibrantLabel" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="331" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="4Hg-B3-zAE" style="IBUITableViewCellStyleDefault" id="glf-Pg-s3P" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="487.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="glf-Pg-s3P" id="bPA-43-Oqh">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Import OPML" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="4Hg-B3-zAE">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="25J-iX-3at" style="IBUITableViewCellStyleDefault" id="qke-Ha-PXl" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="531.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qke-Ha-PXl" id="pZi-ck-RV5">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Export OPML" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="25J-iX-3at">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="About" id="TkH-4v-yhk">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="2o6-8W-nyK" style="IBUITableViewCellStyleDefault" id="he9-Ql-yfa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="631.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="675.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="he9-Ql-yfa" id="q6L-C8-H9a">
<rect key="frame" x="0.0" y="0.0" width="383" height="44"/>
@ -233,7 +250,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="lOk-Dh-GfZ" style="IBUITableViewCellStyleDefault" id="GWZ-jk-qU6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="675.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="719.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GWZ-jk-qU6" id="ZgS-bo-xDl">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -250,7 +267,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Pm8-6D-fdE" style="IBUITableViewCellStyleDefault" id="3cU-BG-6kK" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="719.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="763.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3cU-BG-6kK" id="Qm0-SY-0vx">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -267,7 +284,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="TEA-EG-V6d" style="IBUITableViewCellStyleDefault" id="4yc-ig-I61" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="763.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="807.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4yc-ig-I61" id="uQl-VP-9p9">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -284,7 +301,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Q9a-Pi-uCc" style="IBUITableViewCellStyleDefault" id="mSW-A7-8lf" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="807.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="851.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mSW-A7-8lf" id="shF-ro-Zpx">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -301,7 +318,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dWz-1o-EpJ" style="IBUITableViewCellStyleDefault" id="2MG-qn-idJ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="851.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="895.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2MG-qn-idJ" id="gP9-ry-keC">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
@ -317,23 +334,6 @@
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dXN-Mw-yf2" style="IBUITableViewCellStyleDefault" id="F0L-Ut-reX" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="895.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F0L-Ut-reX" id="5SX-M2-2jR">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Add NetNewsWire News Feed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="dXN-Mw-yf2">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>

View File

@ -66,7 +66,11 @@ class SettingsViewController: UITableViewController {
buildLabel.text = "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))"
buildLabel.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false
tableView.tableFooterView = buildLabel
let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
wrapperView.translatesAutoresizingMaskIntoConstraints = false
wrapperView.addSubview(buildLabel)
tableView.tableFooterView = wrapperView
}
@ -81,7 +85,7 @@ class SettingsViewController: UITableViewController {
switch section {
case 1:
return AccountManager.shared.accounts.count + 1
case 4:
case 2:
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section)
if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) {
return defaultNumberOfRows - 1
@ -135,7 +139,7 @@ class SettingsViewController: UITableViewController {
controller.account = sortedAccounts[indexPath.row]
self.navigationController?.pushViewController(controller, animated: true)
}
case 3:
case 2:
switch indexPath.row {
case 0:
let timeline = UIStoryboard.settings.instantiateController(ofType: RefreshIntervalViewController.self)
@ -152,6 +156,9 @@ class SettingsViewController: UITableViewController {
let sourceRect = tableView.rectForRow(at: indexPath)
exportOPML(sourceView: sourceView, sourceRect: sourceRect)
}
case 3:
addFeed()
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
default:
break
}
@ -175,9 +182,6 @@ class SettingsViewController: UITableViewController {
case 5:
openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes")
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
case 6:
addFeed()
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
default:
break
}
@ -264,7 +268,15 @@ extension SettingsViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
for url in urls {
opmlAccount?.importOPML(url) { result in}
opmlAccount?.importOPML(url) { result in
switch result {
case .success:
break
case .failure(let error):
let title = NSLocalizedString("Import Failed", comment: "Import Failed")
self.presentError(title: title, message: error.localizedDescription)
}
}
}
}