Merge branch 'upstream/master'

This commit is contained in:
Jeremy Beker 2019-06-15 12:33:00 -04:00
commit ce703fd947
No known key found for this signature in database
GPG Key ID: CD5EE767A4A34FD0
80 changed files with 1354 additions and 511 deletions

58
.circleci/config.yml Normal file
View File

@ -0,0 +1,58 @@
# iOS CircleCI 2.0 configuration file
#
version: 2
jobs:
build:
# Specify the Xcode version to use
macos:
xcode: "10.2.1"
# https://circleci.com/docs/2.0/configuration-reference/
# Mac/IOS specific examples and docs under the following links:
# https://circleci.com/docs/2.0/hello-world-macos/
steps:
- checkout
- run: git submodule sync
- run: git submodule update --init
# Commands will execute in macOS container
# with Xcode 10.2.1 installed
- run: xcodebuild -version
#- run:
# name: get xcodebuild build options
# command: xcodebuild -help
- run:
name: get xcodebuild build settings
command: xcodebuild -showBuildSettings
- run:
name: force wipe of any pre-existing derived data in CI
command: rm -rf /Users/distiller/Library/Developer/Xcode/DerivedData/NetNewsWire-*
# Build the app and run tests
- run:
name: Build Mac
command: xcodebuild -workspace NetNewsWire.xcworkspace -scheme NetNewsWire -configuration Debug -showBuildTimingSummary
# NOTE(heckj):
# the -configuration Release build invokes a shell script specifically
# codesigning the Sparkle pieces with the developer 'Brent Simmons',
# so we don't try and invoke that in CI
#
# the stuff below is from example that was using fastlane
# (and we're not using that...) so it's placeholder tidbits
# to clue me in to where I can get things for test log output
# for the CircleCI UI exposure...
# Collect XML test results data to show in the UI,
# and save the same XML files under test-results folder
# in the Artifacts tab
#- store_test_results:
# path: test_output/report.xml
#- store_artifacts:
# path: /tmp/test-results
# destination: scan-test-results
#- store_artifacts:
# path: ~/Library/Logs/scan
# destination: scan-logs

View File

@ -6,6 +6,58 @@
<description>Most recent NetNewsWire changes with links to updates.</description>
<language>en</language>
<item>
<title>NetNewsWire 5.0a3</title>
<description><![CDATA[
<p>Fixed crash happening only on macOS 10.15 beta. We owe Apple a bug report for this one.</p>
<p>Fixed a crash that could happen when finding a feed.</p>
<p>Skip showing error dialogs on automatic refreshes.</p>
<p>Immediately show the refresh progress bar when an OPML import to Feedbin starts.</p>
<p>Add ellipsis to Import from OPML and Export to OPML buttons.</p>
]]></description>
<pubDate>Mon, 10 Jun 2019 21:45:00 -0700</pubDate>
<enclosure url="https://ranchero.com/downloads/NetNewsWire5.0a3.zip" sparkle:version="2223" sparkle:shortVersionString="5.0a3" length="4689888" type="application/zip" />
<sparkle:minimumSystemVersion>10.14.4</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 5.0a2</title>
<description><![CDATA[
<p>Escape HTML in the title in the article view — if theres HTML in the title, the tags should actually be displayed.</p>
<p>The Mark as Read command in the Article menu now turns into Mark as Unread at the appropriate times.</p>
<p>Feedbin syncing: send locally changed statuses before downloading statuses from the server.</p>
<p>Feedbin syncing: fix bug renaming a folder that has no feeds.</p>
<p>Feedbin syncing: fixed a bunch of accuracy and reliability issues, and a crashing bug.</p>
<p>Fixed issue where local account feed finder could lock UI in the case of an error.</p>
]]></description>
<pubDate>Sat, 08 Jun 2019 16:00:00 -0700</pubDate>
<enclosure url="https://ranchero.com/downloads/NetNewsWire5.0a2.zip" sparkle:version="2209" sparkle:shortVersionString="5.0a2" length="4691481" type="application/zip" />
<sparkle:minimumSystemVersion>10.14.4</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 5.0a1</title>
<description><![CDATA[
<p>NetNewsWire 5.0 has reached alpha stage! This means it has no known bugs. It surely <i>does</i> have bugs, though. Now its time for testing. (And writing the Help book. And making the website better.)</p>
<p>Fixed a crashing bug with parsing a response from Feedbin. (Totally our fault, not Feedbins fault.)</p>
<p>Show avatars from Micro.blog feeds with multiple authors (such as your personal timeline feed).</p>
<p>Made OPML import to the On My Mac account way faster.</p>
<p>The Today smart feed now updates when the day changes.</p>
<p>You can now drag and drop in the sidebar between accounts.</p>
<p>Made the default file name for OPML exports “Subscriptions-[accountName].opml”</p>
<p>Add explanation text to Account preferences for the Name field. (Its just a display name and doesnt affect authentication.)</p>
<p>Fixed several bugs with Feedbin syncing — its now more reliable. (We know of no remaining sync bugs, though of course there might be some.)</p>
<p>Added a placeholder web page for the Help book.</p>
<p>New app icon! But it might take a while for your Mac to notice and put in the Dock. (I wish we could speed that up, but its out of our control.)</p>
]]></description>
<pubDate>Fri, 31 May 2019 20:30:00 -0700</pubDate>
<enclosure url="https://ranchero.com/downloads/NetNewsWire5.0a1.zip" sparkle:version="2185" sparkle:shortVersionString="5.0a1" length="4686634" type="application/zip" />
<sparkle:minimumSystemVersion>10.14.4</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 5.0d17</title>
<description><![CDATA[

94
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,94 @@
# Contributing
We welcome contributions!
If youd like to contribute:
1. File a ticket describing the bug you want to fix or feature you want to add. Or find an existing ticket.
2. On the Slack group, bring it up on the #work channel for discussion (which may or may not include implementation discussion).
3. Once approved, then go for it. Write the code, then do a pull request. Well either have comments or well merge it. (We might revise it afterward, of course.)
## Notes
Its important that the pull request merge cleanly with master.
You should have read the [coding guidelines](Technotes/CodingGuidelines.md) first. If your code doesnt follow the guidelines, we will likely suggest revising it.
Patience may be required at times. Brent has a day job, and sometimes everything happens at once. :)
Our code of conduct is below.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting Brent Simmons at brent@ranchero.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -263,6 +263,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
switch credentials {
case .basic(let username, _):
self.username = username
default:
return
}
try CredentialsManager.storeCredentials(credentials, server: server)

View File

@ -18,6 +18,10 @@ public enum AccountError: LocalizedError {
public var errorDescription: String? {
switch self {
case .createErrorNotFound:
return NSLocalizedString("The feed couldn't be found and can't be added.", comment: "Not found")
case .createErrorAlreadySubscribed:
return NSLocalizedString("You are already subscribed to this feed and can't add it again.", comment: "Already subscribed")
case .opmlImportInProgress:
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
case .wrappedError(let error, let account):
@ -32,13 +36,15 @@ public enum AccountError: LocalizedError {
default:
return unknownError(error, account)
}
default:
return NSLocalizedString("An unknown error occurred.", comment: "Unknown error")
}
}
public var recoverySuggestion: String? {
switch self {
case .createErrorNotFound:
return nil
case .createErrorAlreadySubscribed:
return nil
case .wrappedError(let error, _):
switch error {
case TransportError.httpError(let status):

View File

@ -64,7 +64,7 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
set {
let oldNameForDisplay = nameForDisplay
metadata.name = newValue
if oldNameForDisplay != nameForDisplay {
if oldNameForDisplay != newValue {
postDisplayNameDidChangeNotification()
}
}

View File

@ -46,7 +46,7 @@ struct FeedSpecifier: Hashable {
return feedSpecifiers.anyObject()
}
var currentHighScore = 0
var currentHighScore = Int.min
var currentBestFeed: FeedSpecifier? = nil
for oneFeedSpecifier in feedSpecifiers {

View File

@ -532,11 +532,11 @@ extension FeedbinAPICaller {
if let lowerBound = link.range(of: "page=")?.upperBound {
let partialLink = link[lowerBound..<link.endIndex]
if let upperBound = partialLink.range(of: "&")?.lowerBound {
return Int(link[lowerBound..<upperBound])
if let upperBound = partialLink.firstIndex(of: "&") {
return Int(partialLink[partialLink.startIndex..<upperBound])
}
if let upperBound = partialLink.range(of: ">")?.lowerBound {
return Int(link[lowerBound..<upperBound])
if let upperBound = partialLink.firstIndex(of: ">") {
return Int(partialLink[partialLink.startIndex..<upperBound])
}
}

View File

@ -88,11 +88,13 @@ final class FeedbinAccountDelegate: AccountDelegate {
case .success():
self.refreshArticles(account) {
self.refreshArticleStatus(for: account) {
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(()))
self.sendArticleStatus(for: account) {
self.refreshArticleStatus(for: account) {
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(()))
}
}
}
}
@ -206,12 +208,14 @@ final class FeedbinAccountDelegate: AccountDelegate {
os_log(.debug, log: log, "Begin importing OPML...")
opmlImportInProgress = true
refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.importOPML(opmlData: opmlData) { result in
switch result {
case .success(let importResult):
if importResult.complete {
os_log(.debug, log: self.log, "Import OPML done.")
self.refreshProgress.completeTask()
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.success(()))
@ -221,6 +225,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
case .failure(let error):
os_log(.debug, log: self.log, "Import OPML failed.")
self.refreshProgress.completeTask()
self.opmlImportInProgress = false
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
@ -241,10 +246,16 @@ final class FeedbinAccountDelegate: AccountDelegate {
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard folder.hasAtLeastOneFeed() else {
folder.name = name
return
}
caller.renameTag(oldName: folder.name ?? "", newName: name) { result in
switch result {
case .success:
DispatchQueue.main.async {
self.renameFolderRelationship(for: account, fromName: folder.name ?? "", toName: name)
folder.name = name
completion(.success(()))
}
@ -269,16 +280,44 @@ final class FeedbinAccountDelegate: AccountDelegate {
let group = DispatchGroup()
for feed in folder.topLevelFeeds {
group.enter()
removeFeed(for: account, with: feed, from: folder) { result in
group.leave()
switch result {
case .success:
break
case .failure(let error):
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
if feed.folderRelationship?.count ?? 0 > 1 {
if let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
group.enter()
caller.deleteTagging(taggingID: feedTaggingID) { result in
group.leave()
switch result {
case .success:
DispatchQueue.main.async {
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
}
case .failure(let error):
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
}
}
}
} else {
if let subscriptionID = feed.subscriptionID {
group.enter()
caller.deleteSubscription(subscriptionID: subscriptionID) { result in
group.leave()
switch result {
case .success:
DispatchQueue.main.async {
account.clearFeedMetadata(feed)
}
case .failure(let error):
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
}
}
}
}
}
group.notify(queue: DispatchQueue.main) {
@ -347,7 +386,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
if feed.folderRelationship?.count ?? 0 > 1 {
deleteTagging(for: account, with: feed, from: container, completion: completion)
} else {
account.clearFeedMetadata(feed)
deleteSubscription(for: account, with: feed, from: container, completion: completion)
}
}
@ -399,12 +437,23 @@ final class FeedbinAccountDelegate: AccountDelegate {
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
if let existingFeed = account.existingFeed(withURL: feed.url) {
account.addFeed(existingFeed, to: container) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} else {
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
@ -412,22 +461,27 @@ final class FeedbinAccountDelegate: AccountDelegate {
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
account.addFolder(folder)
let group = DispatchGroup()
for feed in folder.topLevelFeeds {
folder.topLevelFeeds.remove(feed)
group.enter()
addFeed(for: account, with: feed, to: folder) { result in
if account.topLevelFeeds.contains(feed) {
account.removeFeed(feed)
}
restoreFeed(for: account, feed: feed, container: folder) { result in
group.leave()
switch result {
case .success:
break
case .failure(let error):
os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription)
}
}
}
group.notify(queue: DispatchQueue.main) {
account.addFolder(folder)
completion(.success(()))
}
@ -502,6 +556,7 @@ private extension FeedbinAccountDelegate {
if let result = importResult, result.complete {
os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.")
timer.invalidate()
self.refreshProgress.completeTask()
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.success(()))
@ -510,6 +565,7 @@ private extension FeedbinAccountDelegate {
case .failure(let error):
os_log(.debug, log: self.log, "Import OPML check failed.")
timer.invalidate()
self.refreshProgress.completeTask()
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.failure(error))
@ -647,7 +703,10 @@ private extension FeedbinAccountDelegate {
DispatchQueue.main.sync {
if let feed = account.idToFeedDictionary[subFeedId] {
feed.name = subscription.name
// If the name has been changed on the server remove the locally edited name
feed.editedName = nil
feed.homePageURL = subscription.homePageURL
feed.subscriptionID = String(subscription.subscriptionID)
} else {
let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL)
feed.subscriptionID = String(subscription.subscriptionID)
@ -792,6 +851,17 @@ private extension FeedbinAccountDelegate {
}
func renameFolderRelationship(for account: Account, fromName: String, toName: String) {
for feed in account.flattenedFeeds() {
if var folderRelationship = feed.folderRelationship {
let relationship = folderRelationship[fromName]
folderRelationship[fromName] = nil
folderRelationship[toName] = relationship
feed.folderRelationship = folderRelationship
}
}
}
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = nil
@ -1158,6 +1228,7 @@ private extension FeedbinAccountDelegate {
switch result {
case .success:
DispatchQueue.main.async {
account.clearFeedMetadata(feed)
account.removeFeed(feed)
if let folders = account.folders {
for folder in folders {

View File

@ -124,12 +124,12 @@ final class LocalAccountDelegate: AccountDelegate {
}
case .failure(let error):
completion(.failure(error))
case .failure:
completion(.failure(AccountError.createErrorNotFound))
}
}
}
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {

View File

@ -413,7 +413,7 @@
<items>
<menuItem title="Mark as Read" keyEquivalent="U" id="Fc9-c7-2AY">
<connections>
<action selector="markRead:" target="Ady-hI-5gd" id="RQv-jl-2Nv"/>
<action selector="toggleRead:" target="Ady-hI-5gd" id="jLQ-ZF-xye"/>
</connections>
</menuItem>
<menuItem title="Mark All as Read" keyEquivalent="k" id="HdN-Ks-cwh">

View File

@ -8,11 +8,18 @@
import AppKit
import Account
import os.log
struct ErrorHandler {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
public static func present(_ error: Error) {
NSApplication.shared.presentError(error)
}
public static func log(_ error: Error) {
os_log(.error, log: self.log, "%@", error.localizedDescription)
}
}

View File

@ -62,7 +62,10 @@ class AddFeedController: AddFeedWindowControllerDelegate {
account.createFeed(url: url.absoluteString, name: title, container: container) { result in
self.endShowingProgress()
DispatchQueue.main.async {
self.endShowingProgress()
}
BatchUpdate.shared.end()
switch result {

View File

@ -18,12 +18,12 @@
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="355" height="180"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="371" height="171"/>
<view key="contentView" wantsLayer="YES" misplaced="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="391" height="171"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="mvx-54-DH0">
<rect key="frame" x="18" y="100" width="335" height="51"/>
<rect key="frame" x="18" y="100" width="355" height="51"/>
<textFieldCell key="cell" selectable="YES" id="7Ap-KG-Lc7">
<font key="font" metaFont="system"/>
<string key="title">Choose the account with the subscriptions youd like to export. Subscriptions are exported in the standard OPML format, which most RSS readers can import.</string>
@ -40,7 +40,7 @@
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bbC-2g-e3k" userLabel="Account Popup">
<rect key="frame" x="85" y="58" width="269" height="25"/>
<rect key="frame" x="85" y="58" width="289" height="25"/>
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="MJb-Bf-UJG" id="xZd-AP-nuM">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -54,8 +54,8 @@
</popUpButtonCell>
</popUpButton>
<button horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eZ4-Ej-Hks">
<rect key="frame" x="219" y="13" width="138" height="32"/>
<buttonCell key="cell" type="push" title="Export as OPML" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bRz-cx-bmm">
<rect key="frame" x="229" y="13" width="148" height="32"/>
<buttonCell key="cell" type="push" title="Export as OPML" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bRz-cx-bmm">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
@ -67,7 +67,7 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PPB-R8-A9a">
<rect key="frame" x="81" y="13" width="138" height="32"/>
<rect key="frame" x="81" y="13" width="148" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6lK-bV-Vwd">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>

View File

@ -18,12 +18,12 @@
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="401" height="183"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="401" height="154"/>
<view key="contentView" wantsLayer="YES" misplaced="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="421" height="154"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="vE6-sv-BA0">
<rect key="frame" x="18" y="100" width="365" height="34"/>
<rect key="frame" x="18" y="100" width="385" height="34"/>
<textFieldCell key="cell" selectable="YES" title="Choose the account to get the imported subscriptions. This requires an OPML file, which most RSS readers can create." id="1Vu-Te-PGl">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -39,7 +39,7 @@
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sEU-ot-DE2" userLabel="Account Popup">
<rect key="frame" x="85" y="58" width="299" height="25"/>
<rect key="frame" x="85" y="58" width="319" height="25"/>
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="xsd-12-2yb" id="NuO-Hk-nk3">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -53,7 +53,7 @@
</popUpButtonCell>
</popUpButton>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ceu-mM-EKm">
<rect key="frame" x="81" y="13" width="153" height="32"/>
<rect key="frame" x="81" y="13" width="163" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="9ab-cB-hex">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -66,8 +66,8 @@ Gw
</connections>
</button>
<button horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="et6-I1-6wB">
<rect key="frame" x="234" y="13" width="153" height="32"/>
<buttonCell key="cell" type="push" title="Import from OPML" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="dhV-on-ayM">
<rect key="frame" x="244" y="13" width="163" height="32"/>
<buttonCell key="cell" type="push" title="Import from OPML" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="dhV-on-ayM">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">

View File

@ -18,15 +18,18 @@ final class SingleLineTextFieldSizer {
private let textField: NSTextField
private var cache = [String: NSSize]()
init(font: NSFont) {
/// Get the NSTextField size for text, given a font.
static func size(for text: String, font: NSFont) -> NSSize {
return sizer(for: font).size(for: text)
}
init(font: NSFont) {
self.textField = NSTextField(labelWithString: "")
self.textField.font = font
self.font = font
}
func size(for text: String) -> NSSize {
if let cachedSize = cache[text] {
return cachedSize
}
@ -40,29 +43,23 @@ final class SingleLineTextFieldSizer {
return calculatedSize
}
static private var sizers = [NSFont: SingleLineTextFieldSizer]()
static private var sizers = [SingleLineTextFieldSizer]()
static func sizer(for font: NSFont) -> SingleLineTextFieldSizer {
if let cachedSizer = sizers[font] {
static private func sizer(for font: NSFont) -> SingleLineTextFieldSizer {
// We used to use an [NSFont: SingleLineTextFieldSizer] dictionary
// until, in 10.14.5, we started getting crashes with the message:
// Fatal error: Duplicate keys of type 'NSFont' were found in a Dictionary.
// This usually means either that the type violates Hashable's requirements, or
// that members of such a dictionary were mutated after insertion.
// We use just an array of sizers now which is totally fine,
// because theres only going to be like three of them.
if let cachedSizer = sizers.firstElementPassingTest({ $0.font == font }) {
return cachedSizer
}
let newSizer = SingleLineTextFieldSizer(font: font)
sizers[font] = newSizer
sizers.append(newSizer)
return newSizer
}
// Use this call. Its easiest.
static func size(for text: String, font: NSFont) -> NSSize {
return sizer(for: font).size(for: text)
}
static func emptyCache() {
sizers = [NSFont: SingleLineTextFieldSizer]()
}
}

View File

@ -1,68 +0,0 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon_16x16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon_16x16@2x.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32@2x.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon_128x128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon_128x128@2x.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon_256x256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon_256x256@2x.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon_512x512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon_512x512@2x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 639 KiB

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>5.0d17</string>
<string>5.0a3</string>
<key>CFBundleURLTypes</key>
<array>
<dict>

View File

@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
510D707422B028E1004E8F65 /* SettingsAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */; };
510D707E22B02A4B004E8F65 /* SettingsLocalAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */; };
510D708022B02A5F004E8F65 /* SettingsFeedbinAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */; };
510D708222B041CC004E8F65 /* SettingsAccountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */; };
51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; };
5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* NavigationStateController.swift */; };
@ -134,6 +138,8 @@
51EF0F8E2279C9260050506E /* AccountsAdd.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51EF0F8D2279C9260050506E /* AccountsAdd.xib */; };
51EF0F902279C9500050506E /* AccountsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */; };
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */; };
51F35D0922AFD4760003CE1B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D0822AFD4760003CE1B /* SettingsView.swift */; };
51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */; };
51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */; };
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BE6227245FC00C787DC /* AboutViewController.swift */; };
51F85BEB22724CB600C787DC /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEA22724CB600C787DC /* About.rtf */; };
@ -159,7 +165,6 @@
840958632201629A002C1579 /* Subscribe to Feed.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840BEE4021D70E64009BBAFA /* CrashReportWindowController.swift */; };
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D617E2029031C009BC708 /* AppDelegate.swift */; };
840D61962029031D009BC708 /* NetNewsWire_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */; };
84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */; };
841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; };
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */; };
@ -456,13 +461,6 @@
remoteGlobalIDString = 844BEE401F0AB3AB004AB7CD;
remoteInfo = ArticlesDatabaseTests;
};
840D61922029031D009BC708 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 840D617B2029031C009BC708;
remoteInfo = "NetNewsWire-iOS";
};
849C64721ED37A5D003D8FC0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */;
@ -661,6 +659,10 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddAccountView.swift; sourceTree = "<group>"; };
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = "<group>"; };
510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeedbinAccountView.swift; sourceTree = "<group>"; };
510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = "<group>"; };
51121AA12265430A00BC0EC1 /* NetNewsWire_iOS_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOS_target.xcconfig; sourceTree = "<group>"; };
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = "<group>"; };
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = "<group>"; };
@ -727,6 +729,8 @@
51EF0F8D2279C9260050506E /* AccountsAdd.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsAdd.xib; sourceTree = "<group>"; };
51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddViewController.swift; sourceTree = "<group>"; };
51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddTableCellView.swift; sourceTree = "<group>"; };
51F35D0822AFD4760003CE1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountView.swift; sourceTree = "<group>"; };
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = "<group>"; };
51F85BE6227245FC00C787DC /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = "<group>"; };
@ -756,7 +760,6 @@
840BEE4021D70E64009BBAFA /* CrashReportWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportWindowController.swift; sourceTree = "<group>"; };
840D617C2029031C009BC708 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; };
840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
840D61912029031D009BC708 /* NetNewsWire-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "NetNewsWire-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire_iOSTests.swift; sourceTree = "<group>"; };
840D61972029031D009BC708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkCommandValidationStatus.swift; sourceTree = "<group>"; };
@ -873,6 +876,7 @@
84C9FCA32262A1B800D921D6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = "<group>"; };
84CC88171FE59CBF00644329 /* SmartFeedsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedsController.swift; sourceTree = "<group>"; };
84D2200922B0BC4B0019E085 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStatusBarView.swift; sourceTree = "<group>"; };
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleLineTextFieldSizer.swift; sourceTree = "<group>"; };
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldSizer.swift; sourceTree = "<group>"; };
@ -949,13 +953,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
840D618E2029031D009BC708 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
849C645D1ED37A5D003D8FC0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -1043,16 +1040,13 @@
5183CCEB227117C70010922C /* Settings */ = {
isa = PBXGroup;
children = (
5183CCEC22711DCE0010922C /* Settings.storyboard */,
51E595AA228DF94C00FCC42B /* SettingsTableViewCell.xib */,
5183CCEE227125970010922C /* SettingsViewController.swift */,
51E595AC228E1C2100FCC42B /* AddAccountViewController.swift */,
515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */,
515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */,
51F85BE6227245FC00C787DC /* AboutViewController.swift */,
51543684228F6753005E1CDF /* DetailAccountViewController.swift */,
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */,
51EF0F7B2277919E0050506E /* TimelineNumberOfLinesViewController.swift */,
510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */,
510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */,
51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */,
510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */,
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */,
51F35D0822AFD4760003CE1B /* SettingsView.swift */,
51F35CFD22AFD0350003CE1B /* UIKit */,
);
path = Settings;
sourceTree = "<group>";
@ -1163,6 +1157,23 @@
name = Frameworks;
sourceTree = "<group>";
};
51F35CFD22AFD0350003CE1B /* UIKit */ = {
isa = PBXGroup;
children = (
5183CCEC22711DCE0010922C /* Settings.storyboard */,
51E595AA228DF94C00FCC42B /* SettingsTableViewCell.xib */,
5183CCEE227125970010922C /* SettingsViewController.swift */,
51E595AC228E1C2100FCC42B /* AddAccountViewController.swift */,
515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */,
515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */,
51F85BE6227245FC00C787DC /* AboutViewController.swift */,
51543684228F6753005E1CDF /* DetailAccountViewController.swift */,
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */,
51EF0F7B2277919E0050506E /* TimelineNumberOfLinesViewController.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
6581C73620CED60100F4AD34 /* SafariExtension */ = {
isa = PBXGroup;
children = (
@ -1447,6 +1458,7 @@
isa = PBXGroup;
children = (
845B14A51FC2299E0013CF92 /* README.md */,
84D2200922B0BC4B0019E085 /* CONTRIBUTING.md */,
84CBDDAE1FD3674C005A61AA /* Technotes */,
84C9FC6522629B3900D921D6 /* Mac */,
84C9FC922262A0E600D921D6 /* iOS */,
@ -1470,7 +1482,6 @@
849C64601ED37A5D003D8FC0 /* NetNewsWire.app */,
849C64711ED37A5D003D8FC0 /* NetNewsWireTests.xctest */,
840D617C2029031C009BC708 /* NetNewsWire.app */,
840D61912029031D009BC708 /* NetNewsWire-iOSTests.xctest */,
6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */,
);
name = Products;
@ -1864,24 +1875,6 @@
productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */;
productType = "com.apple.product-type.application";
};
840D61902029031D009BC708 /* NetNewsWire-iOSTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 840D61A62029031E009BC708 /* Build configuration list for PBXNativeTarget "NetNewsWire-iOSTests" */;
buildPhases = (
840D618D2029031D009BC708 /* Sources */,
840D618E2029031D009BC708 /* Frameworks */,
840D618F2029031D009BC708 /* Resources */,
);
buildRules = (
);
dependencies = (
840D61932029031D009BC708 /* PBXTargetDependency */,
);
name = "NetNewsWire-iOSTests";
productName = "NetNewsWire-iOSTests";
productReference = 840D61912029031D009BC708 /* NetNewsWire-iOSTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
849C645F1ED37A5D003D8FC0 /* NetNewsWire */ = {
isa = PBXNativeTarget;
buildConfigurationList = 849C647A1ED37A5D003D8FC0 /* Build configuration list for PBXNativeTarget "NetNewsWire" */;
@ -1955,12 +1948,6 @@
};
};
};
840D61902029031D009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = 9C84TZ7Q6Z;
ProvisioningStyle = Automatic;
TestTargetID = 840D617B2029031C009BC708;
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG;
@ -2034,7 +2021,6 @@
849C645F1ED37A5D003D8FC0 /* NetNewsWire */,
849C64701ED37A5D003D8FC0 /* NetNewsWireTests */,
840D617B2029031C009BC708 /* NetNewsWire-iOS */,
840D61902029031D009BC708 /* NetNewsWire-iOSTests */,
6581C73220CED60000F4AD34 /* Subscribe to Feed */,
);
};
@ -2215,13 +2201,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
840D618F2029031D009BC708 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
849C645E1ED37A5D003D8FC0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -2342,11 +2321,15 @@
51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */,
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
5183CCDF226F1FCC0010922C /* UINavigationController+Progress.swift in Sources */,
510D707422B028E1004E8F65 /* SettingsAddAccountView.swift in Sources */,
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
51F772F622B279570087D9D1 /* SettingsDetailAccountView.swift in Sources */,
510D707E22B02A4B004E8F65 /* SettingsLocalAccountView.swift in Sources */,
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */,
51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */,
5154368A2291FED9005E1CDF /* FeedbinAccountViewController.swift in Sources */,
510D708222B041CC004E8F65 /* SettingsAccountLabelView.swift in Sources */,
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
@ -2361,6 +2344,7 @@
51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */,
51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */,
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */,
51F35D0922AFD4760003CE1B /* SettingsView.swift in Sources */,
51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */,
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */,
@ -2393,14 +2377,7 @@
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
840D618D2029031D009BC708 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
840D61962029031D009BC708 /* NetNewsWire_iOSTests.swift in Sources */,
510D708022B02A5F004E8F65 /* SettingsFeedbinAccountView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2624,11 +2601,6 @@
name = Account;
targetProxy = 51C451FA2264C83E00C03939 /* PBXContainerItemProxy */;
};
840D61932029031D009BC708 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 840D617B2029031C009BC708 /* NetNewsWire-iOS */;
targetProxy = 840D61922029031D009BC708 /* PBXContainerItemProxy */;
};
849C64731ED37A5D003D8FC0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 849C645F1ED37A5D003D8FC0 /* NetNewsWire */;
@ -2823,7 +2795,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = iOS/Resources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
@ -2886,7 +2858,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = iOS/Resources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.NetNewsWire-Evergreen.iOS";
@ -2898,137 +2870,6 @@
};
name = Release;
};
840D61A72029031E009BC708 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = "NetNewsWire-iOSTests/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.NetNewsWire-Evergreen.iOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NetNewsWire-iOS.app/NetNewsWire-iOS";
};
name = Debug;
};
840D61A82029031E009BC708 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = "NetNewsWire-iOSTests/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.NetNewsWire-Evergreen.iOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NetNewsWire-iOS.app/NetNewsWire-iOS";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
849C64781ED37A5D003D8FC0 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D5907CDD2002F0BE005947E5 /* NetNewsWire_project_debug.xcconfig */;
@ -3113,15 +2954,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
840D61A62029031E009BC708 /* Build configuration list for PBXNativeTarget "NetNewsWire-iOSTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
840D61A72029031E009BC708 /* Debug */,
840D61A82029031E009BC708 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
849C645B1ED37A5D003D8FC0 /* Build configuration list for PBXProject "NetNewsWire" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -27,6 +27,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "849C645F1ED37A5D003D8FC0"
BuildableName = "NetNewsWire.app"
BlueprintName = "NetNewsWire"
ReferencedContainer = "container:NetNewsWire.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
@ -39,17 +48,6 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "849C645F1ED37A5D003D8FC0"
BuildableName = "NetNewsWire.app"
BlueprintName = "NetNewsWire"
ReferencedContainer = "container:NetNewsWire.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -63,6 +61,7 @@
stopOnEveryThreadSanitizerIssue = "YES"
stopOnEveryUBSanitizerIssue = "YES"
stopOnEveryMainThreadCheckerIssue = "YES"
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
@ -75,8 +74,6 @@
ReferencedContainer = "container:NetNewsWire.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -1,8 +1,10 @@
# NetNewsWire
# ![Icon](Technotes/Images/icon.png) NetNewsWire
[![CircleCI](https://circleci.com/gh/brentsimmons/NetNewsWire.svg?style=svg)](https://circleci.com/gh/brentsimmons/NetNewsWire)
Its a free and open source feed reader for macOS.
Its not in beta yet. Not even alpha! While NetNewsWire 5.0 is feature-complete as of May 25, 2019, it has known bugs — and, surely, plenty of unknown bugs.
Its not in beta just yet. Getting close! While NetNewsWire 5.0 is feature-complete as of May 25, 2019, it has known bugs — and, surely, plenty of unknown bugs.
It supports [RSS](http://cyber.harvard.edu/rss/rss.html), [Atom](https://tools.ietf.org/html/rfc4287), [JSON Feed](https://jsonfeed.org/), and [RSS-in-JSON](https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md) formats.
@ -12,19 +14,17 @@ Also see the [Technotes](Technotes/) and the [Roadmap](Technotes/Roadmap.md).
Note: NetNewsWires Help menu has a bunch of these links, so you dont have to remember to come back to this page.
Heres [How to Support NetNewsWire](Technotes/HowToSupportNetNewsWire.markdown). Spoiler: dont send money. :)
#### Community
[Join the Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc) to talk with other NetNewsWire users — and to help out, if youd like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join.
#### On accepting pull requests
Every community member is expected to abide by the code of conduct which is included in the [Contributing](CONTRIBUTING.md) page.
Its pretty early still, and we have strong opinions about how we want to do things, so were not seeking help just yet.
#### Pull Requests
That said, we will seriously consider any pull requests we do get. Just note that we may not accept them, or we may accept them and do a bunch of revision.
Its probably a good idea to let us know first what youd like to do. The best place for that is definitely the [Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc).
We do plan to add more and more contributors over time. Totally. But were taking it slow as we learn how to manage an open source project.
See the [Contributing](Contributing.md) page for our process. Its pretty straightforward.
#### Building

View File

@ -86,10 +86,11 @@ private extension ArticleRenderer {
}
func titleOrTitleLink() -> String {
let escapedTitle = title.escapeHTML()
if let link = article?.preferredLink {
return title.htmlByAddingLink(link)
return escapedTitle.htmlByAddingLink(link)
}
return title
return escapedTitle
}
func substitutions() -> [String: String] {

View File

@ -46,12 +46,30 @@ final class DeleteCommand: UndoableCommand {
func perform() {
BatchUpdate.shared.perform {
itemSpecifiers.forEach { $0.delete() }
itemSpecifiers.forEach { $0.delete() {} }
treeController.rebuild()
}
registerUndo()
}
func perform(completion: @escaping () -> Void) {
let group = DispatchGroup()
group.enter()
itemSpecifiers.forEach {
$0.delete() {
group.leave()
}
}
treeController.rebuild()
group.notify(queue: DispatchQueue.main) {
self.registerUndo()
completion()
}
}
func undo() {
BatchUpdate.shared.perform {
@ -132,18 +150,20 @@ private struct SidebarItemSpecifier {
self.path = ContainerPath(account: account!, folders: node.containingFolders())
}
func delete() {
func delete(completion: @escaping () -> Void) {
if let feed = feed {
BatchUpdate.shared.start()
account?.removeFeed(feed, from: path.resolveContainer()) { result in
BatchUpdate.shared.end()
completion()
self.checkResult(result)
}
} else if let folder = folder {
BatchUpdate.shared.start()
account?.removeFolder(folder) { result in
BatchUpdate.shared.end()
completion()
self.checkResult(result)
}
}

View File

@ -73,7 +73,7 @@ class AccountRefreshTimer {
lastTimedRefresh = Date()
update()
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
}

View File

@ -8,7 +8,7 @@
import Foundation
enum RefreshInterval: Int {
enum RefreshInterval: Int, CaseIterable {
case manually = 1
case every10Minutes = 2
case every30Minutes = 3

View File

@ -0,0 +1,32 @@
# NetNewsWire Branching Strategy
The main repository for NetNewsWire utilizes a [Trunk Based Development](https://trunkbaseddevelopment.com) branching strategy. This branching strategy is a variant of [Three-Flow](https://www.nomachetejuggling.com/2017/04/09/a-different-branching-strategy/).
## Three-Flow
Three-Flow uses 3 branches to facilitate development, stabilize a release, and manage production hotfixes. Development happens on Master and moves to a branch called Candidate when it is ready to be stabilized. New feature development continues on Master and bug fixes to the release candidate happen on Candidate. When the product is released, it is pushed to the Release branch. Hotfixes can happen on the Release branch. Candidate is now free to be reused to stabilize the next release. All bugs found and fixed are back merged to Candidate and then Master respectively.
![Branching](Images/Branching.png)
All arrows going up are promotions (pushes) to the next environment. All arrows going down are back ports of bugfixes.
That is Three-Flow applied to NetNewsWire. It would be that simple, but we have two products we are going to deliver from the same repository. The iOS and the macOS variants of NetNewsWire. To stabilize and manage both variants, each will need to be given their own Candidate and Release branches.
![Branching Full](Images/Branching-Full.png)
Today (6/12/2019) we have 2 branches, master and macOS Candidate, in the main repository which will eventually grow to be 5 branches.
There will also be a number of repository forks that NetNewWire developers will create to do bug fixes and implement new features (not shown here). Typically contributers will fork the Master branch to thier own repository. They would then create a feature/bugfix branch on their repository. Once work on thier forked branch is complete, they will submit a pull request to be merged back into the main repository master.
## Tagging
Each release should be tagged using [Semantic Versioning](https://semver.org/). Candidates will continue to be tagged using the current convention which denotes the difference between developer, alpha and beta releases. Additionally, we will need to use a convention to avoid tag name collisions between iOS and macOS products. macOS will use even minor release numbers and iOS will use odd minor release numbers. (See the above diagram for examples.)
## Submodules
NetNewsWire uses Git submodules to manage project dependencies. All the submodules are under the same project umbrella as NetNewWire and there are no third party dependencies to manage. These submodules are mostly stable at this point. For simplicity sake, all development on the submodules will continue on their repository Master branch. These submodules wont be managed as separate projects with separate releases/tags at this time.
## Summary
There are 3 types of branches: Master, Candidate, and Release. All feature development happens on Master. Stabilization happens on Candidate. Hotfixes happen on Release. Each product gets its own Candidate and Release branches. All candidates and releases get tagged.

View File

@ -0,0 +1,30 @@
# NetNewsWire Continuous Integration
CI for NetNewsWire is enabled through CircleCI, hosted at
<https://circleci.com/gh/brentsimmons/NetNewsWire>. The CI configuration (hosted in
[`.circleci/config.yml`](https://github.com/brentsimmons/NetNewsWire/blob/master/.circleci/config.yml)
uses `xcodebuild` to build the project after syncing the repository and
the various submodules.
As of June 2019, CircleCI offered Xcode 10.2.1, so IOS 13 and Catalina support are not available
via CI as yet.
The build itself focuses on the scheme NetNewsWire and leverages the
`NetNewsWire.xcworkspace` configuration.
Each submodule also has it's own CI configuration, which are set up and built from
their own repositories. The submodule CI systems are entirely independent so that
those libraries can grow and change, getting CI verification, indepdent of NetNewsWire.
The submodule CI are typically set to run a build and any available tests. Refer to the
project repository for the current and complete list of submodules, but for quick reference:
- [RSCore](https://github.com/brentsimmons/RSCore) [![CircleCI](https://circleci.com/gh/brentsimmons/RSCore.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSCore)
- [RSWeb](https://github.com/brentsimmons/RSWeb) [![CircleCI](https://circleci.com/gh/brentsimmons/RSWeb.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSWeb)
- [RSParser](https://github.com/brentsimmons/RSParser) [![CircleCI](https://circleci.com/gh/brentsimmons/RSParser.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSParser)
- [RSTree](https://github.com/brentsimmons/RSTree) [![CircleCI](https://circleci.com/gh/brentsimmons/RSTree.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSTree)
- [RSDatabase](https://github.com/brentsimmons/RSDatabase) [![CircleCI](https://circleci.com/gh/brentsimmons/RSDatabase.svg?style=svg)](https://circleci.com/gh/brentsimmons/RSDatabase)

View File

@ -0,0 +1,43 @@
# How to Support NetNewsWire
First thing: dont send money. This app is [written for love](https://inessential.com/2015/06/30/love), not money. :)
NetNewsWire is all about three things:
* The open web
* High-quality open source Mac and iOS apps
* The community that loves both of the above
Supporting all these things takes *work*.
### Here are some things you can do
In no particular order…
Write a blog instead of posting to Twitter or Facebook. (You can always re-post to those places if you want to extend your reach.) [Micro.blog](https://micro.blog/) is one good place to get going, but its not the only one.
Use an RSS reader even if its not NetNewsWire. (There are a bunch of good ones!)
Teach other people to use RSS readers. Blog about RSS readers. And about other open web technologies and apps.
Suggest apps for [macopenweb.com](https://macopenweb.com/).
Write Mac and iOS apps that promote use of the open web.
Donate to charities that promote literacy.
Tell other people about cool blogs and feeds youve found.
Support indie podcast apps.
Vote for candidates whose policies are not cruel.
Support your local library.
Be bold and do your best work.
Support indie developers — pay for apps that cost money. Even though NetNewsWire is free, apps are most definitely *not* free to make, and it costs money to keep improving them. Its worth it.
Finally: report bugs and make feature requests on our Issues tracker. You can also join the Slack group — its not just for coders. We also need testers, writers, and, especially, people who are willing to talk things over. Most of software development is just making decisions, and we appreciate all the help we can get!
Or: skip helping us, and, instead, help people who need help more than we do. Those people should not be hard to find.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Technotes/Images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -16,4 +16,8 @@
## Contributing
[Contributing](../CONTRIBUTING.md)
[Coding Guidelines](CodingGuidelines.md)
[Branching Strategy](BranchingStrategy.md)

39
Technotes/Reruns.md Normal file
View File

@ -0,0 +1,39 @@
# Why Reruns Happen
Sometimes you might see a new article in a feed that youd swear youve already read. And maybe you can even see, in NetNewsWire, what looks like another copy of that same exact article, with no changes.
Heres the thing to know: if the article really was the exact same in every respect, NetNewsWire would see that. Its super-easy for a computer to tell that some data is the exact same as some other data.
When its not really the exact same, thats where the problem comes in.
Here are some reasons this situation can happen:
## A blog changes its blog engine
If someone switches from (for instance) Ghost to WordPress, then the code that creates that feeds will be different. And that code will make a different choice for the unique ID for each article in the feed.
Those unique IDs are critical: theyre how NetNewsWire identifies an article. If an article appears with a new unique ID, then NetNewsWire treats it like a new article.
In this situation, youll often see that you get a bunch of reruns for a given feed all at once. Youll get 10 or 20 or whatever.
This is by far the most common cause of reruns.
## A feed that lacks unique IDs does something weird
This is quite a bit less common. There are some feeds that dont have unique IDs, which means NetNewsWire has to use some combination of other article metadata to identify articles.
That metadata could change just enough to throw NetNewsWire off. This is rare, but it can happen.
## A feed just has terrible bugs
Weve seen feeds that create a different unique ID for each article every time you fetch the feed, which results in reruns every single time. Weve seen feeds that use the same unique ID for every article in the feed, even — which goes against the very idea of unique IDs!
Some feeds just have bugs, and weird, unpredictable things happen.
NetNewsWire is designed to be resistant to that, and it does a good job — but we havent anticipated every odd case.
However, this is the most rare cause of reruns. The most common cause is, by far, the first one: the feed is now being generated by different software.
## Reporting Bugs
If you have a feed that keeps showing reruns (as opposed to once, when a blog changes its blogging system), please do report a bug, either on our [Issues Tracker](https://github.com/brentsimmons/NetNewsWire/issues) or on the [Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc).

View File

@ -11,6 +11,7 @@ import Account
import Articles
import RSCore
import RSTree
import SwiftUI
class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunner {
@ -393,13 +394,8 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
@IBAction func settings(_ sender: UIBarButtonItem) {
let settingsNavViewController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController
settingsNavViewController.modalPresentationStyle = .formSheet
let settingsViewController = settingsNavViewController.topViewController as! SettingsViewController
settingsViewController.presentingParentController = self
self.present(settingsNavViewController, animated: true)
let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsView.ViewModel()))
self.present(settings, animated: true)
}
@ -526,14 +522,23 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
else {
return
}
navState.beginUpdates()
runCommand(deleteCommand)
navState.rebuildShadowTable()
tableView.deleteRows(at: [indexPath], with: .automatic)
var deleteIndexPaths = [indexPath]
if navState.isExpanded(deleteNode) {
for i in 0..<deleteNode.numberOfChildNodes {
deleteIndexPaths.append(IndexPath(row: indexPath.row + 1 + i, section: indexPath.section))
}
}
navState.endUpdates()
pushUndoableCommand(deleteCommand)
navState.beginUpdates()
deleteCommand.perform {
self.navState.treeController.rebuild()
self.navState.rebuildShadowTable()
self.tableView.deleteRows(at: deleteIndexPaths, with: .automatic)
self.navState.endUpdates()
}
}

View File

@ -28,7 +28,7 @@ extension MasterTimelineCellLayout {
var r = CGRect.zero
r.size = CGSize(width: MasterTimelineDefaultCellLayout.unreadCircleDimension, height: MasterTimelineDefaultCellLayout.unreadCircleDimension)
r.origin.x = point.x
r.origin.y = point.y + 8
r.origin.y = point.y + 4
return r
}
@ -38,14 +38,15 @@ extension MasterTimelineCellLayout {
r.size.width = MasterTimelineDefaultCellLayout.starDimension
r.size.height = MasterTimelineDefaultCellLayout.starDimension
r.origin.x = floor(point.x - ((MasterTimelineDefaultCellLayout.starDimension - MasterTimelineDefaultCellLayout.unreadCircleDimension) / 2.0))
r.origin.y = point.y + 5
r.origin.y = point.y + 2
return r
}
static func rectForAvatar(_ point: CGPoint) -> CGRect {
var r = CGRect.zero
r.size = MasterTimelineDefaultCellLayout.avatarSize
r.origin = point
r.origin.x = point.x
r.origin.y = point.y + 4
return r
}

View File

@ -17,7 +17,7 @@ struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout {
static let unreadCircleDimension = CGFloat(integerLiteral: 12)
static let unreadCircleMarginRight = CGFloat(integerLiteral: 8)
static let starDimension = CGFloat(integerLiteral: 13)
static let starDimension = CGFloat(integerLiteral: 16)
static let avatarSize = CGSize(width: 48.0, height: 48.0)
static let avatarMarginRight = CGFloat(integerLiteral: 8)

View File

@ -17,8 +17,6 @@ class MasterTimelineTableViewCell: UITableViewCell {
private let dateView = MasterTimelineTableViewCell.singleLineUILabel()
private let feedNameView = MasterTimelineTableViewCell.singleLineUILabel()
private var layout: MasterTimelineCellLayout?
private lazy var avatarImageView: UIImageView = {
let imageView = NonIntrinsicImageView(image: AppAssets.feedImage)
imageView.contentMode = .scaleAspectFit
@ -47,29 +45,25 @@ class MasterTimelineTableViewCell: UITableViewCell {
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
if layout == nil {
layout = updatedLayout()
}
return CGSize(width: bounds.width, height: layout!.height)
let layout = updatedLayout(width: size.width)
return CGSize(width: size.width, height: layout.height)
}
override func layoutSubviews() {
super.layoutSubviews()
if layout == nil {
layout = updatedLayout()
}
let layout = updatedLayout(width: bounds.width)
unreadIndicatorView.setFrameIfNotEqual(layout!.unreadIndicatorRect)
starView.setFrameIfNotEqual(layout!.starRect)
avatarImageView.setFrameIfNotEqual(layout!.avatarImageRect)
setFrame(for: titleView, rect: layout!.titleRect)
setFrame(for: summaryView, rect: layout!.summaryRect)
feedNameView.setFrameIfNotEqual(layout!.feedNameRect)
dateView.setFrameIfNotEqual(layout!.dateRect)
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
starView.setFrameIfNotEqual(layout.starRect)
avatarImageView.setFrameIfNotEqual(layout.avatarImageRect)
setFrame(for: titleView, rect: layout.titleRect)
setFrame(for: summaryView, rect: layout.summaryRect)
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
dateView.setFrameIfNotEqual(layout.dateRect)
separatorInset = layout!.separatorInsets
separatorInset = layout.separatorInsets
}
@ -137,11 +131,11 @@ private extension MasterTimelineTableViewCell {
accessoryView = UIImageView(image: AppAssets.chevronRightImage)
}
func updatedLayout() -> MasterTimelineCellLayout {
func updatedLayout(width: CGFloat) -> MasterTimelineCellLayout {
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
return MasterTimelineAccessibilityCellLayout(width: bounds.width, insets: safeAreaInsets, cellData: cellData)
return MasterTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
} else {
return MasterTimelineDefaultCellLayout(width: bounds.width, insets: safeAreaInsets, cellData: cellData)
return MasterTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
}
}
@ -234,7 +228,6 @@ private extension MasterTimelineTableViewCell {
}
func updateSubviews() {
layout = nil
updateTitleView()
updateSummaryView()
updateDateView()

View File

@ -10,6 +10,7 @@
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
"template-rendering-intent" : "template",
"preserves-vector-representation" : true
}
}

View File

@ -10,6 +10,7 @@
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
"template-rendering-intent" : "template",
"preserves-vector-representation" : true
}
}

View File

@ -0,0 +1,40 @@
//
// SettingsAccountLabelView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
struct SettingsAccountLabelView : View {
let accountImage: String
let accountLabel: String
var body: some View {
HStack {
Spacer()
HStack {
Image(accountImage)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(height: 32)
Text(verbatim: accountLabel).font(.title)
}
.layoutPriority(1)
Spacer()
}
.foregroundColor(.primary)
}
}
#if DEBUG
struct SettingsAccountLabelView_Previews : PreviewProvider {
static var previews: some View {
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device")
.previewLayout(.fixed(width: 300, height: 44))
}
}
#endif

View File

@ -0,0 +1,31 @@
//
// SettingsAddAccountView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
struct SettingsAddAccountView : View {
var body: some View {
List {
PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName),
destination: SettingsLocalAccountView(name: "")).padding(.all, 4)
PresentationButton(SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin"),
destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())).padding(.all, 4)
}
.listStyle(.grouped)
.navigationBarTitle(Text("Add Account"), displayMode: .inline)
}
}
#if DEBUG
struct AddAccountView_Previews : PreviewProvider {
static var previews: some View {
SettingsAddAccountView()
}
}
#endif

View File

@ -0,0 +1,114 @@
//
// SettingsDetailAccountView.swift
// NetNewsWire
//
// Created by Maurice Parker on 6/13/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
import Combine
import Account
struct SettingsDetailAccountView : View {
@ObjectBinding var viewModel: ViewModel
@State private var verifyDelete = false
var body: some View {
List {
Section {
HStack {
Text("Name")
Divider()
TextField($viewModel.name, placeholder: Text("(Optional)"))
}
Toggle(isOn: $viewModel.isActive) {
Text("Active")
}
}
Section {
HStack {
Spacer()
Button(action: {
}) {
Text("Credentials")
}
Spacer()
}
}
if viewModel.isDeletable {
Section {
HStack {
Spacer()
Button(action: {
self.verifyDelete = true
}) {
Text("Delete Account")
.foregroundColor(.red)
}
.presentation($verifyDelete) {
Alert(title: Text("Are you sure you want to delete \"\(viewModel.nameForDisplay)\"?"),
primaryButton: Alert.Button.default(Text("Delete"), onTrigger: { self.viewModel.delete() }),
secondaryButton: Alert.Button.cancel())
}
Spacer()
}
}
}
}
.listStyle(.grouped)
.navigationBarTitle(Text(verbatim: viewModel.nameForDisplay), displayMode: .inline)
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<ViewModel, Never>()
let account: Account
init(_ account: Account) {
self.account = account
}
var nameForDisplay: String {
account.nameForDisplay
}
var name: String {
get {
account.name ?? ""
}
set {
account.name = newValue.isEmpty ? nil : newValue
didChange.send(self)
}
}
var isActive: Bool {
get {
account.isActive
}
set {
account.isActive = newValue
didChange.send(self)
}
}
var isDeletable: Bool {
return AccountManager.shared.defaultAccount != account
}
func delete() {
AccountManager.shared.deleteAccount(account)
}
}
}
#if DEBUG
struct SettingsDetailAccountView_Previews : PreviewProvider {
static var previews: some View {
let viewModel = SettingsDetailAccountView.ViewModel(AccountManager.shared.defaultAccount)
return SettingsDetailAccountView(viewModel: viewModel)
}
}
#endif

View File

@ -0,0 +1,150 @@
//
// SettingsFeedbinAccountView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
import Combine
import Account
import RSWeb
struct SettingsFeedbinAccountView : View {
@Environment(\.isPresented) private var isPresented
@ObjectBinding var viewModel: ViewModel
@State var busy: Bool = false
@State var error: Text = Text("")
var account: Account? = nil
var body: some View {
NavigationView {
List {
Section(header:
SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin").padding()
) {
HStack {
Spacer()
TextField($viewModel.email, placeholder: Text("Email"))
.textContentType(.username)
Spacer()
}
HStack {
Spacer()
SecureField($viewModel.password, placeholder: Text("Password"))
Spacer()
}
}
Section(footer:
HStack {
Spacer()
error.color(.red)
Spacer()
}
) {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
Text("Add Account")
}
.disabled(!viewModel.isValid)
Spacer()
}
}
}
.disabled(busy)
.listStyle(.grouped)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading:
Button(action: { self.dismiss() }) { Text("Cancel") }
)
}
}
private func addAccount() {
busy = true
let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces)
let credentials = Credentials.basic(username: emailAddress, password: viewModel.password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.busy = false
switch result {
case .success(let authenticated):
if authenticated {
var newAccount = false
let workAccount: Account
if self.account == nil {
workAccount = AccountManager.shared.createAccount(type: .feedbin)
newAccount = true
} else {
workAccount = self.account!
}
do {
do {
try workAccount.removeBasicCredentials()
} catch {}
try workAccount.storeCredentials(credentials)
if newAccount {
workAccount.refreshAll() { result in }
}
self.dismiss()
} catch {
self.error = Text("Keychain error while storing credentials.")
}
} else {
self.error = Text("Invalid email/password combination.")
}
case .failure:
self.error = Text("Network error. Try again later.")
}
}
}
private func dismiss() {
isPresented?.value = false
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<ViewModel, Never>()
var email: String = "" {
didSet {
didChange.send(self)
}
}
var password: String = "" {
didSet {
didChange.send(self)
}
}
var isValid: Bool {
return !email.isEmpty && !password.isEmpty
}
}
}
#if DEBUG
struct SettingsFeedbinAccountView_Previews : PreviewProvider {
static var previews: some View {
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())
}
}
#endif

View File

@ -0,0 +1,62 @@
//
// SettingsLocalAccountView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
struct SettingsLocalAccountView : View {
@Environment(\.isPresented) private var isPresented
@State var name: String
var body: some View {
NavigationView {
List {
Section(header:
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName).padding()
) {
HStack {
Spacer()
TextField($name, placeholder: Text("Name (Optional)"))
Spacer()
}
}
Section {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
Text("Add Account")
}
Spacer()
}
}
}
.listStyle(.grouped)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } )
}
}
private func addAccount() {
let account = AccountManager.shared.createAccount(type: .onMyMac)
account.name = name
dismiss()
}
private func dismiss() {
isPresented?.value = false
}
}
#if DEBUG
struct SettingsLocalAccountView_Previews : PreviewProvider {
static var previews: some View {
SettingsLocalAccountView(name: "")
}
}
#endif

View File

@ -0,0 +1,159 @@
//
// SettingsView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/11/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
import Combine
import Account
struct SettingsView : View {
@ObjectBinding var viewModel: ViewModel
var body: some View {
NavigationView {
List {
Section(header: Text("ACCOUNTS")) {
ForEach(viewModel.accounts.identified(by: \.self)) { account in
NavigationButton(destination: SettingsDetailAccountView(viewModel: SettingsDetailAccountView.ViewModel(account)), isDetail: false) {
Text(verbatim: account.nameForDisplay)
}
}
NavigationButton(destination: SettingsAddAccountView(), isDetail: false) {
Text("Add Account")
}
}
Section(header: Text("ABOUT")) {
Text("About NetNewsWire")
Button(action: {
UIApplication.shared.open(URL(string: "https://ranchero.com/netnewswire/")!, options: [:])
}) {
Text("Website")
}
Button(action: {
UIApplication.shared.open(URL(string: "https://github.com/brentsimmons/NetNewsWire")!, options: [:])
}) {
Text("Github Repository")
}
Button(action: {
UIApplication.shared.open(URL(string: "https://github.com/brentsimmons/NetNewsWire/issues")!, options: [:])
}) {
Text("Bug Tracker")
}
Button(action: {
UIApplication.shared.open(URL(string: "https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes")!, options: [:])
}) {
Text("Technotes")
}
Text("Add NetNewsWire News Feed")
}
.foregroundColor(.primary)
Section(header: Text("TIMELINE")) {
Toggle(isOn: $viewModel.sortOldestToNewest) {
Text("Sort Oldest to Newest")
}
Stepper(value: $viewModel.timelineNumberOfLines, in: 2...6) {
Text("Number of Text Lines: \(viewModel.timelineNumberOfLines)")
}
}
Section(header: Text("DATABASE")) {
Picker(selection: $viewModel.refreshInterval, label: Text("Refresh Interval")) {
ForEach(RefreshInterval.allCases.identified(by: \.self)) { interval in
Text(interval.description()).tag(interval)
}
}
Text("Import Subscriptions...")
Text("Export Subscriptions...")
}
}
.listStyle(.grouped)
.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<ViewModel, Never>()
init() {
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
}
var accounts: [Account] {
get {
return AccountManager.shared.sortedAccounts
}
set {
}
}
var sortOldestToNewest: Bool {
get {
return AppDefaults.timelineSortDirection == .orderedDescending
}
set {
if newValue == true {
AppDefaults.timelineSortDirection = .orderedDescending
} else {
AppDefaults.timelineSortDirection = .orderedAscending
}
didChange.send(self)
}
}
var timelineNumberOfLines: Int {
get {
return AppDefaults.timelineNumberOfLines
}
set {
AppDefaults.timelineNumberOfLines = newValue
didChange.send(self)
}
}
var refreshInterval: RefreshInterval {
get {
return AppDefaults.refreshInterval
}
set {
AppDefaults.refreshInterval = newValue
didChange.send(self)
}
}
@objc func accountsDidChange(_ notification: Notification) {
didChange.send(self)
}
@objc func displayNameDidChange(_ notification: Notification) {
didChange.send(self)
}
}
}
#if DEBUG
struct SettingsView_Previews : PreviewProvider {
static var previews: some View {
SettingsView(viewModel: SettingsView.ViewModel())
}
}
#endif

View File

@ -10,7 +10,7 @@ import Account
import UIKit
protocol AddAccountDismissDelegate: UIViewController {
func dismiss(_ viewController: UIViewController)
func dismiss()
}
class AddAccountViewController: UITableViewController, AddAccountDismissDelegate {
@ -27,11 +27,13 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
switch indexPath.row {
case 0:
let navController = UIStoryboard.settings.instantiateViewController(withIdentifier: "AddLocalAccountNavigationViewController") as! UINavigationController
navController.modalPresentationStyle = .currentContext
let addViewController = navController.topViewController as! AddLocalAccountViewController
addViewController.delegate = self
present(navController, animated: true)
case 1:
let navController = UIStoryboard.settings.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
navController.modalPresentationStyle = .currentContext
let addViewController = navController.topViewController as! FeedbinAccountViewController
addViewController.delegate = self
present(navController, animated: true)
@ -40,8 +42,8 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
}
}
func dismiss(_ viewController: UIViewController) {
viewController.dismiss(animated: true, completion: nil)
func dismiss() {
navigationController?.popViewController(animated: false)
}
}

View File

@ -25,13 +25,15 @@ class AddLocalAccountViewController: UIViewController {
}
@IBAction func cancel(_ sender: Any) {
delegate?.dismiss(self)
dismiss(animated: true, completion: nil)
delegate?.dismiss()
}
@IBAction func addAccountTapped(_ sender: Any) {
let account = AccountManager.shared.createAccount(type: .onMyMac)
account.name = nameTextField.text
delegate?.dismiss(self)
dismiss(animated: true, completion: nil)
delegate?.dismiss()
}
}

View File

@ -37,15 +37,26 @@ class DetailAccountViewController: UITableViewController {
extension DetailAccountViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
guard let account = account else { return 0 }
if account == AccountManager.shared.defaultAccount {
return 1
} else if account.type == .onMyMac {
return 2
} else {
return super.numberOfSections(in: tableView)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
let cell: UITableViewCell
if indexPath.section == 1, let account = account, account.type == .onMyMac {
cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2))
} else {
cell = super.tableView(tableView, cellForRowAt: indexPath)
}
let bgView = UIView()
bgView.backgroundColor = AppAssets.selectionBackgroundColor
@ -54,7 +65,7 @@ extension DetailAccountViewController {
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
if indexPath.section == 1 {
if indexPath.section > 0 {
return true
}
@ -62,8 +73,19 @@ extension DetailAccountViewController {
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.section == 1 {
deleteAccount()
if let account = account, account.type == .onMyMac {
if indexPath.section == 1 {
deleteAccount()
}
} else {
switch indexPath.section {
case 1:
credentials()
case 2:
deleteAccount()
default:
break
}
}
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
@ -73,6 +95,19 @@ extension DetailAccountViewController {
private extension DetailAccountViewController {
func credentials() {
guard let account = account else { return }
switch account.type {
case .feedbin:
let navController = UIStoryboard.settings.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
let addViewController = navController.topViewController as! FeedbinAccountViewController
addViewController.account = account
present(navController, animated: true)
default:
break
}
}
func deleteAccount() {
let title = NSLocalizedString("Delete Account", comment: "Delete Account")
let message = NSLocalizedString("Are you sure you want to delete this account? This can not be undone.", comment: "Delete Account")

View File

@ -16,7 +16,7 @@ class FeedbinAccountViewController: UIViewController {
@IBOutlet weak var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var addAccountButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var errorMessageLabel: UILabel!
@ -31,18 +31,22 @@ class FeedbinAccountViewController: UIViewController {
passwordTextField.delegate = self
if let account = account, let credentials = try? account.retrieveBasicCredentials() {
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
if case .basic(let username, let password) = credentials {
emailTextField.text = username
passwordTextField.text = password
}
} else {
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Update Credentials"), for: .normal)
}
}
@IBAction func cancel(_ sender: Any) {
delegate?.dismiss(self)
dismiss(animated: true, completion: nil)
delegate?.dismiss()
}
@IBAction func addAccountTapped(_ sender: Any) {
@IBAction func action(_ sender: Any) {
self.errorMessageLabel.text = nil
guard emailTextField.text != nil && passwordTextField.text != nil else {
@ -56,8 +60,7 @@ class FeedbinAccountViewController: UIViewController {
// When you fill in the email address via auto-complete it adds extra whitespace
let emailAddress = emailTextField.text?.trimmingCharacters(in: .whitespaces)
let credentials = Credentials.basic(username: emailAddress ?? "", password: passwordTextField.text ?? "")
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in
guard let self = self else { return }
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.stopAnimtatingActivityIndicator()
self.enableNavigation()
@ -73,7 +76,9 @@ class FeedbinAccountViewController: UIViewController {
do {
try self.account?.removeBasicCredentials()
do {
try self.account?.removeBasicCredentials()
} catch {}
try self.account?.storeCredentials(credentials)
if newAccount {
@ -87,7 +92,8 @@ class FeedbinAccountViewController: UIViewController {
}
}
self.delegate?.dismiss(self)
self.dismiss(animated: true, completion: nil)
self.delegate?.dismiss()
} catch {
self.errorMessageLabel.text = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
}
@ -103,12 +109,12 @@ class FeedbinAccountViewController: UIViewController {
private func enableNavigation() {
self.cancelBarButtonItem.isEnabled = true
self.addAccountButton.isEnabled = true
self.actionButton.isEnabled = true
}
private func disableNavigation() {
cancelBarButtonItem.isEnabled = false
addAccountButton.isEnabled = false
actionButton.isEnabled = false
}
private func startAnimatingActivityIndicator() {

View File

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="9cW-lu-HoC">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14810.11" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="9cW-lu-HoC">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.13"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<capability name="iOS 13.0 system colors" minToolsVersion="11.0"/>
</dependencies>
<scenes>
<!--Settings-->
@ -25,11 +23,11 @@
<rect key="frame" x="0.0" y="55.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XHc-rQ-7FK" id="nmL-EM-Bsi">
<rect key="frame" x="0.0" y="0.0" width="376" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="382.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Add Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="6sn-wY-hHH">
<rect key="frame" x="20" y="0.0" width="356" height="43.5"/>
<rect key="frame" x="20" y="0.0" width="354.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -46,11 +44,11 @@
<rect key="frame" x="0.0" y="155.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="376" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="382.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="About NetNewsWire" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="2o6-8W-nyK">
<rect key="frame" x="20" y="0.0" width="356" height="43.5"/>
<rect key="frame" x="20" y="0.0" width="354.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -63,11 +61,11 @@
<rect key="frame" x="0.0" y="199.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="43.5"/>
<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="Website" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="lOk-Dh-GfZ">
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -80,11 +78,11 @@
<rect key="frame" x="0.0" y="243.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="43.5"/>
<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="Github Repository" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="TEA-EG-V6d">
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -97,11 +95,11 @@
<rect key="frame" x="0.0" y="287.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="43.5"/>
<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="Bug Tracker" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="Q9a-Pi-uCc">
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -114,11 +112,11 @@
<rect key="frame" x="0.0" y="331.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="43.5"/>
<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="Technotes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="dWz-1o-EpJ">
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -131,11 +129,11 @@
<rect key="frame" x="0.0" y="375.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="43.5"/>
<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="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -152,7 +150,7 @@
<rect key="frame" x="0.0" y="475.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="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sort Oldest to Newest" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c9W-IF-u6i">
@ -182,7 +180,7 @@
<rect key="frame" x="0.0" y="519.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="air-4O-D7p" id="76S-R5-xwM">
<rect key="frame" x="0.0" y="0.0" width="376" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="382.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Number of Text Lines" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="B5l-Qp-6Gm">
@ -193,7 +191,7 @@
<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="u7B-tj-cw8">
<rect key="frame" x="332" y="12" width="44" height="20.5"/>
<rect key="frame" x="330.5" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -210,7 +208,7 @@
<rect key="frame" x="0.0" y="619.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="376" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="382.5" 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">
@ -221,7 +219,7 @@
<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">
<rect key="frame" x="332" y="12" width="44" height="20.5"/>
<rect key="frame" x="330.5" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -234,11 +232,11 @@
<rect key="frame" x="0.0" y="663.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="43.5"/>
<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="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -251,11 +249,11 @@
<rect key="frame" x="0.0" y="707.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="43.5"/>
<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="43.5"/>
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -301,7 +299,7 @@
<tableViewSection id="Zeb-b0-lsx">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5DR-M4-NFv">
<rect key="frame" x="0.0" y="35" width="414" height="44"/>
<rect key="frame" x="0.0" y="18" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5DR-M4-NFv" id="edh-bL-MIR">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
@ -333,10 +331,10 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="4n9-sW-i8D">
<rect key="frame" x="0.0" y="79" width="414" height="44"/>
<rect key="frame" x="0.0" y="61.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4n9-sW-i8D" id="h3v-g9-biw">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pvF-Ge-m4M">
@ -362,13 +360,37 @@
<tableViewSection id="yP0-5C-1Dy">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="oJc-7j-G4v">
<rect key="frame" x="0.0" y="159" width="414" height="44"/>
<rect key="frame" x="0.0" y="141.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oJc-7j-G4v" id="mV2-iL-ltS">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Delete Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OKd-Ps-a1K">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Credentials" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OKd-Ps-a1K">
<rect key="frame" x="163.5" y="11.5" width="87" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="OKd-Ps-a1K" firstAttribute="centerY" secondItem="mV2-iL-ltS" secondAttribute="centerY" id="ix2-SF-I7U"/>
<constraint firstItem="OKd-Ps-a1K" firstAttribute="centerX" secondItem="mV2-iL-ltS" secondAttribute="centerX" id="nZf-Eh-VXu"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="sVG-jo-N8H">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5r9-pq-th4">
<rect key="frame" x="0.0" y="221.5" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5r9-pq-th4" id="Z5N-KD-L3U">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Delete Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a8v-SL-W9q">
<rect key="frame" x="148" y="11.5" width="118" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
@ -376,8 +398,8 @@
</label>
</subviews>
<constraints>
<constraint firstItem="OKd-Ps-a1K" firstAttribute="centerY" secondItem="mV2-iL-ltS" secondAttribute="centerY" id="ix2-SF-I7U"/>
<constraint firstItem="OKd-Ps-a1K" firstAttribute="centerX" secondItem="mV2-iL-ltS" secondAttribute="centerX" id="nZf-Eh-VXu"/>
<constraint firstItem="a8v-SL-W9q" firstAttribute="centerX" secondItem="Z5N-KD-L3U" secondAttribute="centerX" id="F0E-Pt-dt3"/>
<constraint firstItem="a8v-SL-W9q" firstAttribute="centerY" secondItem="Z5N-KD-L3U" secondAttribute="centerY" id="TIC-BM-f0j"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
@ -410,10 +432,10 @@
<tableViewSection id="m3P-em-PgI">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="55" id="UFl-6I-ucw">
<rect key="frame" x="0.0" y="35" width="414" height="55"/>
<rect key="frame" x="0.0" y="18" width="414" height="55"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="UFl-6I-ucw" id="99i-Ge-guB">
<rect key="frame" x="0.0" y="0.0" width="414" height="54.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="55"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="iTt-HT-Ane">
@ -444,10 +466,10 @@
<inset key="separatorInset" minX="50" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="56" id="te1-L9-osf">
<rect key="frame" x="0.0" y="90" width="414" height="56"/>
<rect key="frame" x="0.0" y="73" width="414" height="56"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="te1-L9-osf" id="DgY-u7-DRO">
<rect key="frame" x="0.0" y="0.0" width="414" height="55.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="56"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="7dy-NH-2zV">
@ -658,10 +680,9 @@
<constraint firstAttribute="height" constant="48" id="8Vt-l1-eL1"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Add Account"/>
<state key="normal" title="Action"/>
<connections>
<action selector="addAccountTapped:" destination="byh-sg-6p5" eventType="touchUpInside" id="BFR-MI-1qW"/>
<action selector="addAccountTapped:" destination="lkT-rF-XV3" eventType="touchUpInside" id="YKl-dE-pVK"/>
<action selector="action:" destination="byh-sg-6p5" eventType="touchUpInside" id="ZQy-9g-TeU"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9QD-Wz-fqW">
@ -699,7 +720,7 @@
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="plain" id="L90-ti-E7I">
<view key="customView" contentMode="scaleToFill" id="xpt-lr-f2h">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="Pl1-lc-sIl">
@ -712,8 +733,8 @@
</barButtonItem>
</navigationItem>
<connections>
<outlet property="actionButton" destination="pv5-O6-P6Z" id="6Fm-3l-zj1"/>
<outlet property="activityIndicator" destination="Pl1-lc-sIl" id="hqg-mX-Yns"/>
<outlet property="addAccountButton" destination="pv5-O6-P6Z" id="DEh-oq-rnD"/>
<outlet property="cancelBarButtonItem" destination="xVt-VC-XFV" id="yBm-px-sgt"/>
<outlet property="emailTextField" destination="UiV-th-dQb" id="fCb-hg-AXa"/>
<outlet property="errorMessageLabel" destination="9QD-Wz-fqW" id="Kjo-73-Pgh"/>
@ -734,7 +755,7 @@
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="Ebv-d0-yty">
<rect key="frame" x="0.0" y="55.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="55.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Ebv-d0-yty" id="OVc-0q-4qT">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
@ -763,7 +784,7 @@
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="91W-kj-0Dw">
<rect key="frame" x="0.0" y="55.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="55.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="91W-kj-0Dw" id="AXy-Ti-xiS">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
@ -795,10 +816,10 @@
<tableViewSection id="apW-l0-gWz">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="62" id="zbQ-3A-f3f">
<rect key="frame" x="0.0" y="35" width="414" height="62"/>
<rect key="frame" x="0.0" y="18" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zbQ-3A-f3f" id="5Al-LU-dRg">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="NetNewsWire 5 for iOS" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UgA-s6-Vvg">
@ -817,14 +838,14 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="62" id="2DV-bO-vyT">
<rect key="frame" x="0.0" y="97" width="414" height="62"/>
<rect key="frame" x="0.0" y="80" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2DV-bO-vyT" id="YUn-eb-xyx">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5fQ-qz-qbW">
<rect key="frame" x="16" y="0.0" width="382" height="61.5"/>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5fQ-qz-qbW">
<rect key="frame" x="16" y="0.0" width="382" height="62"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
@ -843,14 +864,14 @@
<tableViewSection headerTitle="CREDITS" id="O1X-Iq-ibE">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="62" id="ZY4-id-Iia">
<rect key="frame" x="0.0" y="215" width="414" height="62"/>
<rect key="frame" x="0.0" y="198" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ZY4-id-Iia" id="IPw-QQ-LYI">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LiZ-Tv-tqb">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
@ -869,14 +890,14 @@
<tableViewSection headerTitle="ACKNOWLEDGMENTS" id="0Jq-ba-ylz">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="62" id="0Ge-wc-h3h">
<rect key="frame" x="0.0" y="333" width="414" height="62"/>
<rect key="frame" x="0.0" y="316" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0Ge-wc-h3h" id="tFb-3V-qIg">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YLf-rp-9nE">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
@ -895,14 +916,14 @@
<tableViewSection headerTitle="THANKS" id="Sgx-f1-hsT">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="62" id="bLz-mu-psL">
<rect key="frame" x="0.0" y="451" width="414" height="62"/>
<rect key="frame" x="0.0" y="434" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bLz-mu-psL" id="cxy-IZ-zFZ">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wTL-xl-1rK">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
@ -921,14 +942,14 @@
<tableViewSection headerTitle="DEDICATION" id="nbm-Bs-te6">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="62" id="aab-HD-ce6">
<rect key="frame" x="0.0" y="569" width="414" height="62"/>
<rect key="frame" x="0.0" y="552" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="aab-HD-ce6" id="6pH-5O-3V8">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DIp-6a-oPH">
<rect key="frame" x="0.0" y="0.0" width="414" height="61.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="62"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>

@ -1 +1 @@
Subproject commit 97dc785d171c5ffa151d7df73135641da8f5bc17
Subproject commit aa7107080e90d5be11ae54fd41ee4dd192468e30

@ -1 +1 @@
Subproject commit c38a779de5f935c4d041e89066a0d15d490f3776
Subproject commit 7b44768308dc6970ee78470d0ea1e5287badc2bc

@ -1 +1 @@
Subproject commit 93b481897d84849345daa965bd8e11860c9422e7
Subproject commit 032edf89b64ccbbfb6c05887b239a4bf81329b92

@ -1 +1 @@
Subproject commit b6cd62f04c90922dbc58f2907a8db6c33b96b50e
Subproject commit 350762104423aef963ca945d4388ca9d47f991ce

@ -1 +1 @@
Subproject commit 59685e50640cd4629294bf2c0d63193ffa4ccc74
Subproject commit 5d648e4050b700bb20fc7ae3303f087edcb3228f