Add basic Article Content extraction

This commit is contained in:
Maurice Parker 2019-09-18 18:15:55 -05:00
parent 0fcbcb50e0
commit 8cd6f107e5
24 changed files with 534 additions and 16 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14865.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -168,6 +168,22 @@
<action selector="refreshAll:" target="Oky-zY-oP4" id="KRz-Df-3zA"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="642D2379-B9AF-4990-8E09-A1115C60ED1A" label="Reader" paletteLabel="Reader" image="articleExtractor" id="6Vm-OW-3mR" customClass="RSToolbarItem" customModule="RSCore">
<nil key="toolTip"/>
<size key="minSize" width="38" height="25"/>
<size key="maxSize" width="38" height="27"/>
<button key="view" verticalHuggingPriority="750" id="1b9-Tf-u5V" customClass="ArticleExtractorButton" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="3" y="14" width="38" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="articleExtractor" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sXz-Xe-Kd7">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES" changeBackground="YES" changeGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<connections>
<action selector="toggleArticleExtractor:" target="B8D-0N-5wS" id="ZKQ-SK-5YJ"/>
</connections>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="Skp-5r-70Q"/>
@ -180,6 +196,7 @@
<toolbarItem reference="p7Y-Vm-ILH"/>
<toolbarItem reference="Gxg-WQ-ufC"/>
<toolbarItem reference="N7D-g2-EPD"/>
<toolbarItem reference="6Vm-OW-3mR"/>
<toolbarItem reference="tid-SB-me3"/>
<toolbarItem reference="nv0-Ju-lP7"/>
</defaultToolbarItems>
@ -192,6 +209,7 @@
</connections>
</window>
<connections>
<outlet property="articleExtractorButton" destination="1b9-Tf-u5V" id="W8P-DA-hmV"/>
<segue destination="reS-fe-pD8" kind="relationship" relationship="window.shadowedContentViewController" id="WS2-WB-dc4"/>
</connections>
</windowController>
@ -276,7 +294,7 @@
<tableColumns>
<tableColumn width="164" minWidth="23" maxWidth="1000" id="ih9-mJ-EA7">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="label" size="11"/>
<font key="font" metaFont="menu" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
@ -488,6 +506,7 @@
<image name="NSAddTemplate" width="11" height="11"/>
<image name="NSRefreshTemplate" width="11" height="15"/>
<image name="NSShareTemplate" width="11" height="16"/>
<image name="articleExtractor" width="16" height="16"/>
<image name="markAllRead" width="22" height="19"/>
<image name="markRead" width="19" height="19"/>
<image name="newFolder" width="19" height="19"/>

View File

@ -0,0 +1,109 @@
//
// ArticleExtractorButton.swift
// NetNewsWire
//
// Created by Maurice Parker on 9/18/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
class ArticleExtractorButton: NSButton {
var isError = false {
didSet {
needsDisplay = true
}
}
var isInProgress = false {
didSet {
needsDisplay = true
}
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
wantsLayer = true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let hostedLayer = self.layer else {
return
}
if let imageLayer = hostedLayer.sublayers?[0] {
if needsToDraw(imageLayer.bounds) {
imageLayer.removeFromSuperlayer()
} else {
return
}
}
let opacity: Float = isEnabled ? 1.0 : 0.5
switch true {
case isError:
addImageSublayer(to: hostedLayer, imageName: "articleExtractorError", opacity: opacity)
case isInProgress:
addProgressSublayer(to: hostedLayer)
default:
addImageSublayer(to: hostedLayer, imageName: "articleExtractor", opacity: opacity)
}
}
private func makeLayerForImage(_ image: NSImage) -> CALayer {
let imageLayer = CALayer()
imageLayer.bounds = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
imageLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
return imageLayer
}
private func addImageSublayer(to hostedLayer: CALayer, imageName: String, opacity: Float = 1.0) {
guard let image = NSImage(named: imageName) else {
fatalError("Image doesn't exist: \(imageName)")
}
let imageLayer = makeLayerForImage(image)
imageLayer.contents = image
imageLayer.opacity = opacity
hostedLayer.addSublayer(imageLayer)
}
private func addProgressSublayer(to hostedLayer: CALayer) {
let imageProgress1 = NSImage(named: "articleExtractorProgress1")
let imageProgress2 = NSImage(named: "articleExtractorProgress2")
let imageProgress3 = NSImage(named: "articleExtractorProgress3")
let imageProgress4 = NSImage(named: "articleExtractorProgress4")
let images = [imageProgress1, imageProgress2, imageProgress3, imageProgress4, imageProgress3, imageProgress2]
let imageLayer = CALayer()
imageLayer.bounds = CGRect(x: 0, y: 0, width: imageProgress1?.size.width ?? 0, height: imageProgress1?.size.height ?? 0)
imageLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
hostedLayer.addSublayer(imageLayer)
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = CAAnimationCalculationMode.linear
animation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1]
animation.duration = 2
animation.values = images as [Any]
animation.repeatCount = HUGE
imageLayer.add(animation, forKey: "contents")
}
}

View File

@ -16,6 +16,7 @@ enum DetailState: Equatable {
case noSelection
case multipleSelection
case article(Article)
case extracted(Article, ExtractedArticle)
}
final class DetailViewController: NSViewController, WKUIDelegate {

View File

@ -176,6 +176,8 @@ private extension DetailWebViewController {
html = ArticleRenderer.multipleSelectionHTML(style: style)
case .article(let article):
html = ArticleRenderer.articleHTML(article: article, style: style)
case .extracted(let article, let extractedArticle):
html = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style)
}
webView.loadHTMLString(html, baseURL: nil)

View File

@ -17,6 +17,9 @@ enum TimelineSourceMode {
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@IBOutlet weak var articleExtractorButton: ArticleExtractorButton!
private var articleExtractor: ArticleExtractor? = nil
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow")
@ -206,6 +209,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
return canMarkOlderArticlesAsRead()
}
if item.action == #selector(toggleArticleExtractor(_:)) {
return validateToggleArticleExtractor(item)
}
if item.action == #selector(toolbarShowShareMenu(_:)) {
return canShowShareMenu()
}
@ -292,6 +299,34 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
currentTimelineViewController?.toggleStarredStatusForSelectedArticles()
}
@IBAction func toggleArticleExtractor(_ sender: Any?) {
guard let currentLink = currentLink, let article = oneSelectedArticle else {
return
}
guard articleExtractorButton.state == .on else {
let detailState = DetailState.article(article)
detailViewController?.setState(detailState, mode: timelineSourceMode)
return
}
if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article {
if currentLink == articleExtractor.articleLink {
let detailState = DetailState.extracted(article, extractedArticle)
detailViewController?.setState(detailState, mode: timelineSourceMode)
}
} else {
if let extractor = ArticleExtractor(currentLink) {
extractor.delegate = self
extractor.process()
articleExtractor = extractor
makeToolbarValidate()
}
}
}
@IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
markAllAsRead(sender)
nextUnread(sender)
@ -407,6 +442,11 @@ extension MainWindowController: SidebarDelegate {
extension MainWindowController: TimelineContainerViewControllerDelegate {
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
articleExtractorButton.isError = false
articleExtractorButton.isInProgress = false
articleExtractorButton.state = .off
articleExtractor = nil
let detailState: DetailState
if let articles = articles {
detailState = articles.count == 1 ? .article(articles.first!) : .multipleSelection
@ -414,6 +454,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
else {
detailState = .noSelection
}
detailViewController?.setState(detailState, mode: mode)
}
}
@ -481,6 +522,24 @@ extension MainWindowController: NSSearchFieldDelegate {
}
}
// MARK: - ArticleExtractorDelegate
extension MainWindowController: ArticleExtractorDelegate {
func articleExtractionDidFail(with: Error) {
makeToolbarValidate()
}
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
makeToolbarValidate()
if articleExtractorButton.state == .on, let article = oneSelectedArticle {
let detailState = DetailState.extracted(article, extractedArticle)
detailViewController?.setState(detailState, mode: timelineSourceMode)
}
}
}
// MARK: - Scripting Access
/*
@ -632,6 +691,34 @@ private extension MainWindowController {
return result
}
func validateToggleArticleExtractor(_ item: NSValidatedUserInterfaceItem) -> Bool {
guard let articleExtractorState = articleExtractor?.state else {
articleExtractorButton.isError = false
articleExtractorButton.isInProgress = false
return currentLink != nil
}
switch articleExtractorState {
case .ready:
articleExtractorButton.isError = false
articleExtractorButton.isInProgress = false
return currentLink != nil
case .processing:
articleExtractorButton.isError = false
articleExtractorButton.isInProgress = true
return true
case .failedToParse:
articleExtractorButton.isError = true
articleExtractorButton.isInProgress = false
articleExtractorButton.state = .off
return true
case .complete:
articleExtractorButton.isError = false
articleExtractorButton.isInProgress = false
return currentLink != nil
}
}
func canMarkOlderArticlesAsRead() -> Bool {
return currentTimelineViewController?.canMarkOlderArticlesAsRead() ?? false

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FullArticle.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FullArticleError.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FullArticleProgress1.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FullArticleProgress2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FullArticleProgress3.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FullArticleProgress4.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -199,6 +199,13 @@
51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; };
51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; };
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; };
51FA73A42332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; };
51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; };
51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; };
51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; };
51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; };
51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; };
51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */; };
55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; };
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; };
5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; };
@ -863,6 +870,10 @@
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = "<group>"; };
51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = "<group>"; };
51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = "<group>"; };
51FA73A32332BE110090D516 /* ArticleExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractor.swift; sourceTree = "<group>"; };
51FA73A62332BE880090D516 /* ExtractedArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtractedArticle.swift; sourceTree = "<group>"; };
51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorConfig.swift; sourceTree = "<group>"; };
51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = "<group>"; };
557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = "<group>"; };
55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = "<group>"; };
55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = "<group>"; };
@ -1391,6 +1402,16 @@
name = Frameworks;
sourceTree = "<group>";
};
51FA739A2332BDE70090D516 /* Article Extractor */ = {
isa = PBXGroup;
children = (
51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */,
51FA73A32332BE110090D516 /* ArticleExtractor.swift */,
51FA73A62332BE880090D516 /* ExtractedArticle.swift */,
);
path = "Article Extractor";
sourceTree = "<group>";
};
6581C73620CED60100F4AD34 /* SafariExtension */ = {
isa = PBXGroup;
children = (
@ -1456,6 +1477,7 @@
849A975D1ED9EB72007D329B /* MainWindowController.swift */,
519B8D322143397200FA689C /* SharingServiceDelegate.swift */,
849EE72020391F560082A1EA /* SharingServicePickerDelegate.swift */,
51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */,
844B5B6B1FEA224B00C7C76A /* Keyboard */,
849A975F1ED9EB95007D329B /* Sidebar */,
849A97681ED9EBC8007D329B /* Timeline */,
@ -1807,6 +1829,7 @@
51C452AD2265102800C03939 /* Timeline */,
84702AB31FA27AE8006B8943 /* Commands */,
51934CCC231078DC006127BE /* Activity */,
51FA739A2332BDE70090D516 /* Article Extractor */,
51C452A822650DA100C03939 /* Article Rendering */,
849A97861ED9ECEF007D329B /* Article Styles */,
84DAEE201F86CAE00058304B /* Importers */,
@ -2645,10 +2668,12 @@
51322859232FDDB80033D4ED /* VibrantButtonStyle.swift in Sources */,
514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */,
5152E0F923248F6200E5C7AD /* SettingsLocalAccountView.swift in Sources */,
51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */,
FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */,
510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */,
51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */,
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */,
5132285B232FF2C40033D4ED /* SettingsRefreshSelectionView.swift in Sources */,
51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */,
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
@ -2676,6 +2701,7 @@
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */,
51C45258226508CF00C03939 /* AppAssets.swift in Sources */,
51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */,
51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */,
51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */,
51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */,
@ -2736,6 +2762,7 @@
84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */,
848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */,
847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */,
51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
51EF0F7A22771B890050506E /* ColorHash.swift in Sources */,
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */,
@ -2815,6 +2842,7 @@
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
84DEE56522C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */,
51FA73B72332D5F70090D516 /* ArticleExtractorButton.swift in Sources */,
51EF0F922279CA620050506E /* AccountsAddTableCellView.swift in Sources */,
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
8405DDA522168C62008CE1BF /* TimelineContainerViewController.swift in Sources */,
@ -2838,6 +2866,7 @@
84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */,
849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */,
849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */,
51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */,
84B99C9D1FAE83C600ECDEDB /* DeleteCommand.swift in Sources */,
849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */,
5144EA40227A37EC00D19003 /* ImportOPMLWindowController.swift in Sources */,
@ -2846,6 +2875,7 @@
D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */,
84C9FC7922629E1200D921D6 /* PreferencesWindowController.swift in Sources */,
84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */,
51FA73A42332BE110090D516 /* ArticleExtractor.swift in Sources */,
84CAFCA422BC8C08007694F0 /* FetchRequestQueue.swift in Sources */,
844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */,
84C9FC7C22629E1200D921D6 /* AccountsPreferencesViewController.swift in Sources */,

View File

@ -58,9 +58,6 @@
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
stopOnEveryThreadSanitizerIssue = "YES"
stopOnEveryUBSanitizerIssue = "YES"
stopOnEveryMainThreadCheckerIssue = "YES"
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
@ -74,6 +71,18 @@
ReferencedContainer = "container:NetNewsWire.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "MERCURY_CLIENT_ID"
value = "netnewswire"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "MERCURY_CLIENT_SECRET"
value = "PM9QZZwckFPLcJdUt4BADDqwHdKCAy8zxaAakjmpGdbjEjfcAdU3CTNdBf8Lw"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -0,0 +1,102 @@
//
// ArticleExtractor.swift
// NetNewsWire
//
// Created by Maurice Parker on 9/18/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
public enum ArticleExtractorState {
case ready
case processing
case failedToParse
case complete
}
protocol ArticleExtractorDelegate {
func articleExtractionDidFail(with: Error)
func articleExtractionDidComplete(extractedArticle: ExtractedArticle)
}
enum ArticleExtractorError: Error {
case UnableToParseHTML
case MissingURL
case UnableToLoadURL
}
class ArticleExtractor {
var state: ArticleExtractorState!
var article: ExtractedArticle?
var delegate: ArticleExtractorDelegate?
var articleLink: String?
private var url: URL!
public init?(_ articleLink: String) {
self.articleLink = articleLink
let clientURL = ArticleExtractorConfig.Mercury.clientURL
let username = ArticleExtractorConfig.Mercury.clientId
let signiture = articleLink.hmacUsingSHA1(key: ArticleExtractorConfig.Mercury.clientSecret)
if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() {
let fullURL = "\(clientURL)/\(username)/\(signiture)?base64_url=\(base64URL)"
if let url = URL(string: fullURL) {
self.url = url
return
}
}
return nil
}
public func process() {
state = .processing
let dataTask = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
self.state = .failedToParse
DispatchQueue.main.async {
self.delegate?.articleExtractionDidFail(with: error)
}
return
}
guard let data = data else {
self.state = .failedToParse
DispatchQueue.main.async {
self.delegate?.articleExtractionDidFail(with: ArticleExtractorError.UnableToLoadURL)
}
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
self.article = try decoder.decode(ExtractedArticle.self, from: data)
self.state = .complete
DispatchQueue.main.async {
self.delegate?.articleExtractionDidComplete(extractedArticle: self.article!)
}
} catch {
self.state = .failedToParse
DispatchQueue.main.async {
self.delegate?.articleExtractionDidFail(with: error)
}
}
}
dataTask.resume()
}
}

View File

@ -0,0 +1,35 @@
//
// ArticleExtractorSecrets.swift
// NetNewsWire
//
// Created by Maurice Parker on 9/18/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
enum ArticleExtractorConfig {
enum Mercury {
// For testing add the environment variables in the scheme you are using
static let clientId = ArticleExtractorConfig.environmentVariable(named: "MERCURY_CLIENT_ID") ?? Release.mercuryId
static let clientSecret = ArticleExtractorConfig.environmentVariable(named: "MERCURY_CLIENT_SECRET") ?? Release.mercurySecret
static let clientURL = Release.mercuryURL
}
private enum Release {
static let mercuryId = "{MERCURYID}"
static let mercurySecret = "{MERCURYSECRET}"
static let mercuryURL = "https://extract.feedbin.com/parser"
}
private static func environmentVariable(named: String) -> String? {
let processInfo = ProcessInfo.processInfo
guard let value = processInfo.environment[named] else {
print("‼️ Missing Environment Variable: '\(named)'")
return nil
}
return value
}
}

View File

@ -0,0 +1,45 @@
//
// ExtractedArticle.swift
// NetNewsWire
//
// Created by Maurice Parker on 9/18/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
struct ExtractedArticle: Codable, Equatable {
let title: String?
let author: String?
let datePublished: String?
let dek: String?
let leadImageURL: String?
let content: String?
let nextPageURL: String?
let url: String?
let domain: String?
let excerpt: String?
let wordCount: Int?
let direction: String?
let totalPages: Int?
let renderedPages: Int?
enum CodingKeys: String, CodingKey {
case title = "title"
case author = "author"
case datePublished = "date_published"
case dek = "dek"
case leadImageURL = "lead_image_url"
case content = "content"
case nextPageURL = "next_page_url"
case url = "url"
case domain = "domain"
case excerpt = "excerpt"
case wordCount = "word_count"
case direction = "direction"
case totalPages = "total_pages"
case renderedPages = "rendered_pages"
}
}

View File

@ -14,36 +14,44 @@ import Account
struct ArticleRenderer {
private let article: Article?
private let extractedArticle: ExtractedArticle?
private let articleStyle: ArticleStyle
private let title: String
private let body: String
private let baseURL: String?
private init(article: Article?, style: ArticleStyle) {
private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle) {
self.article = article
self.extractedArticle = extractedArticle
self.articleStyle = style
self.title = article?.title ?? ""
if let content = extractedArticle?.content {
self.body = content
} else {
self.body = article?.body ?? ""
}
self.baseURL = article?.baseURL?.absoluteString
}
// MARK: - API
static func articleHTML(article: Article, style: ArticleStyle) -> String {
let renderer = ArticleRenderer(article: article, style: style)
static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle) -> String {
let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style)
return renderer.articleHTML
}
static func multipleSelectionHTML(style: ArticleStyle) -> String {
let renderer = ArticleRenderer(article: nil, style: style)
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return renderer.multipleSelectionHTML
}
static func noSelectionHTML(style: ArticleStyle) -> String {
let renderer = ArticleRenderer(article: nil, style: style)
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return renderer.noSelectionHTML
}
static func noContentHTML(style: ArticleStyle) -> String {
let renderer = ArticleRenderer(article: nil, style: style)
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return renderer.noContentHTML
}
}
@ -53,7 +61,7 @@ struct ArticleRenderer {
private extension ArticleRenderer {
private var articleHTML: String {
let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: substitutions(), macroStart: "[[", macroEnd: "]]")
let body = RSMacroProcessor.renderedText(withTemplate: template(), substitutions: articleSubstitutions(), macroStart: "[[", macroEnd: "]]")
return renderHTML(withBody: body)
}
@ -101,7 +109,7 @@ private extension ArticleRenderer {
return title
}
func substitutions() -> [String: String] {
func articleSubstitutions() -> [String: String] {
var d = [String: String]()
guard let article = article else {
@ -112,7 +120,6 @@ private extension ArticleRenderer {
let title = titleOrTitleLink()
d["title"] = title
let body = article.body ?? ""
d["body"] = body
d["avatars"] = ""

@ -1 +1 @@
Subproject commit 98c050aca6e2c4f034b22c1d3d4f938893290543
Subproject commit 4dbd31b090ab15c3966e9810a65edbf4abdbdd33