Merge branch 'ios-release'
This commit is contained in:
commit
5b5eaf6bb7
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 it’s not an error on first run.
|
||||
// TODO: make it so we know if it’s 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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 */,
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
81
iOS/Article/PreloadedWebView.swift
Normal file
81
iOS/Article/PreloadedWebView.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"/>
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
|
39
iOS/MasterFeed/MasterFeedDataSourceOperation.swift
Normal file
39
iOS/MasterFeed/MasterFeedDataSourceOperation.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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?()
|
||||
}
|
||||
|
@ -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%
|
||||
|
@ -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"/>
|
||||
|
60
iOS/MasterFeed/UpdateSelectionOperation.swift
Normal file
60
iOS/MasterFeed/UpdateSelectionOperation.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -9,10 +9,10 @@
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"red" : "242",
|
||||
"red" : "235",
|
||||
"alpha" : "1.000",
|
||||
"blue" : "242",
|
||||
"green" : "242"
|
||||
"blue" : "237",
|
||||
"green" : "235"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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?) {
|
||||
|
@ -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:
|
||||
|
@ -81,6 +81,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
coordinator.suspend()
|
||||
}
|
||||
|
||||
func refreshInterface() {
|
||||
coordinator.refreshInterface()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension SceneDelegate {
|
||||
|
@ -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"/>
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user