Add basic Article Content extraction
This commit is contained in:
parent
0fcbcb50e0
commit
8cd6f107e5
@ -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"/>
|
||||
|
109
Mac/MainWindow/ArticleExtractorButton.swift
Normal file
109
Mac/MainWindow/ArticleExtractorButton.swift
Normal 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")
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -16,6 +16,7 @@ enum DetailState: Equatable {
|
||||
case noSelection
|
||||
case multipleSelection
|
||||
case article(Article)
|
||||
case extracted(Article, ExtractedArticle)
|
||||
}
|
||||
|
||||
final class DetailViewController: NSViewController, WKUIDelegate {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
12
Mac/Resources/Assets.xcassets/articleExtractor.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractor.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FullArticle.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/articleExtractor.imageset/FullArticle.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractor.imageset/FullArticle.pdf
vendored
Normal file
Binary file not shown.
12
Mac/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FullArticleError.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/articleExtractorError.imageset/FullArticleError.png
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractorError.imageset/FullArticleError.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 282 B |
12
Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FullArticleProgress1.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/FullArticleProgress1.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/FullArticleProgress1.pdf
vendored
Normal file
Binary file not shown.
12
Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FullArticleProgress2.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/FullArticleProgress2.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/FullArticleProgress2.pdf
vendored
Normal file
Binary file not shown.
12
Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FullArticleProgress3.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/FullArticleProgress3.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/FullArticleProgress3.pdf
vendored
Normal file
Binary file not shown.
12
Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FullArticleProgress4.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/FullArticleProgress4.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/FullArticleProgress4.pdf
vendored
Normal file
Binary file not shown.
@ -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 */,
|
||||
|
@ -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"
|
||||
|
102
Shared/Article Extractor/ArticleExtractor.swift
Normal file
102
Shared/Article Extractor/ArticleExtractor.swift
Normal 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()
|
||||
|
||||
}
|
||||
|
||||
}
|
35
Shared/Article Extractor/ArticleExtractorConfig.swift
Normal file
35
Shared/Article Extractor/ArticleExtractorConfig.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
45
Shared/Article Extractor/ExtractedArticle.swift
Normal file
45
Shared/Article Extractor/ExtractedArticle.swift
Normal 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"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user