diff --git a/Frameworks/Account/AccountMetadataFile.swift b/Frameworks/Account/AccountMetadataFile.swift index 6e17ac734..d3bc67d65 100644 --- a/Frameworks/Account/AccountMetadataFile.swift +++ b/Frameworks/Account/AccountMetadataFile.swift @@ -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) } } diff --git a/Frameworks/Account/FeedFinder/FeedSpecifier.swift b/Frameworks/Account/FeedFinder/FeedSpecifier.swift index 009c39644..5b48b1b98 100644 --- a/Frameworks/Account/FeedFinder/FeedSpecifier.swift +++ b/Frameworks/Account/FeedFinder/FeedSpecifier.swift @@ -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 } diff --git a/Frameworks/Account/OPMLFile.swift b/Frameworks/Account/OPMLFile.swift index 034efe4e1..eb17aa033 100644 --- a/Frameworks/Account/OPMLFile.swift +++ b/Frameworks/Account/OPMLFile.swift @@ -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 diff --git a/Frameworks/Account/WebFeedMetadataFile.swift b/Frameworks/Account/WebFeedMetadataFile.swift index 8b0b3c029..be04530d7 100644 --- a/Frameworks/Account/WebFeedMetadataFile.swift +++ b/Frameworks/Account/WebFeedMetadataFile.swift @@ -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) } } diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css index ec475a886..caf91e7b2 100644 --- a/Mac/MainWindow/Detail/styleSheet.css +++ b/Mac/MainWindow/Detail/styleSheet.css @@ -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; } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 51a138d08..05212949e 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -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 = ""; }; 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = ""; }; - 51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMigrator.swift; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; + 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = ""; }; + 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = ""; }; + 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedSelectFolderTableViewCell.swift; sourceTree = ""; }; 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddWebFeedSelectFolderTableViewCell.xib; sourceTree = ""; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 92f0de19c..a66568560 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -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 } diff --git a/Shared/Article Rendering/main.js b/Shared/Article Rendering/main.js index 2a3c07337..36a5cb5d7 100644 --- a/Shared/Article Rendering/main.js +++ b/Shared/Article Rendering/main.js @@ -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() } diff --git a/Shared/Article Rendering/newsfoot.js b/Shared/Article Rendering/newsfoot.js index e841d3365..296c74d0b 100644 --- a/Shared/Article Rendering/newsfoot.js +++ b/Shared/Article Rendering/newsfoot.js @@ -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(); }); diff --git a/Shared/Extensions/IconImage.swift b/Shared/Extensions/IconImage.swift index 8d0a6c8b2..1ddc2935d 100644 --- a/Shared/Extensions/IconImage.swift +++ b/Shared/Extensions/IconImage.swift @@ -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 } } diff --git a/iOS/Account/Account.storyboard b/iOS/Account/Account.storyboard index 9c4011f8f..52982af01 100644 --- a/iOS/Account/Account.storyboard +++ b/iOS/Account/Account.storyboard @@ -1,8 +1,8 @@ - + - + diff --git a/iOS/Account/FeedWranglerAccountViewController.swift b/iOS/Account/FeedWranglerAccountViewController.swift index a5356d09a..b6c297df8 100644 --- a/iOS/Account/FeedWranglerAccountViewController.swift +++ b/iOS/Account/FeedWranglerAccountViewController.swift @@ -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 } diff --git a/iOS/Account/FeedbinAccountViewController.swift b/iOS/Account/FeedbinAccountViewController.swift index 695ee2a1c..df6110b64 100644 --- a/iOS/Account/FeedbinAccountViewController.swift +++ b/iOS/Account/FeedbinAccountViewController.swift @@ -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 } diff --git a/iOS/AccountMigrator.swift b/iOS/AccountMigrator.swift deleted file mode 100644 index 344513917..000000000 --- a/iOS/AccountMigrator.swift +++ /dev/null @@ -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) - } - -} diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 61b76c648..c6a2b3123 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -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() diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index b189719dc..1907dbac1 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -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 } diff --git a/iOS/Article/ImageViewController.swift b/iOS/Article/ImageViewController.swift index acd572a13..79285d4fb 100644 --- a/iOS/Article/ImageViewController.swift +++ b/iOS/Article/ImageViewController.swift @@ -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 diff --git a/iOS/Article/PreloadedWebView.swift b/iOS/Article/PreloadedWebView.swift new file mode 100644 index 000000000..1573b6a3d --- /dev/null +++ b/iOS/Article/PreloadedWebView.swift @@ -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) + } + +} diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 111a1e23f..5e04ea578 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -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) } diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift index 8fda2d206..9d05473a9 100644 --- a/iOS/Article/WebViewProvider.swift +++ b/iOS/Article/WebViewProvider.swift @@ -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 - } -} diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 02ad94cf9..ad9b7cb52 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -239,16 +239,16 @@ - - + + - + -