Merge branch 'ios-release'

This commit is contained in:
Maurice Parker 2020-03-14 17:13:25 -05:00
commit 5b5eaf6bb7
39 changed files with 648 additions and 515 deletions

View File

@ -34,20 +34,11 @@ final class AccountMetadataFile {
}
func load() {
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
if let fileData = try? Data(contentsOf: readURL) {
let decoder = PropertyListDecoder()
account.metadata = (try? decoder.decode(AccountMetadata.self, from: fileData)) ?? AccountMetadata()
}
account.metadata.delegate = account
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
if let fileData = try? Data(contentsOf: fileURL) {
let decoder = PropertyListDecoder()
account.metadata = (try? decoder.decode(AccountMetadata.self, from: fileData)) ?? AccountMetadata()
}
account.metadata.delegate = account
}
func save() {
@ -56,20 +47,11 @@ final class AccountMetadataFile {
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
let data = try encoder.encode(account.metadata)
try data.write(to: writeURL)
} catch let error as NSError {
os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Save to disk coordination failed: %@.", error.localizedDescription)
do {
let data = try encoder.encode(account.metadata)
try data.write(to: fileURL)
} catch let error as NSError {
os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription)
}
}

View File

@ -72,6 +72,9 @@ private extension FeedSpecifier {
if urlString.caseInsensitiveContains("comments") {
score = score - 10
}
if urlString.caseInsensitiveContains("podcast") {
score = score - 10
}
if urlString.caseInsensitiveContains("rss") {
score = score + 5
}

View File

@ -46,24 +46,13 @@ final class OPMLFile {
func save() {
guard !account.isDeleted else { return }
let opmlDocumentString = opmlDocument()
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
try opmlDocumentString.write(to: writeURL, atomically: true, encoding: .utf8)
} catch let error as NSError {
os_log(.error, log: log, "OPML save to disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "OPML save to disk coordination failed: %@.", error.localizedDescription)
do {
try opmlDocumentString.write(to: fileURL, atomically: true, encoding: .utf8)
} catch let error as NSError {
os_log(.error, log: log, "OPML save to disk failed: %@.", error.localizedDescription)
}
}
}
@ -83,22 +72,11 @@ private extension OPMLFile {
func opmlFileData() -> Data? {
var fileData: Data? = nil
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
do {
fileData = try Data(contentsOf: readURL)
} catch {
// Commented out because its not an error on first run.
// TODO: make it so we know if its first run or not.
//NSApplication.shared.presentError(error)
os_log(.error, log: log, "OPML read from disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "OPML read from disk coordination failed: %@.", error.localizedDescription)
do {
fileData = try Data(contentsOf: fileURL)
} catch {
os_log(.error, log: log, "OPML read from disk failed: %@.", error.localizedDescription)
}
return fileData

View File

@ -34,20 +34,11 @@ final class WebFeedMetadataFile {
}
func load() {
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
if let fileData = try? Data(contentsOf: readURL) {
let decoder = PropertyListDecoder()
account.webFeedMetadata = (try? decoder.decode(Account.WebFeedMetadataDictionary.self, from: fileData)) ?? Account.WebFeedMetadataDictionary()
}
account.webFeedMetadata.values.forEach { $0.delegate = account }
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
if let fileData = try? Data(contentsOf: fileURL) {
let decoder = PropertyListDecoder()
account.webFeedMetadata = (try? decoder.decode(Account.WebFeedMetadataDictionary.self, from: fileData)) ?? Account.WebFeedMetadataDictionary()
}
account.webFeedMetadata.values.forEach { $0.delegate = account }
}
func save() {
@ -58,20 +49,11 @@ final class WebFeedMetadataFile {
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
let data = try encoder.encode(feedMetadata)
try data.write(to: writeURL)
} catch let error as NSError {
os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Save to disk coordination failed: %@.", error.localizedDescription)
do {
let data = try encoder.encode(feedMetadata)
try data.write(to: fileURL)
} catch let error as NSError {
os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription)
}
}

View File

@ -300,9 +300,7 @@ sup[id^='fn'] {
vertical-align: baseline;
}
a.footnote,
sup > a[href^='#fn'],
sup > div > a[href^='#fn'] {
a.footnote {
display: inline-block;
text-decoration: none;
padding: 0.05em 0.75em;
@ -331,17 +329,13 @@ sup > div > a[href^='#fn'] {
}
body a.footnote,
body a.footnote:visited,
.newsfoot-footnote-popover + a.footnote:hover,
sup > a[href^='#fn'],
sup > div > a[href^='#fn'] {
.newsfoot-footnote-popover + a.footnote:hover {
background: #aaa;
color: white;
transition: background-color 200ms ease-out;
}
a.footnote:hover,
.newsfoot-footnote-popover + a.footnote,
sup > a[href^='#fn']:hover,
sup > div > a[href^='#fn']:hover {
.newsfoot-footnote-popover + a.footnote {
background: #666;
transition: background-color 200ms ease-out;
}
@ -362,17 +356,13 @@ sup > div > a[href^='#fn']:hover {
}
body a.footnote,
body a.footnote:visited,
.newsfoot-footnote-popover + a.footnote:hover,
sup > a[href^='#fn'],
sup > div > a[href^='#fn'] {
.newsfoot-footnote-popover + a.footnote:hover {
background: #aaa;
color: white;
transition: background-color 200ms ease-out;
}
a.footnote:hover,
.newsfoot-footnote-popover + a.footnote,
sup > a[href^='#fn']:hover,
sup > div > a[href^='#fn']:hover {
.newsfoot-footnote-popover + a.footnote {
background: #666;
transition: background-color 200ms ease-out;
}

View File

@ -237,13 +237,15 @@
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; };
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */; };
51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; };
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; };
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; };
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */; };
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; };
51E36E71239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */; };
51E36E8C239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */; };
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
@ -1374,11 +1376,13 @@
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = "<group>"; };
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMigrator.swift; sourceTree = "<group>"; };
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; };
51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; };
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = "<group>"; };
51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = "<group>"; };
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = "<group>"; };
51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedSelectFolderTableViewCell.swift; sourceTree = "<group>"; };
51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddWebFeedSelectFolderTableViewCell.xib; sourceTree = "<group>"; };
51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
@ -1947,11 +1951,13 @@
isa = PBXGroup;
children = (
51C45264226508F600C03939 /* MasterFeedViewController.swift */,
51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */,
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */,
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */,
51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */,
51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */,
51CE1C0A23622006005548FC /* RefreshProgressView.swift */,
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */,
51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */,
51C45260226508F600C03939 /* Cell */,
);
path = MasterFeed;
@ -2009,6 +2015,7 @@
518651D9235621840078E021 /* ImageTransition.swift */,
5142192923522B5500E07E2C /* ImageViewController.swift */,
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */,
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */,
51AB8AB223B7F4C6008F147D /* WebViewController.swift */,
517630222336657E00E15FFF /* WebViewProvider.swift */,
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */,
@ -2643,7 +2650,6 @@
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */,
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */,
51B62E67233186730085F949 /* IconView.swift */,
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */,
51C4525D226508F600C03939 /* MasterFeed */,
51C4526D2265091600C03939 /* MasterTimeline */,
51C4527D2265092C00C03939 /* Article */,
@ -3996,14 +4002,15 @@
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */,
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */,
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */,
@ -4016,6 +4023,7 @@
FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */,
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */,
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */,
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */,

View File

@ -7,6 +7,9 @@
//
import Foundation
#if os(iOS)
import UIKit
#endif
import RSCore
import Articles
import Account
@ -49,27 +52,27 @@ struct ArticleRenderer {
static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style)
return (renderer.styleString(), renderer.articleHTML, renderer.title, renderer.baseURL ?? "")
return (renderer.articleCSS, renderer.articleHTML, renderer.title, renderer.baseURL ?? "")
}
static func multipleSelectionHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.multipleSelectionHTML, renderer.title, renderer.baseURL ?? "")
return (renderer.articleCSS, renderer.multipleSelectionHTML, renderer.title, renderer.baseURL ?? "")
}
static func loadingHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.loadingHTML, renderer.title, renderer.baseURL ?? "")
return (renderer.articleCSS, renderer.loadingHTML, renderer.title, renderer.baseURL ?? "")
}
static func noSelectionHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.noSelectionHTML, renderer.title, renderer.baseURL ?? "")
return (renderer.articleCSS, renderer.noSelectionHTML, renderer.title, renderer.baseURL ?? "")
}
static func noContentHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.noContentHTML, renderer.title, renderer.baseURL ?? "")
return (renderer.articleCSS, renderer.noContentHTML, renderer.title, renderer.baseURL ?? "")
}
}
@ -78,8 +81,7 @@ struct ArticleRenderer {
private extension ArticleRenderer {
private var articleHTML: String {
let body = try! MacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions())
return body
return try! MacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions())
}
private var multipleSelectionHTML: String {
@ -100,6 +102,15 @@ private extension ArticleRenderer {
private var noContentHTML: String {
return ""
}
private var articleCSS: String {
#if os(iOS)
let style = try! MacroProcessor.renderedText(withTemplate: styleString(), substitutions: styleSubstitutions())
return style
#else
return styleString()
#endif
}
static var defaultStyleSheet: String = {
let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
@ -233,6 +244,15 @@ private extension ArticleRenderer {
dateFormatter.timeStyle = timeStyle
return dateFormatter.string(from: date)
}
#if os(iOS)
func styleSubstitutions() -> [String: String] {
var d = [String: String]()
let bodyFont = UIFont.preferredFont(forTextStyle: .body)
d["font-size"] = String(describing: bodyFont.pointSize)
return d
}
#endif
}

View File

@ -35,7 +35,7 @@ function convertImgSrc() {
// Wrap tables in an overflow-x: auto; div
function wrapTables() {
var tables = document.querySelector("div.articleBody").getElementsByTagName("table");
var tables = document.querySelectorAll("div.articleBody table");
for (table of tables) {
var wrapper = document.createElement("div");
@ -99,6 +99,24 @@ function error() {
document.body.innerHTML = "error";
}
// Takes into account absoluting of URLs.
function isLocalFootnote(target) {
return target.hash.startsWith("#fn") && target.href.indexOf(document.baseURI) === 0;
}
function styleLocalFootnotes() {
for (elem of document.querySelectorAll("sup > a[href*='#fn'], sup > div > a[href*='#fn']")) {
if (isLocalFootnote(elem)) {
if (elem.className.indexOf("footnote") === -1) {
if (elem.className.length === 0)
elem.className = "footnote";
else
elem.className = elem.className + " " + "footnote";
}
}
}
}
function render(data, scrollY) {
document.getElementsByTagName("style")[0].innerHTML = data.style;
@ -117,6 +135,7 @@ function render(data, scrollY) {
stripStyles()
convertImgSrc()
flattenPreElements()
styleLocalFootnotes()
postRenderProcessing()
}

View File

@ -118,19 +118,13 @@
function idFromHash(target) {
if (!target.hash) return;
return target.hash.substring(1);
return decodeURIComponent(target.hash.substring(1));
}
/** @type {{fnref(target:HTMLAnchorElement): string|undefined}[]} */
const footnoteFormats = [
{ // Multimarkdown
fnref(target) {
if (!target.matches(".footnote")) return;
return idFromHash(target);
}
},
{// Daring Fireball
fnref(target) {
if (!target.matches("sup > a[href^='#fn'], sup > div > a[href^='#fn']")) return;
if (!target.matches(".footnote")) return;
return idFromHash(target);
}
}
@ -158,11 +152,11 @@
document.addEventListener("click", (ev) =>
{
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, footnotes .footnote-return")) return;
const hash = ev.target.hash;
if (!hash) return;
const fnref = document.getElementById(hash.substring(1));
if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return")) return;
const id = idFromHash(ev.target);
if (!id) return;
const fnref = document.getElementById(id);
window.scrollTo({ top: fnref.getBoundingClientRect().top + window.scrollY });
ev.preventDefault();
});

View File

@ -20,6 +20,10 @@ final class IconImage {
return image.isDark()
}()
lazy var isBright: Bool = {
return image.isBright()
}()
let image: RSImage
init(_ image: RSImage) {
@ -33,22 +37,48 @@ final class IconImage {
func isDark() -> Bool {
return self.cgImage(forProposedRect: nil, context: nil, hints: nil)?.isDark() ?? false
}
func isBright() -> Bool {
return self.cgImage(forProposedRect: nil, context: nil, hints: nil)?.isBright() ?? false
}
}
#else
extension UIImage {
func isDark() -> Bool {
return self.cgImage?.isDark() ?? false
}
func isBright() -> Bool {
return self.cgImage?.isBright() ?? false
}
}
#endif
fileprivate enum ImageLuminanceType {
case regular, bright, dark
}
extension CGImage {
func isBright() -> Bool {
guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else {
return false
}
return luminanceType == .bright
}
func isDark() -> Bool {
guard let imageData = self.dataProvider?.data else { return false }
guard let ptr = CFDataGetBytePtr(imageData) else { return false }
let length = CFDataGetLength(imageData)
guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else {
return false
}
return luminanceType == .dark
}
fileprivate func getLuminanceType(from data: CFData) -> ImageLuminanceType? {
guard let ptr = CFDataGetBytePtr(data) else {
return nil
}
let length = CFDataGetLength(data)
var pixelCount = 0
var totalLuminance = 0.0
@ -68,10 +98,12 @@ extension CGImage {
}
let avgLuminance = totalLuminance / Double(pixelCount)
if totalLuminance == 0 {
return true
if totalLuminance == 0 || avgLuminance < 40 {
return .dark
} else if avgLuminance > 180 {
return .bright
} else {
return avgLuminance < 40
return .regular
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>

View File

@ -78,17 +78,17 @@ class FeedWranglerAccountViewController: UITableViewController {
showError(NSLocalizedString("Username & password required.", comment: "Credentials Error"))
return
}
startAnimatingActivityIndicator()
disableNavigation()
resignFirstResponder()
toggleActivityIndicatorAnimation(visible: true)
setNavigationEnabled(to: false)
// When you fill in the email address via auto-complete it adds extra whitespace
let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
let credentials = Credentials(type: .feedWranglerBasic, username: trimmedEmail, secret: password)
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { result in
self.stopAnimtatingActivityIndicator()
self.enableNavigation()
self.toggleActivityIndicatorAnimation(visible: false)
self.setNavigationEnabled(to: true)
switch result {
case .success(let validatedCredentials):
@ -138,27 +138,21 @@ class FeedWranglerAccountViewController: UITableViewController {
}
private func showError(_ message: String) {
presentError(title: "Error", message: message)
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
}
private func enableNavigation() {
self.cancelBarButtonItem.isEnabled = true
self.actionButton.isEnabled = true
private func setNavigationEnabled(to value:Bool){
cancelBarButtonItem.isEnabled = value
actionButton.isEnabled = value
}
private func disableNavigation() {
cancelBarButtonItem.isEnabled = false
actionButton.isEnabled = false
}
private func startAnimatingActivityIndicator() {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
}
private func stopAnimtatingActivityIndicator() {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
private func toggleActivityIndicatorAnimation(visible value: Bool){
activityIndicator.isHidden = !value
if value {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
}
@ -166,7 +160,12 @@ class FeedWranglerAccountViewController: UITableViewController {
extension FeedWranglerAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
if textField == emailTextField {
passwordTextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
action(self)
}
return true
}

View File

@ -79,17 +79,16 @@ class FeedbinAccountViewController: UITableViewController {
showError(NSLocalizedString("Username & password required.", comment: "Credentials Error"))
return
}
startAnimatingActivityIndicator()
disableNavigation()
resignFirstResponder()
toggleActivityIndicatorAnimation(visible: true)
setNavigationEnabled(to: false)
// When you fill in the email address via auto-complete it adds extra whitespace
let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.stopAnimtatingActivityIndicator()
self.enableNavigation()
self.toggleActivityIndicatorAnimation(visible: false)
self.setNavigationEnabled(to: true)
switch result {
case .success(let credentials):
@ -138,27 +137,21 @@ class FeedbinAccountViewController: UITableViewController {
}
private func showError(_ message: String) {
presentError(title: "Error", message: message)
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
}
private func enableNavigation() {
self.cancelBarButtonItem.isEnabled = true
self.actionButton.isEnabled = true
private func setNavigationEnabled(to value:Bool){
cancelBarButtonItem.isEnabled = value
actionButton.isEnabled = value
}
private func disableNavigation() {
cancelBarButtonItem.isEnabled = false
actionButton.isEnabled = false
}
private func startAnimatingActivityIndicator() {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
}
private func stopAnimtatingActivityIndicator() {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
private func toggleActivityIndicatorAnimation(visible value: Bool){
activityIndicator.isHidden = !value
if value {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
}
@ -166,7 +159,12 @@ class FeedbinAccountViewController: UITableViewController {
extension FeedbinAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
if textField == emailTextField {
passwordTextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
action(self)
}
return true
}

View File

@ -1,24 +0,0 @@
//
// AccountMigrator.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/9/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
struct AccountMigrator {
static func migrate() {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let containerAccountsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
let containerAccountsFolder = containerAccountsURL!.appendingPathComponent("Accounts")
let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts")
try? FileManager.default.moveItem(at: containerAccountsFolder, to: documentAccountsFolder)
}
}

View File

@ -60,8 +60,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
super.init()
appDelegate = self
AccountMigrator.migrate()
let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts").absoluteString
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
@ -134,6 +132,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
// MARK: - API
func manualRefresh(errorHandler: @escaping (Error) -> ()) {
UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach {
$0.refreshInterface()
}
AccountManager.shared.refreshAll(errorHandler: errorHandler)
}
func resumeDatabaseProcessingIfNecessary() {
if AccountManager.shared.isSuspended {
AccountManager.shared.resumeAll()

View File

@ -44,7 +44,7 @@ class ArticleViewController: UIViewController {
var article: Article? {
didSet {
if let controller = currentWebViewController, controller.article != article {
controller.article = article
controller.setArticle(article)
DispatchQueue.main.async {
// You have to set the view controller to clear out the UIPageViewController child controller cache.
// You also have to do it in an async call or you will get a strange assertion error.
@ -103,7 +103,7 @@ class ArticleViewController: UIViewController {
view.bottomAnchor.constraint(equalTo: pageViewController.view.bottomAnchor)
])
let controller = createWebViewController(article)
let controller = createWebViewController(article, updateView: false)
if let state = restoreState {
controller.extractedArticle = state.extractedArticle
controller.isShowingExtractedArticle = state.isShowingExtractedArticle
@ -111,18 +111,14 @@ class ArticleViewController: UIViewController {
controller.windowScrollY = state.windowScrollY
}
articleExtractorButton.buttonState = controller.articleExtractorButtonState
pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)
self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)
if AppDefaults.articleFullscreenEnabled {
controller.hideBars()
}
updateUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if AppDefaults.articleFullscreenEnabled {
currentWebViewController?.hideBars()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
coordinator.isArticleViewControllerPending = false
@ -145,16 +141,17 @@ class ArticleViewController: UIViewController {
actionBarButtonItem.isEnabled = false
return
}
nextUnreadBarButtonItem.isEnabled = coordinator.isAnyUnreadAvailable
prevArticleBarButtonItem.isEnabled = coordinator.isPrevArticleAvailable
nextArticleBarButtonItem.isEnabled = coordinator.isNextArticleAvailable
articleExtractorButton.isEnabled = true
readBarButtonItem.isEnabled = true
starBarButtonItem.isEnabled = true
actionBarButtonItem.isEnabled = true
let permalinkPresent = article.preferredLink != nil
articleExtractorButton.isEnabled = permalinkPresent
actionBarButtonItem.isEnabled = permalinkPresent
if article.status.read {
readBarButtonItem.image = AppAssets.circleOpenImage
readBarButtonItem.isEnabled = article.isAvailableToMarkUnread
@ -332,11 +329,11 @@ extension ArticleViewController: UIPageViewControllerDelegate {
private extension ArticleViewController {
func createWebViewController(_ article: Article?) -> WebViewController {
func createWebViewController(_ article: Article?, updateView: Bool = true) -> WebViewController {
let controller = WebViewController()
controller.coordinator = coordinator
controller.delegate = self
controller.article = article
controller.setArticle(article, updateView: updateView)
return controller
}

View File

@ -10,9 +10,14 @@ import UIKit
class ImageViewController: UIViewController {
@IBOutlet weak var closeButton: UIButton!
@IBOutlet weak var shareButton: UIButton!
@IBOutlet weak var imageScrollView: ImageScrollView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var titleBackground: UIVisualEffectView!
@IBOutlet weak var titleLeading: NSLayoutConstraint!
@IBOutlet weak var titleTrailing: NSLayoutConstraint!
var image: UIImage!
var imageTitle: String?
@ -23,6 +28,8 @@ class ImageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
closeButton.imageView?.contentMode = .scaleAspectFit
imageScrollView.setup()
imageScrollView.imageScrollViewDelegate = self
imageScrollView.imageContentMode = .aspectFit
@ -30,6 +37,13 @@ class ImageViewController: UIViewController {
imageScrollView.display(image: image)
titleLabel.text = imageTitle ?? ""
layoutTitleLabel()
guard imageTitle != "" else {
titleBackground.removeFromSuperview()
return
}
titleBackground.layer.cornerRadius = 6
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@ -51,6 +65,13 @@ class ImageViewController: UIViewController {
dismiss(animated: true)
}
private func layoutTitleLabel(){
let width = view.frame.width
let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04)
titleLeading.constant += width * multiplier
titleTrailing.constant -= width * multiplier
titleLabel.layoutIfNeeded()
}
}
// MARK: ImageScrollViewDelegate

View File

@ -0,0 +1,81 @@
//
// PreloadedWebView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/25/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import WebKit
class PreloadedWebView: WKWebView {
private struct MessageName {
static let domContentLoaded = "domContentLoaded"
}
private var isReady: Bool = false
private var readyCompletion: ((PreloadedWebView) -> Void)?
init(articleIconSchemeHandler: ArticleIconSchemeHandler) {
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = false
preferences.javaScriptEnabled = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = .video
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
super.init(frame: .zero, configuration: configuration)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func preload() {
configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded)
loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL)
}
func ready(completion: @escaping (PreloadedWebView) -> Void) {
if isReady {
completeRequest(completion: completion)
} else {
readyCompletion = completion
}
}
}
// MARK: WKScriptMessageHandler
extension PreloadedWebView: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == MessageName.domContentLoaded {
isReady = true
if let completion = readyCompletion {
completeRequest(completion: completion)
readyCompletion = nil
}
}
}
}
// MARK: Private
private extension PreloadedWebView {
func completeRequest(completion: @escaping (PreloadedWebView) -> Void) {
isReady = false
configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded)
completion(self)
}
}

View File

@ -29,8 +29,8 @@ class WebViewController: UIViewController {
private var topShowBarsViewConstraint: NSLayoutConstraint!
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
private var webView: WKWebView? {
return view.subviews[0] as? WKWebView
private var webView: PreloadedWebView? {
return view.subviews[0] as? PreloadedWebView
}
private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self)
@ -57,18 +57,7 @@ class WebViewController: UIViewController {
weak var coordinator: SceneCoordinator!
weak var delegate: WebViewControllerDelegate?
var article: Article? {
didSet {
stopArticleExtractor()
if article?.webFeed?.isArticleExtractorAlwaysOn ?? false {
startArticleExtractor()
}
if article != oldValue {
windowScrollY = 0
loadWebView()
}
}
}
private(set) var article: Article?
let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 1.0)
var windowScrollY = 0
@ -114,6 +103,22 @@ class WebViewController: UIViewController {
// MARK: API
func setArticle(_ article: Article?, updateView: Bool = true) {
stopArticleExtractor()
if article != self.article {
self.article = article
if updateView {
if article?.webFeed?.isArticleExtractorAlwaysOn ?? false {
startArticleExtractor()
}
windowScrollY = 0
loadWebView()
}
}
}
func focus() {
webView?.becomeFirstResponder()
}
@ -375,8 +380,12 @@ extension WebViewController: UIScrollViewDelegate {
}
@objc func scrollPositionDidChange() {
webView?.evaluateJavaScript("window.scrollY") { (scrollY, _) in
self.windowScrollY = scrollY as? Int ?? 0
webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in
guard error == nil else { return }
let javascriptScrollY = scrollY as? Int ?? 0
// I don't know why this value gets returned sometimes, but it is in error
guard javascriptScrollY != 33554432 else { return }
self.windowScrollY = javascriptScrollY
}
}
@ -450,7 +459,7 @@ private extension WebViewController {
}
func recycleWebView(_ webView: WKWebView?) {
func recycleWebView(_ webView: PreloadedWebView?) {
guard let webView = webView else { return }
webView.removeFromSuperview()
@ -467,7 +476,7 @@ private extension WebViewController {
coordinator.webViewProvider.enqueueWebView(webView)
}
func renderPage(_ webView: WKWebView?) {
func renderPage(_ webView: PreloadedWebView?) {
guard let webView = webView else { return }
let style = ArticleStylesManager.shared.currentStyle
@ -498,8 +507,6 @@ private extension WebViewController {
render = "render(\(json), \(windowScrollY));"
}
windowScrollY = 0
webView.evaluateJavaScript(render)
}

View File

@ -11,140 +11,57 @@ import WebKit
/// WKWebView has an awful behavior of a flash to white on first load when in dark mode.
/// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle.
class WebViewProvider: NSObject, WKNavigationDelegate {
class WebViewProvider: NSObject {
private struct MessageName {
static let domContentLoaded = "domContentLoaded"
}
let articleIconSchemeHandler: ArticleIconSchemeHandler
private let minimumQueueDepth = 3
private let maximumQueueDepth = 6
private var queue = UIView()
private var waitingForFirstLoad = true
private var waitingCompletionHandler: ((WKWebView) -> ())?
init(coordinator: SceneCoordinator, viewController: UIViewController) {
articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator)
super.init()
viewController.view.insertSubview(queue, at: 0)
replenishQueueIfNeeded()
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
@objc func didEnterBackground() {
flushQueue()
}
@objc func willEnterForeground() {
replenishQueueIfNeeded()
}
func flushQueue() {
queue.subviews.forEach { $0.removeFromSuperview() }
waitingForFirstLoad = true
}
func replenishQueueIfNeeded() {
while queue.subviews.count < minimumQueueDepth {
let webView = WKWebView(frame: .zero, configuration: buildConfiguration())
enqueueWebView(webView)
enqueueWebView(PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler))
}
}
func dequeueWebView(completion: @escaping (WKWebView) -> ()) {
if waitingForFirstLoad {
waitingCompletionHandler = completion
} else {
completeRequest(completion: completion)
func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) {
if let webView = queue.subviews.last as? PreloadedWebView {
webView.ready { preloadedWebView in
preloadedWebView.removeFromSuperview()
self.replenishQueueIfNeeded()
completion(preloadedWebView)
}
return
}
assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.")
let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)
webView.ready { preloadedWebView in
self.replenishQueueIfNeeded()
completion(preloadedWebView)
}
}
func enqueueWebView(_ webView: WKWebView) {
func enqueueWebView(_ webView: PreloadedWebView) {
guard queue.subviews.count < maximumQueueDepth else {
return
}
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded)
queue.insertSubview(webView, at: 0)
webView.loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL)
}
// MARK: WKNavigationDelegate
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if waitingForFirstLoad {
waitingForFirstLoad = false
if let completion = waitingCompletionHandler {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.completeRequest(completion: completion)
self.waitingCompletionHandler = nil
}
}
}
webView.preload()
}
}
// MARK: WKScriptMessageHandler
extension WebViewProvider: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case MessageName.domContentLoaded:
if waitingForFirstLoad {
waitingForFirstLoad = false
if let completion = waitingCompletionHandler {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.completeRequest(completion: completion)
self.waitingCompletionHandler = nil
}
}
}
default:
return
}
}
}
// MARK: Private
private extension WebViewProvider {
func completeRequest(completion: @escaping (WKWebView) -> ()) {
if let webView = queue.subviews.last as? WKWebView {
webView.removeFromSuperview()
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded)
replenishQueueIfNeeded()
completion(webView)
return
}
assertionFailure("Creating WKWebView in \(#function); queue has run dry.")
let webView = WKWebView(frame: .zero)
completion(webView)
}
func buildConfiguration() -> WKWebViewConfiguration {
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = false
preferences.javaScriptEnabled = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = .video
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
return configuration
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -239,16 +239,16 @@
<viewLayoutGuide key="contentLayoutGuide" id="phv-DN-krZ"/>
<viewLayoutGuide key="frameLayoutGuide" id="NNU-C8-Fsz"/>
</scrollView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bHh-pW-oTS">
<rect key="frame" x="0.0" y="862" width="414" height="0.0"/>
<visualEffectView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bHh-pW-oTS">
<rect key="frame" x="-4" y="850" width="422" height="8"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="StS-kO-TuW">
<rect key="frame" x="0.0" y="0.0" width="414" height="0.0"/>
<rect key="frame" x="0.0" y="0.0" width="422" height="8"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<blurEffect style="systemUltraThinMaterial"/>
</visualEffectView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eMj-1g-3xm">
<rect key="frame" x="0.0" y="862" width="414" height="0.0"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eMj-1g-3xm">
<rect key="frame" x="0.0" y="854" width="414" height="0.0"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -265,14 +265,16 @@
<action selector="share:" destination="vO9-a3-Dnu" eventType="touchUpInside" id="m8y-Gs-YF1"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cXR-ll-xBx">
<button opaque="NO" clipsSubviews="YES" contentMode="center" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cXR-ll-xBx">
<rect key="frame" x="8" y="44" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="6kc-Gw-KbZ"/>
<constraint firstAttribute="width" constant="44" id="cBq-gs-WzN"/>
</constraints>
<color key="tintColor" name="primaryAccentColor"/>
<state key="normal" image="multiply.circle.fill" catalog="system"/>
<state key="normal" image="multiply.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large" weight="regular"/>
</state>
<connections>
<action selector="done:" destination="vO9-a3-Dnu" eventType="touchUpInside" id="tgd-ov-4Ft"/>
</connections>
@ -282,26 +284,31 @@
<constraints>
<constraint firstItem="RmY-a3-hUg" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="A0i-Hs-1Ac"/>
<constraint firstAttribute="bottom" secondItem="msG-pz-EKk" secondAttribute="bottom" id="AtA-bA-jDr"/>
<constraint firstItem="eMj-1g-3xm" firstAttribute="trailing" secondItem="mbY-02-GFL" secondAttribute="trailing" id="E7e-Lv-6ZA"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="bottom" secondItem="eMj-1g-3xm" secondAttribute="bottom" id="P3m-i2-3pJ"/>
<constraint firstItem="eMj-1g-3xm" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="mbY-02-GFL" secondAttribute="trailing" id="E7e-Lv-6ZA"/>
<constraint firstItem="eMj-1g-3xm" firstAttribute="centerX" secondItem="w6Q-vH-063" secondAttribute="centerX" id="H2b-IA-6hz"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="bottom" secondItem="eMj-1g-3xm" secondAttribute="bottom" constant="4" id="P3m-i2-3pJ"/>
<constraint firstAttribute="trailing" secondItem="msG-pz-EKk" secondAttribute="trailing" id="R49-qV-8nm"/>
<constraint firstItem="msG-pz-EKk" firstAttribute="leading" secondItem="w6Q-vH-063" secondAttribute="leading" id="XN1-xN-hYS"/>
<constraint firstItem="eMj-1g-3xm" firstAttribute="leading" secondItem="mbY-02-GFL" secondAttribute="leading" id="Xni-Dn-I3Z"/>
<constraint firstItem="eMj-1g-3xm" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="mbY-02-GFL" secondAttribute="leading" id="Xni-Dn-I3Z"/>
<constraint firstItem="mbY-02-GFL" firstAttribute="trailing" secondItem="RmY-a3-hUg" secondAttribute="trailing" constant="8" id="Zlz-lM-LV8"/>
<constraint firstItem="mbY-02-GFL" firstAttribute="bottom" secondItem="eMj-1g-3xm" secondAttribute="bottom" id="eaS-iG-yMv"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="leading" secondItem="eMj-1g-3xm" secondAttribute="leading" id="f8r-dq-Irr"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="top" secondItem="eMj-1g-3xm" secondAttribute="top" id="gTP-i5-FYQ"/>
<constraint firstItem="mbY-02-GFL" firstAttribute="bottom" secondItem="eMj-1g-3xm" secondAttribute="bottom" constant="8" id="eaS-iG-yMv"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="leading" secondItem="eMj-1g-3xm" secondAttribute="leading" constant="-4" id="f8r-dq-Irr"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="top" secondItem="eMj-1g-3xm" secondAttribute="top" constant="-4" id="gTP-i5-FYQ"/>
<constraint firstItem="msG-pz-EKk" firstAttribute="top" secondItem="w6Q-vH-063" secondAttribute="top" id="p1a-s0-wdK"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="trailing" secondItem="eMj-1g-3xm" secondAttribute="trailing" id="qB9-zk-5JN"/>
<constraint firstItem="bHh-pW-oTS" firstAttribute="trailing" secondItem="eMj-1g-3xm" secondAttribute="trailing" constant="4" id="qB9-zk-5JN"/>
<constraint firstItem="cXR-ll-xBx" firstAttribute="leading" secondItem="mbY-02-GFL" secondAttribute="leading" constant="8" id="vJs-LN-Ydd"/>
<constraint firstItem="cXR-ll-xBx" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="xVN-Qt-WYA"/>
</constraints>
<viewLayoutGuide key="safeArea" id="mbY-02-GFL"/>
</view>
<connections>
<outlet property="closeButton" destination="cXR-ll-xBx" id="sDG-49-joi"/>
<outlet property="imageScrollView" destination="msG-pz-EKk" id="dGi-M6-dcO"/>
<outlet property="shareButton" destination="RmY-a3-hUg" id="Z54-ah-WAI"/>
<outlet property="titleBackground" destination="bHh-pW-oTS" id="o2K-cY-90c"/>
<outlet property="titleLabel" destination="eMj-1g-3xm" id="6wF-IZ-fNw"/>
<outlet property="titleLeading" destination="Xni-Dn-I3Z" id="8Ik-la-Qkw"/>
<outlet property="titleTrailing" destination="E7e-Lv-6ZA" id="lGu-iv-C9W"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ZPN-tH-JAG" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>

View File

@ -24,8 +24,15 @@ final class IconView: UIView {
self.setNeedsLayout()
}
} else {
self.setNeedsLayout()
if self.iconImage?.isBright ?? false {
self.isDisconcernable = false
self.setNeedsLayout()
} else {
self.isDisconcernable = true
self.setNeedsLayout()
}
}
self.setNeedsLayout()
}
}
}

View File

@ -7,7 +7,6 @@
//
import UIKit
import RSCore
import RSTree
import Account

View File

@ -0,0 +1,39 @@
//
// MasterFeedDataSourceOperation.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/23/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
import RSTree
class MasterFeedDataSourceOperation: MainThreadOperation {
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "MasterFeedDataSourceOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private var dataSource: UITableViewDiffableDataSource<Node, Node>
private var snapshot: NSDiffableDataSourceSnapshot<Node, Node>
private var animating: Bool
init(dataSource: UITableViewDiffableDataSource<Node, Node>, snapshot: NSDiffableDataSourceSnapshot<Node, Node>, animating: Bool) {
self.dataSource = dataSource
self.snapshot = snapshot
self.animating = animating
}
func run() {
dataSource.apply(snapshot, animatingDifferences: animating) { [weak self] in
guard let self = self else { return }
self.operationDelegate?.operationDidComplete(self)
}
}
}

View File

@ -17,8 +17,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
@IBOutlet weak var filterButton: UIBarButtonItem!
private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var addNewItemButton: UIBarButtonItem!
private let operationQueue = MainThreadOperationQueue()
lazy var dataSource = makeDataSource()
var undoableCommands = [UndoableCommand]()
weak var coordinator: SceneCoordinator!
@ -27,7 +29,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return keyboardManager.keyCommands
}
var restoreSelection = false
override var canBecomeFirstResponder: Bool {
return true
}
@ -73,17 +74,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
updateUI()
super.viewWillAppear(animated)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// We have to delay the selection of the Feed until the subviews have been layed out
// so that the visible area is correct and we scroll correctly.
if restoreSelection {
restoreSelectionIfNecessary(adjustScroll: true)
restoreSelection = false
}
}
// MARK: Notifications
@ -111,16 +101,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
}
// Only do the reload of the node when absolutely necessary. It can stop programatic scrolling from
// completing if called to soon after a selectRow where scrolling is necessary. See discloseFeed.
if let node = node,
let indexPath = dataSource.indexPath(for: node),
let cell = tableView.cellForRow(at: indexPath) as? MasterFeedTableViewCell {
if cell.unreadCount != coordinator.unreadCountFor(node) {
self.reloadNode(node)
}
if let node = node, dataSource.indexPath(for: node) != nil {
self.reloadNode(node)
}
}
@ -144,10 +126,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
}
@objc func webFeedMetadataDidChange(_ note: Notification) {
reloadAllVisibleCells()
}
@objc func contentSizeCategoryDidChange(_ note: Notification) {
resetEstimatedRowHeight()
applyChanges(animated: false)
@ -440,11 +418,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
// This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl.
// If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) {
if AppDefaults.refreshClearsReadArticles {
self.coordinator.refreshTimeline(resetScroll: false)
}
}
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
}
}
@ -517,24 +491,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
func updateFeedSelection(animations: Animations) {
if dataSource.snapshot().numberOfItems > 0 {
if let indexPath = coordinator.currentFeedIndexPath {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
}
} else {
if animations.contains(.select) {
// This nasty bit of duct tape is because there is something, somewhere
// interrupting the deselection animation, which will leave the row selected.
// This seems to get it far enough away the problem that it always works.
DispatchQueue.main.async {
self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
}
} else {
self.tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
}
}
}
operationQueue.add(UpdateSelectionOperation(coordinator: coordinator, dataSource: dataSource, tableView: tableView, animations: animations))
}
func reloadFeeds(initialLoad: Bool, completion: (() -> Void)? = nil) {
@ -646,7 +603,7 @@ private extension MasterFeedViewController {
func reloadNode(_ node: Node) {
var snapshot = dataSource.snapshot()
snapshot.reloadItems([node])
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in
self?.restoreSelectionIfNecessary(adjustScroll: false)
}
}
@ -661,13 +618,22 @@ private extension MasterFeedViewController {
snapshot.appendItems(shadowTableNodes, toSection: sectionNode)
}
dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in
queueApply(snapshot: snapshot, animatingDifferences: animated) { [weak self] in
self?.restoreSelectionIfNecessary(adjustScroll: adjustScroll)
completion?()
}
}
func queueApply(snapshot: NSDiffableDataSourceSnapshot<Node, Node>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) {
let operation = MasterFeedDataSourceOperation(dataSource: dataSource, snapshot: snapshot, animating: animatingDifferences)
operation.completionBlock = { _ in
completion?()
}
operationQueue.add(operation)
}
func makeDataSource() -> UITableViewDiffableDataSource<Node, Node> {
func makeDataSource() -> MasterFeedDataSource {
let dataSource = MasterFeedDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
self?.configure(cell, node)
@ -774,7 +740,7 @@ private extension MasterFeedViewController {
private func reloadCells(_ nodes: [Node], completion: (() -> Void)? = nil) {
var snapshot = dataSource.snapshot()
snapshot.reloadItems(nodes)
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in
self?.restoreSelectionIfNecessary(adjustScroll: false)
completion?()
}

View File

@ -13,7 +13,6 @@ class RefreshProgressView: UIView {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var label: UILabel!
private lazy var progressWidthConstraint = progressView.widthAnchor.constraint(equalToConstant: 100.0)
override func awakeFromNib() {
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
@ -24,7 +23,7 @@ class RefreshProgressView: UIView {
} else {
updateRefreshLabel()
}
scheduleUpdateRefreshLabel()
}
@ -34,26 +33,26 @@ class RefreshProgressView: UIView {
func updateRefreshLabel() {
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
if Date() > accountLastArticleFetchEndTime.addingTimeInterval(60) {
let relativeDateTimeFormatter = RelativeDateTimeFormatter()
relativeDateTimeFormatter.dateTimeStyle = .named
let refreshed = relativeDateTimeFormatter.localizedString(for: accountLastArticleFetchEndTime, relativeTo: Date())
let localizedRefreshText = NSLocalizedString("Updated %@", comment: "Updated")
let refreshText = NSString.localizedStringWithFormat(localizedRefreshText as NSString, refreshed) as String
label.text = refreshText
} else {
label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
}
} else {
label.text = ""
}
}
@objc func progressDidChange(_ note: Notification) {
progressChanged()
}
@ -80,27 +79,26 @@ private extension RefreshProgressView {
let isInViewHierarchy = self.superview != nil
let progress = AccountManager.shared.combinedRefreshProgress
if progress.isComplete {
if isInViewHierarchy {
progressView.setProgress(1, animated: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.updateRefreshLabel()
self.label.isHidden = false
self.progressView.isHidden = true
self.progressWidthConstraint.isActive = false
if isInViewHierarchy {
self.progressView.setProgress(0, animated: true)
// Check that there are no pending downloads.
if (AccountManager.shared.combinedRefreshProgress.isComplete) {
self.updateRefreshLabel()
self.label.isHidden = false
self.progressView.isHidden = true
if self.superview != nil {
self.progressView.setProgress(0, animated: true)
}
}
}
} else {
label.isHidden = true
progressView.isHidden = false
progressWidthConstraint.isActive = true
if isInViewHierarchy {
progressView.setNeedsLayout()
progressView.layoutIfNeeded()
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
// Don't let the progress bar go backwards unless we need to go back more than 25%

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -14,13 +14,14 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<progressView hidden="YES" opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="gKH-fc-zh7" customClass="RoundedProgressView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="27.5" width="375" height="5"/>
<rect key="frame" x="137.5" y="27.5" width="100" height="5"/>
<constraints>
<constraint firstAttribute="height" constant="5" id="OCl-qi-owb"/>
<constraint firstAttribute="width" constant="100" id="v3Q-GE-krS"/>
</constraints>
</progressView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="07B-Zy-FCt">
<rect key="frame" x="0.0" y="30" width="375" height="0.0"/>
<rect key="frame" x="187.5" y="30" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -28,11 +29,13 @@
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="07B-Zy-FCt" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="6eL-GN-NIE"/>
<constraint firstItem="07B-Zy-FCt" firstAttribute="centerX" secondItem="vUN-kp-3ea" secondAttribute="centerX" id="01K-1E-PEm"/>
<constraint firstItem="07B-Zy-FCt" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="vUN-kp-3ea" secondAttribute="leading" id="6eL-GN-NIE"/>
<constraint firstItem="07B-Zy-FCt" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="QJ0-os-kZt"/>
<constraint firstAttribute="trailing" secondItem="gKH-fc-zh7" secondAttribute="trailing" id="SbS-0T-bdo"/>
<constraint firstItem="gKH-fc-zh7" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="V0P-ix-fa3"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="07B-Zy-FCt" secondAttribute="trailing" id="Zor-53-U98"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="gKH-fc-zh7" secondAttribute="trailing" id="SbS-0T-bdo"/>
<constraint firstItem="gKH-fc-zh7" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="iN0-l3-epB" secondAttribute="leading" id="V0P-ix-fa3"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="07B-Zy-FCt" secondAttribute="trailing" id="Zor-53-U98"/>
<constraint firstItem="gKH-fc-zh7" firstAttribute="centerX" secondItem="vUN-kp-3ea" secondAttribute="centerX" id="eX0-hg-5sb"/>
<constraint firstItem="gKH-fc-zh7" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="mnf-7m-knt"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>

View File

@ -0,0 +1,60 @@
//
// UpdateSelectionOperation.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/22/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
class UpdateSelectionOperation: MainThreadOperation {
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "UpdateSelectionOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private var coordinator: SceneCoordinator
private var dataSource: MasterFeedDataSource
private var tableView: UITableView
private var animations: Animations
init(coordinator: SceneCoordinator, dataSource: MasterFeedDataSource, tableView: UITableView, animations: Animations) {
self.coordinator = coordinator
self.dataSource = dataSource
self.tableView = tableView
self.animations = animations
}
func run() {
if dataSource.snapshot().numberOfItems > 0 {
if let indexPath = coordinator.currentFeedIndexPath {
CATransaction.begin()
CATransaction.setCompletionBlock {
self.operationDelegate?.operationDidComplete(self)
}
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
CATransaction.commit()
} else {
if animations.contains(.select) {
CATransaction.begin()
CATransaction.setCompletionBlock {
self.operationDelegate?.operationDidComplete(self)
}
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
CATransaction.commit()
} else {
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
self.operationDelegate?.operationDidComplete(self)
}
}
} else {
self.operationDelegate?.operationDidComplete(self)
}
}
}

View File

@ -89,7 +89,14 @@ struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout {
}
self.summaryRect = MasterTimelineDefaultCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
currentPoint.y = [self.titleRect, self.summaryRect].maxY()
var y = [self.titleRect, self.summaryRect].maxY()
if y == 0 {
y = iconImageRect.origin.y + iconImageRect.height
// Necessary calculation of either feed name or date since we are working with dynamic font-sizes
let tmp = MasterTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
y -= tmp.height
}
currentPoint.y = y
// Feed Name and Pub Date
self.dateRect = MasterTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)

View File

@ -137,11 +137,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl.
// If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) {
if AppDefaults.refreshClearsReadArticles {
self.coordinator.refreshTimeline(resetScroll: false)
}
}
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
}
}
@ -198,6 +194,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
updateUI()
}
func updateUI() {
refreshProgressView?.updateRefreshLabel()
updateTitleUnreadCount()
updateToolbar()
}
func hideSearch() {
navigationItem.searchController?.isActive = false
@ -545,8 +547,7 @@ private extension MasterTimelineViewController {
func configureToolbar() {
if coordinator.isThreePanelMode {
firstUnreadButton.isHidden = true
guard !coordinator.isThreePanelMode else {
return
}
@ -602,12 +603,6 @@ private extension MasterTimelineViewController {
}
func updateUI() {
refreshProgressView?.updateRefreshLabel()
updateTitleUnreadCount()
updateToolbar()
}
func setFilterButtonToActive() {
filterButton?.image = AppAssets.filterActiveImage
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
@ -621,6 +616,11 @@ private extension MasterTimelineViewController {
func updateToolbar() {
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
if coordinator.isRootSplitCollapsed {
firstUnreadButton.isHidden = false
} else {
firstUnreadButton.isHidden = true
}
}
func updateTitleUnreadCount() {

View File

@ -9,10 +9,10 @@
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "242",
"red" : "235",
"alpha" : "1.000",
"blue" : "242",
"green" : "242"
"blue" : "237",
"green" : "235"
}
}
},

View File

@ -1,17 +1,22 @@
:root {
font: -apple-system-body;
font-size: [[font-size]]px;
}
body {
margin-top: 3px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
margin-left: auto;
margin-right: auto;
padding-left: 20px;
padding-right: 20px;
word-wrap: break-word;
word-break: break-word;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
max-width: 44em;
}
a {
@ -75,7 +80,8 @@ body .headerTable {
border-bottom: 1px solid var(--header-table-border-color);
}
body .header {
font: -apple-system-body;
font: -apple-system-body;
font-size: [[font-size]]px;
color: var(--header-color);
}
body .header a:link, body .header a:visited {
@ -187,6 +193,11 @@ img, figure, iframe, div {
margin: 0 auto;
}
figure {
margin-bottom: 1em;
margin-top: 1em;
}
video {
width: 100% !important;
height: auto !important;
@ -345,9 +356,7 @@ sup[id^='fn'] {
vertical-align: baseline;
}
a.footnote,
sup > a[href^='#fn'],
sup > div > a[href^='#fn'] {
a.footnote {
display: inline-block;
text-decoration: none;
padding: 0.05em 0.75em;
@ -376,17 +385,13 @@ sup > div > a[href^='#fn'] {
}
body a.footnote,
body a.footnote:visited,
.newsfoot-footnote-popover + a.footnote:hover,
sup > a[href^='#fn'],
sup > div > a[href^='#fn'] {
.newsfoot-footnote-popover + a.footnote:hover {
background: #aaa;
color: white;
transition: background-color 200ms ease-out;
}
a.footnote:hover,
.newsfoot-footnote-popover + a.footnote,
sup > a[href^='#fn']:hover,
sup > div > a[href^='#fn']:hover {
.newsfoot-footnote-popover + a.footnote {
background: #666;
transition: background-color 200ms ease-out;
}
@ -407,17 +412,13 @@ sup > div > a[href^='#fn']:hover {
}
body a.footnote,
body a.footnote:visited,
.newsfoot-footnote-popover + a.footnote:hover,
sup > a[href^='#fn'],
sup > div > a[href^='#fn'] {
.newsfoot-footnote-popover + a.footnote:hover {
background: #aaa;
color: white;
transition: background-color 200ms ease-out;
}
a.footnote:hover,
.newsfoot-footnote-popover + a.footnote,
sup > a[href^='#fn']:hover,
sup > div > a[href^='#fn']:hover {
.newsfoot-footnote-popover + a.footnote {
background: #666;
transition: background-color 200ms ease-out;
}

View File

@ -91,7 +91,7 @@ class RootSplitViewController: UISplitViewController {
}
@objc func refresh(_ sender: Any?) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self))
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
}
@objc func goToToday(_ sender: Any?) {

View File

@ -61,6 +61,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private var wasRootSplitViewControllerCollapsed = false
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
private let rebuildBackingStoresWithMergeQueue = CoalescingQueue(name: "Rebuild The Backing Stores by Merging", interval: 1.0)
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
@ -445,9 +446,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return
}
addShadowTableToFilterExceptions()
rebuildBackingStores()
treeControllerDelegate.resetFilterExceptions()
rebuildBackingStoresWithMergeQueue.add(self, #selector(rebuildBackingStoresWithMerge))
}
@objc func statusesDidChange(_ note: Notification) {
@ -466,7 +465,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
@objc func batchUpdateDidPerform(_ notification: Notification) {
rebuildBackingStores()
rebuildBackingStoresWithMerge()
}
@objc func displayNameDidChange(_ note: Notification) {
@ -569,9 +568,19 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func suspend() {
fetchAndMergeArticlesQueue.performCallsImmediately()
rebuildBackingStoresWithMergeQueue.performCallsImmediately()
fetchRequestQueue.cancelAllRequests()
}
func refreshInterface() {
if isReadFeedsFiltered {
rebuildBackingStores()
}
if isReadArticlesFiltered && AppDefaults.refreshClearsReadArticles {
refreshTimeline(resetScroll: false)
}
}
func shadowNodesFor(section: Int) -> [Node] {
return shadowTable[section]
}
@ -1195,6 +1204,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
extension SceneCoordinator: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
masterTimelineViewController?.updateUI()
guard !isThreePanelMode else {
return true
}
@ -1209,6 +1220,8 @@ extension SceneCoordinator: UISplitViewControllerDelegate {
}
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
masterTimelineViewController?.updateUI()
guard !isThreePanelMode else {
return subSplitViewController
}
@ -1362,6 +1375,11 @@ private extension SceneCoordinator {
}
}
@objc func rebuildBackingStoresWithMerge() {
addShadowTableToFilterExceptions()
rebuildBackingStores()
}
func rebuildShadowTable() {
shadowTable = [[Node]]()
@ -1738,7 +1756,10 @@ private extension SceneCoordinator {
}
@objc func fetchAndMergeArticlesAsync() {
fetchAndMergeArticlesAsync(animated: true, completion: nil)
fetchAndMergeArticlesAsync(animated: true) {
self.masterTimelineViewController?.reinitializeArticles(resetScroll: false)
self.masterTimelineViewController?.restoreSelectionIfNecessary(adjustScroll: false)
}
}
func fetchAndMergeArticlesAsync(animated: Bool = true, completion: (() -> Void)? = nil) {
@ -2002,7 +2023,6 @@ private extension SceneCoordinator {
}
treeControllerDelegate.addFilterException(feedIdentifier)
masterFeedViewController.restoreSelection = true
switch feedIdentifier {
@ -2087,8 +2107,6 @@ private extension SceneCoordinator {
return false
}
masterFeedViewController.restoreSelection = true
switch feedIdentifier {
case .smartFeed:

View File

@ -81,6 +81,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
coordinator.suspend()
}
func refreshInterface() {
coordinator.refreshInterface()
}
}
private extension SceneDelegate {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="9cW-lu-HoC">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" 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>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -40,7 +40,7 @@
<tableViewSection headerTitle="Accounts" id="0ac-Ze-Dh4">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="6sn-wY-hHH" style="IBUITableViewCellStyleDefault" id="XHc-rQ-7FK" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="155.5" width="374" height="44"/>
<rect key="frame" x="0.0" y="155.5" width="374" 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="343" height="44"/>
@ -61,7 +61,7 @@
<tableViewSection headerTitle="Feeds" id="hAC-uA-RbS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="4Hg-B3-zAE" style="IBUITableViewCellStyleDefault" id="glf-Pg-s3P" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="255.5" width="374" height="44"/>
<rect key="frame" x="0.0" y="255.5" width="374" 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="374" height="44"/>
@ -129,7 +129,7 @@
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="Keq-Np-l9O">
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
<rect key="frame" x="307" y="6.5" width="51" height="31"/>
<color key="onTintColor" name="primaryAccentColor"/>
<connections>
<action selector="switchTimelineOrder:" destination="a0p-rk-skQ" eventType="valueChanged" id="ARp-jk-sAo"/>
@ -137,12 +137,14 @@
</switch>
</subviews>
<constraints>
<constraint firstItem="Keq-Np-l9O" firstAttribute="top" relation="greaterThanOrEqual" secondItem="GhU-ib-Mz8" secondAttribute="top" constant="6" id="B0w-Jk-LsD"/>
<constraint firstItem="Keq-Np-l9O" firstAttribute="centerY" secondItem="GhU-ib-Mz8" secondAttribute="centerY" id="Fn1-4L-3Mu"/>
<constraint firstItem="c9W-IF-u6i" firstAttribute="top" secondItem="GhU-ib-Mz8" secondAttribute="topMargin" id="I6I-av-62X"/>
<constraint firstAttribute="bottomMargin" secondItem="c9W-IF-u6i" secondAttribute="bottom" id="RKN-iP-jvM"/>
<constraint firstAttribute="trailing" secondItem="Keq-Np-l9O" secondAttribute="trailing" constant="20" symbolic="YES" id="Ry8-Ww-bwT"/>
<constraint firstAttribute="trailing" secondItem="Keq-Np-l9O" secondAttribute="trailing" constant="18" id="Ry8-Ww-bwT"/>
<constraint firstItem="Keq-Np-l9O" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="c9W-IF-u6i" secondAttribute="trailing" constant="8" id="crw-aM-cVp"/>
<constraint firstItem="c9W-IF-u6i" firstAttribute="leading" secondItem="GhU-ib-Mz8" secondAttribute="leadingMargin" id="hcX-Ao-zHb"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="Keq-Np-l9O" secondAttribute="bottom" constant="6" id="zNh-Vc-Ryn"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
@ -160,7 +162,7 @@
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="JNi-Wz-RbU">
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
<rect key="frame" x="307" y="6.5" width="51" height="31"/>
<color key="onTintColor" name="primaryAccentColor"/>
<connections>
<action selector="switchGroupByFeed:" destination="a0p-rk-skQ" eventType="valueChanged" id="Bxb-Jq-EEi"/>
@ -168,7 +170,9 @@
</switch>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="JNi-Wz-RbU" secondAttribute="trailing" constant="20" symbolic="YES" id="99y-MA-Qbl"/>
<constraint firstAttribute="trailing" secondItem="JNi-Wz-RbU" secondAttribute="trailing" constant="18" id="99y-MA-Qbl"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="JNi-Wz-RbU" secondAttribute="bottom" constant="6" id="Jls-0y-9cd"/>
<constraint firstItem="JNi-Wz-RbU" firstAttribute="top" relation="greaterThanOrEqual" secondItem="KHC-cc-tOC" secondAttribute="top" constant="6" id="Qoi-uf-HDC"/>
<constraint firstItem="JNi-Wz-RbU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="cit-i1-0Hp" secondAttribute="trailing" constant="8" id="SaK-1e-1Mg"/>
<constraint firstItem="cit-i1-0Hp" firstAttribute="bottom" secondItem="KHC-cc-tOC" secondAttribute="bottomMargin" id="cHk-g1-Wsg"/>
<constraint firstItem="JNi-Wz-RbU" firstAttribute="centerY" secondItem="KHC-cc-tOC" secondAttribute="centerY" id="idT-LP-oPt"/>
@ -191,7 +195,7 @@
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="duV-CN-JmH">
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
<rect key="frame" x="307" y="6.5" width="51" height="31"/>
<color key="onTintColor" name="primaryAccentColor"/>
<connections>
<action selector="switchClearsReadArticles:" destination="a0p-rk-skQ" eventType="valueChanged" id="Nel-mq-9fP"/>
@ -201,7 +205,9 @@
<constraints>
<constraint firstItem="KtJ-tk-DlD" firstAttribute="top" secondItem="XAn-lK-LoN" secondAttribute="topMargin" id="3mX-9g-2Bp"/>
<constraint firstItem="KtJ-tk-DlD" firstAttribute="leading" secondItem="XAn-lK-LoN" secondAttribute="leadingMargin" id="AOT-A0-ak0"/>
<constraint firstAttribute="trailing" secondItem="duV-CN-JmH" secondAttribute="trailing" constant="20" symbolic="YES" id="Qkh-LF-zez"/>
<constraint firstItem="duV-CN-JmH" firstAttribute="top" relation="greaterThanOrEqual" secondItem="XAn-lK-LoN" secondAttribute="top" constant="6" id="FeD-Le-7bK"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="duV-CN-JmH" secondAttribute="bottom" constant="6" id="MJP-qB-ivU"/>
<constraint firstAttribute="trailing" secondItem="duV-CN-JmH" secondAttribute="trailing" constant="18" id="Qkh-LF-zez"/>
<constraint firstItem="duV-CN-JmH" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="KtJ-tk-DlD" secondAttribute="trailing" constant="8" id="cCz-fb-lta"/>
<constraint firstItem="duV-CN-JmH" firstAttribute="centerY" secondItem="XAn-lK-LoN" secondAttribute="centerY" id="eui-vJ-Bp8"/>
<constraint firstAttribute="bottomMargin" secondItem="KtJ-tk-DlD" secondAttribute="bottom" id="iyQ-7h-MT3"/>
@ -249,7 +255,7 @@
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="UOo-9z-IuL">
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
<rect key="frame" x="307" y="6.5" width="51" height="31"/>
<color key="onTintColor" name="primaryAccentColor"/>
<connections>
<action selector="switchConfirmMarkAllAsRead:" destination="a0p-rk-skQ" eventType="valueChanged" id="7wW-hF-2OY"/>
@ -259,9 +265,11 @@
<constraints>
<constraint firstItem="5tY-5k-v2g" firstAttribute="top" secondItem="BpI-Hz-KH2" secondAttribute="topMargin" id="K3n-tK-0tu"/>
<constraint firstItem="UOo-9z-IuL" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="5tY-5k-v2g" secondAttribute="trailing" constant="8" id="KeP-ft-0GH"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="UOo-9z-IuL" secondAttribute="bottom" constant="6" id="RUM-9w-Q3y"/>
<constraint firstItem="UOo-9z-IuL" firstAttribute="centerY" secondItem="BpI-Hz-KH2" secondAttribute="centerY" id="V2L-l3-s1E"/>
<constraint firstAttribute="bottomMargin" secondItem="5tY-5k-v2g" secondAttribute="bottom" id="VeZ-P7-kQW"/>
<constraint firstAttribute="trailing" secondItem="UOo-9z-IuL" secondAttribute="trailing" constant="20" symbolic="YES" id="mNk-x8-oJx"/>
<constraint firstAttribute="trailing" secondItem="UOo-9z-IuL" secondAttribute="trailing" constant="18" id="mNk-x8-oJx"/>
<constraint firstItem="UOo-9z-IuL" firstAttribute="top" relation="greaterThanOrEqual" secondItem="BpI-Hz-KH2" secondAttribute="top" constant="6" id="tps-aV-CKA"/>
<constraint firstItem="5tY-5k-v2g" firstAttribute="leading" secondItem="BpI-Hz-KH2" secondAttribute="leadingMargin" id="v4X-Nd-cpC"/>
</constraints>
</tableViewCellContentView>
@ -280,14 +288,14 @@
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2Md-2E-7Z4">
<rect key="frame" x="305" y="3.5" width="51" height="31"/>
<color key="onTintColor" name="secondaryAccentColor"/>
<rect key="frame" x="305" y="6" width="51" height="25.5"/>
<color key="onTintColor" name="primaryAccentColor"/>
<connections>
<action selector="switchFullscreenArticles:" destination="a0p-rk-skQ" eventType="valueChanged" id="5fa-Ad-e0j"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="Tap the article top bar to enter Full Screen. Tap the top or bottom to exit." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a30-nc-ZS4">
<rect key="frame" x="20" y="33" width="266.5" height="0.0"/>
<rect key="frame" x="20" y="33" width="263" height="0.0"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -298,7 +306,8 @@
<constraint firstItem="a30-nc-ZS4" firstAttribute="leading" secondItem="zX8-l2-bVH" secondAttribute="leadingMargin" id="52y-SY-gbp"/>
<constraint firstItem="79e-5s-vd0" firstAttribute="top" secondItem="zX8-l2-bVH" secondAttribute="topMargin" id="9bF-Q1-sYE"/>
<constraint firstItem="2Md-2E-7Z4" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="a30-nc-ZS4" secondAttribute="trailing" constant="8" id="E9l-8S-WBL"/>
<constraint firstAttribute="trailing" secondItem="2Md-2E-7Z4" secondAttribute="trailing" constant="20" symbolic="YES" id="ELH-06-H2j"/>
<constraint firstAttribute="trailing" secondItem="2Md-2E-7Z4" secondAttribute="trailing" constant="20" id="ELH-06-H2j"/>
<constraint firstItem="2Md-2E-7Z4" firstAttribute="top" relation="greaterThanOrEqual" secondItem="zX8-l2-bVH" secondAttribute="top" constant="6" id="aBe-aC-mva"/>
<constraint firstItem="a30-nc-ZS4" firstAttribute="bottom" secondItem="zX8-l2-bVH" secondAttribute="bottomMargin" id="b3g-at-rjh"/>
<constraint firstItem="2Md-2E-7Z4" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="79e-5s-vd0" secondAttribute="trailing" constant="8" id="lUn-8D-X20"/>
<constraint firstItem="79e-5s-vd0" firstAttribute="leading" secondItem="zX8-l2-bVH" secondAttribute="leadingMargin" id="tdZ-30-ACC"/>
@ -396,14 +405,14 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dWz-1o-EpJ" style="IBUITableViewCellStyleDefault" id="2MG-qn-idJ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="1039.5" width="374" height="44"/>
<rect key="frame" x="20" y="1039.5" width="374" 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="374" 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="15" y="0.0" width="351" height="44"/>
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -413,14 +422,14 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="2o6-8W-nyK" style="IBUITableViewCellStyleDefault" id="he9-Ql-yfa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="1083.5" width="374" height="44"/>
<rect key="frame" x="20" y="1083.5" width="374" 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="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="NetNewsWire Slack" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="2o6-8W-nyK">
<rect key="frame" x="15" y="0.0" width="351" height="44"/>
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -430,14 +439,14 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Uwu-af-31r" style="IBUITableViewCellStyleDefault" id="EvG-yE-gDF" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="1127.5" width="374" height="44"/>
<rect key="frame" x="20" y="1127.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EvG-yE-gDF" id="wBN-zJ-6pN">
<rect key="frame" x="0.0" y="0.0" width="355" height="44"/>
<rect key="frame" x="0.0" y="0.0" width="343" 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="Uwu-af-31r">
<rect key="frame" x="15" y="0.0" width="332" height="44"/>
<rect key="frame" x="20" y="0.0" width="315" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>

View File

@ -38,6 +38,8 @@ class SettingsViewController: UITableViewController {
tableView.register(UINib(nibName: "SettingsAccountTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsAccountTableViewCell")
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44
}
override func viewWillAppear(_ animated: Bool) {
@ -236,7 +238,7 @@ class SettingsViewController: UITableViewController {
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1))
return UITableView.automaticDimension
}
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {

View File

@ -11,8 +11,11 @@ import UIKit
extension UIFont {
func withTraits(traits:UIFontDescriptor.SymbolicTraits) -> UIFont {
let descriptor = fontDescriptor.withSymbolicTraits(traits)
return UIFont(descriptor: descriptor!, size: 0) //size 0 means keep the size as it is
if let descriptor = fontDescriptor.withSymbolicTraits(traits) {
return UIFont(descriptor: descriptor, size: 0) //size 0 means keep the size as it is
} else {
return self
}
}
func bold() -> UIFont {

View File

@ -1,7 +1,7 @@
// High Level Settings common to both the iOS application and any extensions we bundle with it
MARKETING_VERSION = 5.0
CURRENT_PROJECT_VERSION = 38
CURRENT_PROJECT_VERSION = 39
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon