mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'master' into feature/feed-wrangler
# Conflicts: # Frameworks/Account/Account.swift # Frameworks/Account/Account.xcodeproj/project.pbxproj # NetNewsWire.xcodeproj/project.pbxproj # submodules/RSCore
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
extension NSImage.Name {
|
||||
static let star = NSImage.Name("star")
|
||||
@@ -16,12 +17,6 @@ extension NSImage.Name {
|
||||
|
||||
struct AppAssets {
|
||||
|
||||
static var genericFeedImage: RSImage? = {
|
||||
let path = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/BookmarkIcon.icns"
|
||||
let image = RSImage(contentsOfFile: path)
|
||||
return image
|
||||
}()
|
||||
|
||||
static var timelineStar: RSImage! = {
|
||||
return RSImage(named: .timelineStar)
|
||||
}()
|
||||
@@ -54,6 +49,14 @@ struct AppAssets {
|
||||
return RSImage(named: "articleExtractorError")
|
||||
}()
|
||||
|
||||
static var articleExtractorInactiveDark: RSImage! = {
|
||||
return RSImage(named: "articleExtractorInactiveDark")
|
||||
}()
|
||||
|
||||
static var articleExtractorInactiveLight: RSImage! = {
|
||||
return RSImage(named: "articleExtractorInactiveLight")
|
||||
}()
|
||||
|
||||
static var articleExtractorProgress1: RSImage! = {
|
||||
return RSImage(named: "articleExtractorProgress1")
|
||||
}()
|
||||
@@ -74,28 +77,63 @@ struct AppAssets {
|
||||
return RSImage(named: "faviconTemplateImage")!
|
||||
}()
|
||||
|
||||
static var avatarLightBackgroundColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("avatarLightBackgroundColor"))!
|
||||
static var iconLightBackgroundColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("iconLightBackgroundColor"))!
|
||||
}()
|
||||
|
||||
static var avatarDarkBackgroundColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("avatarDarkBackgroundColor"))!
|
||||
static var iconDarkBackgroundColor: NSColor = {
|
||||
return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
|
||||
}()
|
||||
|
||||
static var searchFeedImage: RSImage = {
|
||||
return RSImage(named: NSImage.smartBadgeTemplateName)!
|
||||
static var masterFolderImage: IconImage = {
|
||||
return IconImage(RSImage(named: NSImage.folderName)!)
|
||||
}()
|
||||
|
||||
static var starredFeedImage: RSImage = {
|
||||
return RSImage(named: NSImage.smartBadgeTemplateName)!
|
||||
static var searchFeedImage: IconImage = {
|
||||
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!)
|
||||
}()
|
||||
|
||||
static var todayFeedImage: RSImage = {
|
||||
return RSImage(named: NSImage.smartBadgeTemplateName)!
|
||||
static var starredFeedImage: IconImage = {
|
||||
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!)
|
||||
}()
|
||||
|
||||
static var unreadFeedImage: RSImage = {
|
||||
return RSImage(named: NSImage.smartBadgeTemplateName)!
|
||||
static var todayFeedImage: IconImage = {
|
||||
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!)
|
||||
}()
|
||||
|
||||
static var unreadFeedImage: IconImage = {
|
||||
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!)
|
||||
}()
|
||||
|
||||
static var swipeMarkReadImage: RSImage = {
|
||||
return RSImage(named: "swipeMarkRead")!
|
||||
}()
|
||||
|
||||
static var swipeMarkUnreadImage: RSImage = {
|
||||
return RSImage(named: "swipeMarkUnread")!
|
||||
}()
|
||||
|
||||
static var swipeMarkStarredImage: RSImage = {
|
||||
return RSImage(named: "swipeMarkStarred")!
|
||||
}()
|
||||
|
||||
static var swipeMarkUnstarredImage: RSImage = {
|
||||
return RSImage(named: "swipeMarkUnstarred")!
|
||||
}()
|
||||
|
||||
static func image(for accountType: AccountType) -> NSImage? {
|
||||
switch accountType {
|
||||
case .onMyMac:
|
||||
return AppAssets.accountLocal
|
||||
case .feedbin:
|
||||
return AppAssets.accountFeedbin
|
||||
case .feedly:
|
||||
return AppAssets.accountFeedly
|
||||
case .freshRSS:
|
||||
return AppAssets.accountFreshRSS
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ struct AppDefaults {
|
||||
static let openInBrowserInBackground = "openInBrowserInBackground"
|
||||
static let mainWindowWidths = "mainWindowWidths"
|
||||
static let refreshInterval = "refreshInterval"
|
||||
static let addFeedAccountID = "addFeedAccountID"
|
||||
static let addWebFeedAccountID = "addWebFeedAccountID"
|
||||
static let addWebFeedFolderName = "addWebFeedFolderName"
|
||||
static let addFolderAccountID = "addFolderAccountID"
|
||||
static let importOPMLAccountID = "importOPMLAccountID"
|
||||
static let exportOPMLAccountID = "exportOPMLAccountID"
|
||||
@@ -99,15 +100,24 @@ struct AppDefaults {
|
||||
}
|
||||
}
|
||||
|
||||
static var addFeedAccountID: String? {
|
||||
static var addWebFeedAccountID: String? {
|
||||
get {
|
||||
return string(for: Key.addFeedAccountID)
|
||||
return string(for: Key.addWebFeedAccountID)
|
||||
}
|
||||
set {
|
||||
setString(for: Key.addFeedAccountID, newValue)
|
||||
setString(for: Key.addWebFeedAccountID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
static var addWebFeedFolderName: String? {
|
||||
get {
|
||||
return string(for: Key.addWebFeedFolderName)
|
||||
}
|
||||
set {
|
||||
setString(for: Key.addWebFeedFolderName, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
static var addFolderAccountID: String? {
|
||||
get {
|
||||
return string(for: Key.addFolderAccountID)
|
||||
|
||||
@@ -33,7 +33,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
var faviconDownloader: FaviconDownloader!
|
||||
var imageDownloader: ImageDownloader!
|
||||
var authorAvatarDownloader: AuthorAvatarDownloader!
|
||||
var feedIconDownloader: FeedIconDownloader!
|
||||
var webFeedIconDownloader: WebFeedIconDownloader!
|
||||
var appName: String!
|
||||
|
||||
var refreshTimer: AccountRefreshTimer?
|
||||
@@ -163,20 +163,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
|
||||
let tempDirectory = NSTemporaryDirectory()
|
||||
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
|
||||
let cacheFolder = (tempDirectory as NSString).appendingPathComponent(bundleIdentifier)
|
||||
CacheCleaner.purgeIfNecessary()
|
||||
|
||||
// If the image disk cache hasn't been flushed for 3 days and the network is available, delete it
|
||||
if let flushDate = AppDefaults.lastImageCacheFlushDate, flushDate.addingTimeInterval(3600*24*3) < Date() {
|
||||
if let reachability = try? Reachability(hostname: "apple.com") {
|
||||
if reachability.connection != .unavailable {
|
||||
try? FileManager.default.removeItem(atPath: cacheFolder)
|
||||
AppDefaults.lastImageCacheFlushDate = Date()
|
||||
}
|
||||
}
|
||||
// Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir
|
||||
let cacheFolder: String
|
||||
if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path {
|
||||
cacheFolder = userCacheFolder
|
||||
}
|
||||
|
||||
else {
|
||||
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
|
||||
cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier)
|
||||
}
|
||||
|
||||
let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons")
|
||||
let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder)
|
||||
try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
|
||||
@@ -188,7 +186,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
imageDownloader = ImageDownloader(folder: imagesFolder)
|
||||
|
||||
authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
|
||||
feedIconDownloader = FeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
|
||||
webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
|
||||
|
||||
updateSortMenuItems()
|
||||
updateGroupByFeedMenuItem()
|
||||
@@ -197,7 +195,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
mainWindowController?.window?.center()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@@ -301,12 +299,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
|
||||
@objc func feedSettingDidChange(_ note: Notification) {
|
||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||
|
||||
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
|
||||
guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
||||
return
|
||||
}
|
||||
if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
|
||||
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
||||
let _ = faviconDownloader.favicon(for: feed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12118" systemVersion="16F73" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15400" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12118"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15400"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AddFolderWindowController" customModule="Evergreen" customModuleProvider="target">
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AddFolderWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="accountPopupButton" destination="8HL-br-Pw6" id="MiU-3T-yqH"/>
|
||||
<outlet property="addFolderButton" destination="7gd-Tw-TDU" id="KrI-Wc-QDM"/>
|
||||
<outlet property="folderNameTextField" destination="1xA-Eb-rIK" id="M56-jU-Gqs"/>
|
||||
<outlet property="window" destination="QvC-M9-y7g" id="bw1-oV-4if"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Add Folder" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
|
||||
<window title="Add Folder" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="133"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
|
||||
<view key="contentView" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="133"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="132"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CWJ-Gi-Q6q">
|
||||
<rect key="frame" x="18" y="94" width="91" height="17"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="CWJ-Gi-Q6q">
|
||||
<rect key="frame" x="18" y="94" width="91" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Folder Name:" id="k8o-om-Rgh">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1xA-Eb-rIK" userLabel="Folder Name Text Field">
|
||||
<rect key="frame" x="115" y="90" width="345" height="22"/>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1xA-Eb-rIK" userLabel="Folder Name Text Field">
|
||||
<rect key="frame" x="115" y="90" width="345" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="v4n-IU-EZ9">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="UPI-CR-haX"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hNa-g7-hfw">
|
||||
<rect key="frame" x="46" y="64" width="63" height="17"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hNa-g7-hfw">
|
||||
<rect key="frame" x="46" y="64" width="63" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Account:" id="t6T-ar-V3d">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -49,7 +53,7 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8HL-br-Pw6" userLabel="Account Popup">
|
||||
<rect key="frame" x="113" y="58" width="350" height="26"/>
|
||||
<rect key="frame" x="113" y="58" width="350" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SDZ-cp-t7j" id="U9R-SI-VBz">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -64,7 +68,7 @@
|
||||
</popUpButton>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7gd-Tw-TDU">
|
||||
<rect key="frame" x="359" y="13" width="107" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Add Folder" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="F5a-Q7-NMn">
|
||||
<buttonCell key="cell" type="push" title="Add Folder" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="F5a-Q7-NMn">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@@ -606,7 +605,6 @@
|
||||
<connections>
|
||||
<outlet property="checkForUpdatesMenuItem" destination="1nF-7O-aKU" id="JmT-jc-DJ8"/>
|
||||
<outlet property="debugMenuItem" destination="UqE-mp-gtV" id="OnR-lr-Zlt"/>
|
||||
<outlet property="enableWebInspectorMenuItem" destination="EwI-z4-ZA3" id="EGp-lP-f91"/>
|
||||
<outlet property="groupArticlesByFeedMenuItem" destination="Zxm-O6-NRE" id="gwn-VT-2YZ"/>
|
||||
<outlet property="sortByNewestArticleOnTopMenuItem" destination="TNS-TV-n0U" id="gix-Nd-9k4"/>
|
||||
<outlet property="sortByOldestArticleOnTopMenuItem" destination="iii-kP-qoF" id="fTe-Tf-EWG"/>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -32,19 +31,11 @@
|
||||
<!--Feed-->
|
||||
<scene sceneID="vUh-Rc-fPi">
|
||||
<objects>
|
||||
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="FeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="WebFeedInspectorViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="ecA-UY-KEd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="256" height="332"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H9X-OG-K0p">
|
||||
<rect key="frame" x="104" y="264" width="48" height="48"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="48" id="1Cy-0w-dBg"/>
|
||||
<constraint firstAttribute="height" constant="48" id="edb-lw-Ict"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSNetwork" id="MZ2-89-Bje"/>
|
||||
</imageView>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5">
|
||||
<rect key="frame" x="20" y="200" width="216" height="56"/>
|
||||
<constraints>
|
||||
@@ -114,13 +105,19 @@ Field</string>
|
||||
<action selector="isNotifyAboutNewArticlesChanged:" target="sfH-oR-GXm" id="Vx9-pQ-RnP"/>
|
||||
</connections>
|
||||
</button>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="I6k-QR-VmV" customClass="IconView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="104" y="264" width="48" height="48"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="48" id="Faa-nE-lkA"/>
|
||||
<constraint firstAttribute="width" constant="48" id="esD-dT-oWU"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="zm0-15-BFy" firstAttribute="top" secondItem="2WO-Iu-p5e" secondAttribute="bottom" constant="4" id="2fb-QO-XIm"/>
|
||||
<constraint firstItem="IWu-80-XC5" firstAttribute="top" secondItem="H9X-OG-K0p" secondAttribute="bottom" constant="8" symbolic="YES" id="4WB-WJ-3Z4"/>
|
||||
<constraint firstItem="ZBX-E8-k9c" firstAttribute="top" secondItem="IWu-80-XC5" secondAttribute="bottom" constant="20" id="5L7-aZ-vdg"/>
|
||||
<constraint firstItem="nH2-ab-KJ5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="8pK-lW-xQk"/>
|
||||
<constraint firstItem="H9X-OG-K0p" firstAttribute="centerX" secondItem="ecA-UY-KEd" secondAttribute="centerX" id="9CA-KA-HEg"/>
|
||||
<constraint firstItem="IWu-80-XC5" firstAttribute="top" secondItem="I6k-QR-VmV" secondAttribute="bottom" constant="8" symbolic="YES" id="Bea-j0-QMb"/>
|
||||
<constraint firstItem="nH2-ab-KJ5" firstAttribute="top" secondItem="ZBX-E8-k9c" secondAttribute="bottom" constant="20" id="CpA-X9-EbP"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Vvk-KG-JlG" secondAttribute="bottom" constant="20" id="IxJ-5N-NhL"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ju6-Zo-8X4" secondAttribute="trailing" constant="20" symbolic="YES" id="Jzi-tP-TIw"/>
|
||||
@@ -128,11 +125,12 @@ Field</string>
|
||||
<constraint firstItem="ju6-Zo-8X4" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="NwI-2x-dAr"/>
|
||||
<constraint firstItem="ju6-Zo-8X4" firstAttribute="top" secondItem="zm0-15-BFy" secondAttribute="bottom" constant="20" id="PFv-jF-JIZ"/>
|
||||
<constraint firstItem="2WO-Iu-p5e" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="PeT-mm-2HJ"/>
|
||||
<constraint firstItem="I6k-QR-VmV" firstAttribute="top" secondItem="ecA-UY-KEd" secondAttribute="top" constant="20" symbolic="YES" id="URB-DN-7vz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="IWu-80-XC5" secondAttribute="trailing" constant="20" symbolic="YES" id="WW6-xR-Zue"/>
|
||||
<constraint firstItem="H9X-OG-K0p" firstAttribute="top" secondItem="ecA-UY-KEd" secondAttribute="top" constant="20" symbolic="YES" id="Z6q-PN-wOC"/>
|
||||
<constraint firstItem="zm0-15-BFy" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="aho-BJ-kmB"/>
|
||||
<constraint firstItem="ZBX-E8-k9c" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="cjR-0i-YNG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2WO-Iu-p5e" secondAttribute="trailing" constant="20" symbolic="YES" id="dLU-a6-nfx"/>
|
||||
<constraint firstItem="I6k-QR-VmV" firstAttribute="centerX" secondItem="ecA-UY-KEd" secondAttribute="centerX" id="gFG-ZY-eNp"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zm0-15-BFy" secondAttribute="trailing" constant="20" symbolic="YES" id="js6-b2-FIR"/>
|
||||
<constraint firstItem="IWu-80-XC5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="r6h-Z0-g7b"/>
|
||||
<constraint firstItem="2WO-Iu-p5e" firstAttribute="top" secondItem="nH2-ab-KJ5" secondAttribute="bottom" constant="20" id="rRv-qO-dPa"/>
|
||||
@@ -142,7 +140,7 @@ Field</string>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="homePageURLTextField" destination="zm0-15-BFy" id="0Jh-yy-mnF"/>
|
||||
<outlet property="imageView" destination="H9X-OG-K0p" id="Rm6-X6-csH"/>
|
||||
<outlet property="iconView" destination="I6k-QR-VmV" id="zrk-zx-zk7"/>
|
||||
<outlet property="isNotifyAboutNewArticlesCheckBox" destination="ZBX-E8-k9c" id="FWc-Ds-LUy"/>
|
||||
<outlet property="isReaderViewAlwaysOnCheckBox" destination="nH2-ab-KJ5" id="xPg-P5-3cr"/>
|
||||
<outlet property="nameTextField" destination="IWu-80-XC5" id="zg4-5h-hoP"/>
|
||||
@@ -151,7 +149,7 @@ Field</string>
|
||||
</viewController>
|
||||
<customObject id="1ho-ZO-Gkb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="67" y="69.5"/>
|
||||
<point key="canvasLocation" x="67" y="69"/>
|
||||
</scene>
|
||||
<!--Folder-->
|
||||
<scene sceneID="8By-fa-WDQ">
|
||||
@@ -294,7 +292,6 @@ Field</string>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="NSFolder" width="32" height="32"/>
|
||||
<image name="NSNetwork" width="32" height="32"/>
|
||||
<image name="NSSmartBadgeTemplate" width="14" height="14"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -10,16 +10,16 @@ import AppKit
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
final class FeedInspectorViewController: NSViewController, Inspector {
|
||||
final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet weak var imageView: NSImageView?
|
||||
@IBOutlet weak var iconView: IconView!
|
||||
@IBOutlet weak var nameTextField: NSTextField?
|
||||
@IBOutlet weak var homePageURLTextField: NSTextField?
|
||||
@IBOutlet weak var urlTextField: NSTextField?
|
||||
@IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton!
|
||||
@IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton?
|
||||
|
||||
private var feed: Feed? {
|
||||
private var feed: WebFeed? {
|
||||
didSet {
|
||||
if feed != oldValue {
|
||||
updateUI()
|
||||
@@ -37,17 +37,13 @@ final class FeedInspectorViewController: NSViewController, Inspector {
|
||||
}
|
||||
|
||||
func canInspect(_ objects: [Any]) -> Bool {
|
||||
return objects.count == 1 && objects.first is Feed
|
||||
return objects.count == 1 && objects.first is WebFeed
|
||||
}
|
||||
|
||||
// MARK: NSViewController
|
||||
|
||||
override func viewDidLoad() {
|
||||
imageView!.wantsLayer = true
|
||||
imageView!.layer?.cornerRadius = 4.0
|
||||
|
||||
updateUI()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
|
||||
}
|
||||
|
||||
@@ -68,7 +64,7 @@ final class FeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
}
|
||||
|
||||
extension FeedInspectorViewController: NSTextFieldDelegate {
|
||||
extension WebFeedInspectorViewController: NSTextFieldDelegate {
|
||||
|
||||
func controlTextDidChange(_ note: Notification) {
|
||||
guard let feed = feed, let nameTextField = nameTextField else {
|
||||
@@ -79,10 +75,10 @@ extension FeedInspectorViewController: NSTextFieldDelegate {
|
||||
|
||||
}
|
||||
|
||||
private extension FeedInspectorViewController {
|
||||
private extension WebFeedInspectorViewController {
|
||||
|
||||
func updateFeed() {
|
||||
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? Feed else {
|
||||
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? WebFeed else {
|
||||
feed = nil
|
||||
return
|
||||
}
|
||||
@@ -101,25 +97,21 @@ private extension FeedInspectorViewController {
|
||||
}
|
||||
|
||||
func updateImage() {
|
||||
guard let feed = feed else {
|
||||
imageView?.image = nil
|
||||
guard let feed = feed, let iconView = iconView else {
|
||||
return
|
||||
}
|
||||
|
||||
if let feedIcon = appDelegate.feedIconDownloader.icon(for: feed) {
|
||||
imageView?.image = feedIcon
|
||||
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
|
||||
iconView.iconImage = feedIcon
|
||||
return
|
||||
}
|
||||
|
||||
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
|
||||
if favicon.size.height < 16.0 && favicon.size.width < 16.0 {
|
||||
favicon.size = NSSize(width: 16, height: 16)
|
||||
}
|
||||
imageView?.image = favicon
|
||||
iconView.iconImage = favicon
|
||||
return
|
||||
}
|
||||
|
||||
imageView?.image = AppAssets.genericFeedImage
|
||||
iconView.iconImage = feed.smallIcon
|
||||
}
|
||||
|
||||
func updateName() {
|
||||
@@ -53,14 +53,14 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
}
|
||||
let account = accountAndFolderSpecifier.account
|
||||
|
||||
if account.hasFeed(withURL: url.absoluteString) {
|
||||
if account.hasWebFeed(withURL: url.absoluteString) {
|
||||
showAlreadySubscribedError(url.absoluteString)
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
|
||||
account.createFeed(url: url.absoluteString, name: title, container: container) { result in
|
||||
account.createWebFeed(url: url.absoluteString, name: title, container: container) { result in
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.endShowingProgress()
|
||||
@@ -70,7 +70,7 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case AccountError.createErrorAlreadySubscribed:
|
||||
|
||||
@@ -69,11 +69,16 @@ class AddFeedWindowController : NSWindowController {
|
||||
}
|
||||
|
||||
folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode)
|
||||
|
||||
if let account = initialAccount {
|
||||
FolderTreeMenu.select(account: account, folder: initialFolder, in: folderPopupButton)
|
||||
} else if let accountID = AppDefaults.addFeedAccountID {
|
||||
if let account = AccountManager.shared.existingAccount(with: accountID) {
|
||||
FolderTreeMenu.select(account: account, folder: nil, in: folderPopupButton)
|
||||
} else if let container = AddWebFeedDefaultContainer.defaultContainer {
|
||||
if let folder = container as? Folder, let account = folder.account {
|
||||
FolderTreeMenu.select(account: account, folder: folder, in: folderPopupButton)
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
FolderTreeMenu.select(account: account, folder: nil, in: folderPopupButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,11 +105,7 @@ class AddFeedWindowController : NSWindowController {
|
||||
}
|
||||
|
||||
let container = selectedContainer()!
|
||||
if let selectedAccount = container as? Account {
|
||||
AppDefaults.addFeedAccountID = selectedAccount.accountID
|
||||
} else if let selectedFolder = container as? Folder, let selectedAccount = selectedFolder.account {
|
||||
AppDefaults.addFeedAccountID = selectedAccount.accountID
|
||||
}
|
||||
AddWebFeedDefaultContainer.saveDefaultContainer(container)
|
||||
|
||||
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container)
|
||||
|
||||
|
||||
@@ -14,17 +14,16 @@ class AddFolderWindowController : NSWindowController {
|
||||
|
||||
@IBOutlet var folderNameTextField: NSTextField!
|
||||
@IBOutlet var accountPopupButton: NSPopUpButton!
|
||||
var hostWindow: NSWindow?
|
||||
@IBOutlet var addFolderButton: NSButton!
|
||||
private var hostWindow: NSWindow?
|
||||
|
||||
convenience init() {
|
||||
|
||||
self.init(windowNibName: NSNib.Name("AddFolderSheet"))
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
// MARK: - API
|
||||
|
||||
func runSheetOnWindow(_ w: NSWindow) {
|
||||
|
||||
hostWindow = w
|
||||
hostWindow!.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in
|
||||
|
||||
@@ -34,7 +33,7 @@ class AddFolderWindowController : NSWindowController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSViewController
|
||||
// MARK: - NSViewController
|
||||
|
||||
override func windowDidLoad() {
|
||||
let preferredAccountID = AppDefaults.addFolderAccountID
|
||||
@@ -53,25 +52,50 @@ class AddFolderWindowController : NSWindowController {
|
||||
if oneAccount.accountID == preferredAccountID {
|
||||
accountPopupButton.select(oneMenuItem)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func cancel(_ sender: Any?) {
|
||||
hostWindow!.endSheet(window!, returnCode: .cancel)
|
||||
}
|
||||
|
||||
@IBAction func addFolder(_ sender: Any?) {
|
||||
hostWindow!.endSheet(window!, returnCode: .OK)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Text Field Delegate
|
||||
|
||||
extension AddFolderWindowController: NSTextFieldDelegate {
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
guard let folderName = (obj.object as? NSTextField)?.stringValue else {
|
||||
addFolderButton.isEnabled = false
|
||||
return
|
||||
}
|
||||
addFolderButton.isEnabled = !folderName.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension AddFolderWindowController {
|
||||
|
||||
private func addFolderIfNeeded() {
|
||||
guard let menuItem = accountPopupButton.selectedItem else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let account = menuItem.representedObject as! Account
|
||||
AppDefaults.addFolderAccountID = account.accountID
|
||||
|
||||
|
||||
let folderName = self.folderNameTextField.stringValue
|
||||
if folderName.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
account.addFolder(folderName) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -80,19 +104,5 @@ class AddFolderWindowController : NSWindowController {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func cancel(_ sender: Any?) {
|
||||
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
|
||||
}
|
||||
|
||||
@IBAction func addFolder(_ sender: Any?) {
|
||||
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,7 +59,15 @@ class ArticleExtractorButton: NSButton {
|
||||
case isInProgress:
|
||||
addAnimatedSublayer(to: hostedLayer)
|
||||
default:
|
||||
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity)
|
||||
if NSApplication.shared.isActive {
|
||||
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity)
|
||||
} else {
|
||||
if NSApplication.shared.effectiveAppearance.isDarkMode {
|
||||
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorInactiveDark, opacity: opacity)
|
||||
} else {
|
||||
addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorInactiveLight, opacity: opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate {
|
||||
}
|
||||
#endif
|
||||
|
||||
private let articleIconSchemeHandler = ArticleIconSchemeHandler()
|
||||
private var waitingForFirstReload = false
|
||||
private let keyboardDelegate = DetailKeyboardDelegate()
|
||||
|
||||
@@ -61,12 +62,11 @@ final class DetailWebViewController: NSViewController, WKUIDelegate {
|
||||
let preferences = WKPreferences()
|
||||
preferences.minimumFontSize = 12.0
|
||||
preferences.javaScriptCanOpenWindowsAutomatically = false
|
||||
preferences.javaEnabled = false
|
||||
preferences.javaScriptEnabled = true
|
||||
preferences.plugInsEnabled = false
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.preferences = preferences
|
||||
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
|
||||
|
||||
let userContentController = WKUserContentController()
|
||||
userContentController.add(self, name: MessageName.mouseDidEnter)
|
||||
@@ -100,14 +100,31 @@ final class DetailWebViewController: NSViewController, WKUIDelegate {
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
webInspectorEnabled = AppDefaults.webInspectorEnabled
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webInspectorEnabledDidChange(_:)), name: .WebInspectorEnabledDidChange, object: nil)
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
|
||||
webView.loadHTMLString(ArticleRenderer.page.html, baseURL: ArticleRenderer.page.baseURL)
|
||||
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
// MARK: Scrolling
|
||||
|
||||
func canScrollDown(_ callback: @escaping (Bool) -> Void) {
|
||||
@@ -175,6 +192,10 @@ struct TemplateData: Codable {
|
||||
|
||||
private extension DetailWebViewController {
|
||||
|
||||
func reloadArticleImage() {
|
||||
webView.evaluateJavaScript("reloadArticleImage()")
|
||||
}
|
||||
|
||||
func reloadHTML() {
|
||||
let style = ArticleStylesManager.shared.currentStyle
|
||||
let rendering: ArticleRenderer.Rendering
|
||||
@@ -187,8 +208,10 @@ private extension DetailWebViewController {
|
||||
case .loading:
|
||||
rendering = ArticleRenderer.loadingHTML(style: style)
|
||||
case .article(let article):
|
||||
articleIconSchemeHandler.currentArticle = article
|
||||
rendering = ArticleRenderer.articleHTML(article: article, style: style)
|
||||
case .extracted(let article, let extractedArticle):
|
||||
articleIconSchemeHandler.currentArticle = article
|
||||
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// TimelineAvatarView.swift
|
||||
// TimelineIconView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 9/15/19.
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class TimelineAvatarView: NSView {
|
||||
final class IconView: NSView {
|
||||
|
||||
var image: NSImage? = nil {
|
||||
var iconImage: IconImage? = nil {
|
||||
didSet {
|
||||
if image !== oldValue {
|
||||
imageView.image = image
|
||||
if iconImage !== oldValue {
|
||||
imageView.image = iconImage?.image
|
||||
needsDisplay = true
|
||||
needsLayout = true
|
||||
}
|
||||
@@ -36,8 +36,8 @@ final class TimelineAvatarView: NSView {
|
||||
return imageView.frame.size.height < bounds.size.height
|
||||
}
|
||||
|
||||
private static var lightBackgroundColor = AppAssets.avatarLightBackgroundColor
|
||||
private static var darkBackgroundColor = AppAssets.avatarDarkBackgroundColor
|
||||
private static var lightBackgroundColor = AppAssets.iconLightBackgroundColor
|
||||
private static var darkBackgroundColor = AppAssets.iconDarkBackgroundColor
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
@@ -71,21 +71,22 @@ final class TimelineAvatarView: NSView {
|
||||
return
|
||||
}
|
||||
|
||||
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? TimelineAvatarView.darkBackgroundColor : TimelineAvatarView.lightBackgroundColor
|
||||
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor
|
||||
color.set()
|
||||
dirtyRect.fill()
|
||||
}
|
||||
}
|
||||
|
||||
private extension TimelineAvatarView {
|
||||
private extension IconView {
|
||||
|
||||
func commonInit() {
|
||||
addSubview(imageView)
|
||||
wantsLayer = true
|
||||
layer?.cornerRadius = 4.0
|
||||
}
|
||||
|
||||
func rectForImageView() -> NSRect {
|
||||
guard let image = image else {
|
||||
guard let image = iconImage?.image else {
|
||||
return NSRect.zero
|
||||
}
|
||||
|
||||
@@ -117,14 +117,16 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
|
||||
func handle(_ response: UNNotificationResponse) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: userInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: userInfo)
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
}
|
||||
|
||||
func handle(_ activity: NSUserActivity) {
|
||||
guard let userInfo = activity.userInfo else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: userInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: userInfo)
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
@@ -170,7 +172,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
}
|
||||
}
|
||||
|
||||
if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed {
|
||||
if let feed = currentFeedOrFolder as? WebFeed, let noteObject = noteObject as? WebFeed {
|
||||
if feed == noteObject {
|
||||
updateWindowTitle()
|
||||
return
|
||||
@@ -479,8 +481,8 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
|
||||
let detailState: DetailState
|
||||
if let articles = articles {
|
||||
if articles.count == 1 {
|
||||
activityManager.reading(articles.first!)
|
||||
if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false {
|
||||
activityManager.reading(feed: nil, article: articles.first)
|
||||
if articles.first?.webFeed?.isArticleExtractorAlwaysOn ?? false {
|
||||
detailState = .loading
|
||||
startArticleExtractorForCurrentLink()
|
||||
} else {
|
||||
|
||||
@@ -32,7 +32,6 @@ import RSCore
|
||||
}()
|
||||
|
||||
static func customSharingServices(for items: [Any]) -> [NSSharingService] {
|
||||
|
||||
let customServices = sendToCommands.compactMap { (sendToCommand) -> NSSharingService? in
|
||||
|
||||
guard let object = items.first else {
|
||||
@@ -42,7 +41,7 @@ import RSCore
|
||||
return nil
|
||||
}
|
||||
|
||||
let image = sendToCommand.image ?? AppAssets.genericFeedImage ?? NSImage()
|
||||
let image = sendToCommand.image ?? NSImage()
|
||||
return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) {
|
||||
sendToCommand.sendObject(object, selectedText: nil)
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ class SidebarCell : NSTableCellView {
|
||||
}()
|
||||
|
||||
private let faviconImageView: NSImageView = {
|
||||
let image = AppAssets.genericFeedImage
|
||||
let imageView = image != nil ? NSImageView(image: image!) : NSImageView(frame: NSRect.zero)
|
||||
let imageView = NSImageView(frame: NSRect.zero)
|
||||
imageView.animates = false
|
||||
imageView.imageAlignment = .alignCenter
|
||||
imageView.imageScaling = .scaleProportionallyDown
|
||||
|
||||
@@ -52,7 +52,7 @@ struct PasteboardFolder: Hashable {
|
||||
}
|
||||
|
||||
if let foundType = pasteboardType {
|
||||
if let folderDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardFeedDictionary {
|
||||
if let folderDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardWebFeedDictionary {
|
||||
self.init(dictionary: folderDictionary)
|
||||
return
|
||||
}
|
||||
@@ -72,7 +72,7 @@ struct PasteboardFolder: Hashable {
|
||||
// MARK: - Writing
|
||||
|
||||
func internalDictionary() -> PasteboardFolderDictionary {
|
||||
var d = PasteboardFeedDictionary()
|
||||
var d = PasteboardWebFeedDictionary()
|
||||
d[PasteboardFolder.Key.name] = name
|
||||
if let folderID = folderID {
|
||||
d[PasteboardFolder.Key.folderID] = folderID
|
||||
@@ -131,7 +131,7 @@ private extension FolderPasteboardWriter {
|
||||
return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID)
|
||||
}
|
||||
|
||||
var internalDictionary: PasteboardFeedDictionary {
|
||||
var internalDictionary: PasteboardWebFeedDictionary {
|
||||
return pasteboardFolder.internalDictionary()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import Articles
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
typealias PasteboardFeedDictionary = [String: String]
|
||||
typealias PasteboardWebFeedDictionary = [String: String]
|
||||
|
||||
struct PasteboardFeed: Hashable {
|
||||
struct PasteboardWebFeed: Hashable {
|
||||
|
||||
private struct Key {
|
||||
static let url = "URL"
|
||||
@@ -23,12 +23,12 @@ struct PasteboardFeed: Hashable {
|
||||
// Internal
|
||||
static let accountID = "accountID"
|
||||
static let accountType = "accountType"
|
||||
static let feedID = "feedID"
|
||||
static let webFeedID = "webFeedID"
|
||||
static let editedName = "editedName"
|
||||
}
|
||||
|
||||
let url: String
|
||||
let feedID: String?
|
||||
let webFeedID: String?
|
||||
let homePageURL: String?
|
||||
let name: String?
|
||||
let editedName: String?
|
||||
@@ -36,9 +36,9 @@ struct PasteboardFeed: Hashable {
|
||||
let accountType: AccountType?
|
||||
let isLocalFeed: Bool
|
||||
|
||||
init(url: String, feedID: String?, homePageURL: String?, name: String?, editedName: String?, accountID: String?, accountType: AccountType?) {
|
||||
init(url: String, webFeedID: String?, homePageURL: String?, name: String?, editedName: String?, accountID: String?, accountType: AccountType?) {
|
||||
self.url = url.rs_normalizedURL()
|
||||
self.feedID = feedID
|
||||
self.webFeedID = webFeedID
|
||||
self.homePageURL = homePageURL?.rs_normalizedURL()
|
||||
self.name = name
|
||||
self.editedName = editedName
|
||||
@@ -49,7 +49,7 @@ struct PasteboardFeed: Hashable {
|
||||
|
||||
// MARK: - Reading
|
||||
|
||||
init?(dictionary: PasteboardFeedDictionary) {
|
||||
init?(dictionary: PasteboardWebFeedDictionary) {
|
||||
guard let url = dictionary[Key.url] else {
|
||||
return nil
|
||||
}
|
||||
@@ -57,7 +57,7 @@ struct PasteboardFeed: Hashable {
|
||||
let homePageURL = dictionary[Key.homePageURL]
|
||||
let name = dictionary[Key.name]
|
||||
let accountID = dictionary[Key.accountID]
|
||||
let feedID = dictionary[Key.feedID]
|
||||
let webFeedID = dictionary[Key.webFeedID]
|
||||
let editedName = dictionary[Key.editedName]
|
||||
|
||||
var accountType: AccountType? = nil
|
||||
@@ -65,19 +65,19 @@ struct PasteboardFeed: Hashable {
|
||||
accountType = AccountType(rawValue: accountTypeInt)
|
||||
}
|
||||
|
||||
self.init(url: url, feedID: feedID, homePageURL: homePageURL, name: name, editedName: editedName, accountID: accountID, accountType: accountType)
|
||||
self.init(url: url, webFeedID: webFeedID, homePageURL: homePageURL, name: name, editedName: editedName, accountID: accountID, accountType: accountType)
|
||||
}
|
||||
|
||||
init?(pasteboardItem: NSPasteboardItem) {
|
||||
var pasteboardType: NSPasteboard.PasteboardType?
|
||||
if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIInternalType) {
|
||||
pasteboardType = FeedPasteboardWriter.feedUTIInternalType
|
||||
if pasteboardItem.types.contains(WebFeedPasteboardWriter.webFeedUTIInternalType) {
|
||||
pasteboardType = WebFeedPasteboardWriter.webFeedUTIInternalType
|
||||
}
|
||||
else if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIType) {
|
||||
pasteboardType = FeedPasteboardWriter.feedUTIType
|
||||
else if pasteboardItem.types.contains(WebFeedPasteboardWriter.webFeedUTIType) {
|
||||
pasteboardType = WebFeedPasteboardWriter.webFeedUTIType
|
||||
}
|
||||
if let foundType = pasteboardType {
|
||||
if let feedDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardFeedDictionary {
|
||||
if let feedDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardWebFeedDictionary {
|
||||
self.init(dictionary: feedDictionary)
|
||||
return
|
||||
}
|
||||
@@ -94,7 +94,7 @@ struct PasteboardFeed: Hashable {
|
||||
if let foundType = pasteboardType {
|
||||
if let possibleURLString = pasteboardItem.string(forType: foundType) {
|
||||
if possibleURLString.rs_stringMayBeURL() {
|
||||
self.init(url: possibleURLString, feedID: nil, homePageURL: nil, name: nil, editedName: nil, accountID: nil, accountType: nil)
|
||||
self.init(url: possibleURLString, webFeedID: nil, homePageURL: nil, name: nil, editedName: nil, accountID: nil, accountType: nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -103,18 +103,18 @@ struct PasteboardFeed: Hashable {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func pasteboardFeeds(with pasteboard: NSPasteboard) -> Set<PasteboardFeed>? {
|
||||
static func pasteboardFeeds(with pasteboard: NSPasteboard) -> Set<PasteboardWebFeed>? {
|
||||
guard let items = pasteboard.pasteboardItems else {
|
||||
return nil
|
||||
}
|
||||
let feeds = items.compactMap { PasteboardFeed(pasteboardItem: $0) }
|
||||
return feeds.isEmpty ? nil : Set(feeds)
|
||||
let webFeeds = items.compactMap { PasteboardWebFeed(pasteboardItem: $0) }
|
||||
return webFeeds.isEmpty ? nil : Set(webFeeds)
|
||||
}
|
||||
|
||||
// MARK: - Writing
|
||||
|
||||
func exportDictionary() -> PasteboardFeedDictionary {
|
||||
var d = PasteboardFeedDictionary()
|
||||
func exportDictionary() -> PasteboardWebFeedDictionary {
|
||||
var d = PasteboardWebFeedDictionary()
|
||||
d[Key.url] = url
|
||||
d[Key.homePageURL] = homePageURL ?? ""
|
||||
if let nameForDisplay = editedName ?? name {
|
||||
@@ -123,54 +123,54 @@ struct PasteboardFeed: Hashable {
|
||||
return d
|
||||
}
|
||||
|
||||
func internalDictionary() -> PasteboardFeedDictionary {
|
||||
var d = PasteboardFeedDictionary()
|
||||
d[PasteboardFeed.Key.feedID] = feedID
|
||||
d[PasteboardFeed.Key.url] = url
|
||||
func internalDictionary() -> PasteboardWebFeedDictionary {
|
||||
var d = PasteboardWebFeedDictionary()
|
||||
d[PasteboardWebFeed.Key.webFeedID] = webFeedID
|
||||
d[PasteboardWebFeed.Key.url] = url
|
||||
if let homePageURL = homePageURL {
|
||||
d[PasteboardFeed.Key.homePageURL] = homePageURL
|
||||
d[PasteboardWebFeed.Key.homePageURL] = homePageURL
|
||||
}
|
||||
if let name = name {
|
||||
d[PasteboardFeed.Key.name] = name
|
||||
d[PasteboardWebFeed.Key.name] = name
|
||||
}
|
||||
if let editedName = editedName {
|
||||
d[PasteboardFeed.Key.editedName] = editedName
|
||||
d[PasteboardWebFeed.Key.editedName] = editedName
|
||||
}
|
||||
if let accountID = accountID {
|
||||
d[PasteboardFeed.Key.accountID] = accountID
|
||||
d[PasteboardWebFeed.Key.accountID] = accountID
|
||||
}
|
||||
if let accountType = accountType {
|
||||
d[PasteboardFeed.Key.accountType] = String(accountType.rawValue)
|
||||
d[PasteboardWebFeed.Key.accountType] = String(accountType.rawValue)
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
extension Feed: PasteboardWriterOwner {
|
||||
extension WebFeed: PasteboardWriterOwner {
|
||||
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
return FeedPasteboardWriter(feed: self)
|
||||
return WebFeedPasteboardWriter(webFeed: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
@objc final class WebFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let feed: Feed
|
||||
static let feedUTI = "com.ranchero.feed"
|
||||
static let feedUTIType = NSPasteboard.PasteboardType(rawValue: feedUTI)
|
||||
static let feedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.feed"
|
||||
static let feedUTIInternalType = NSPasteboard.PasteboardType(rawValue: feedUTIInternal)
|
||||
private let webFeed: WebFeed
|
||||
static let webFeedUTI = "com.ranchero.webFeed"
|
||||
static let webFeedUTIType = NSPasteboard.PasteboardType(rawValue: webFeedUTI)
|
||||
static let webFeedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.webFeed"
|
||||
static let webFeedUTIInternalType = NSPasteboard.PasteboardType(rawValue: webFeedUTIInternal)
|
||||
|
||||
|
||||
init(feed: Feed) {
|
||||
self.feed = feed
|
||||
init(webFeed: WebFeed) {
|
||||
self.webFeed = webFeed
|
||||
}
|
||||
|
||||
// MARK: - NSPasteboardWriting
|
||||
|
||||
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
||||
|
||||
return [FeedPasteboardWriter.feedUTIType, .URL, .string, FeedPasteboardWriter.feedUTIInternalType]
|
||||
return [WebFeedPasteboardWriter.webFeedUTIType, .URL, .string, WebFeedPasteboardWriter.webFeedUTIInternalType]
|
||||
}
|
||||
|
||||
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
|
||||
@@ -179,12 +179,12 @@ extension Feed: PasteboardWriterOwner {
|
||||
|
||||
switch type {
|
||||
case .string:
|
||||
plist = feed.nameForDisplay
|
||||
plist = webFeed.nameForDisplay
|
||||
case .URL:
|
||||
plist = feed.url
|
||||
case FeedPasteboardWriter.feedUTIType:
|
||||
plist = webFeed.url
|
||||
case WebFeedPasteboardWriter.webFeedUTIType:
|
||||
plist = exportDictionary
|
||||
case FeedPasteboardWriter.feedUTIInternalType:
|
||||
case WebFeedPasteboardWriter.webFeedUTIInternalType:
|
||||
plist = internalDictionary
|
||||
default:
|
||||
plist = nil
|
||||
@@ -194,17 +194,17 @@ extension Feed: PasteboardWriterOwner {
|
||||
}
|
||||
}
|
||||
|
||||
private extension FeedPasteboardWriter {
|
||||
private extension WebFeedPasteboardWriter {
|
||||
|
||||
var pasteboardFeed: PasteboardFeed {
|
||||
return PasteboardFeed(url: feed.url, feedID: feed.feedID, homePageURL: feed.homePageURL, name: feed.name, editedName: feed.editedName, accountID: feed.account?.accountID, accountType: feed.account?.type)
|
||||
var pasteboardFeed: PasteboardWebFeed {
|
||||
return PasteboardWebFeed(url: webFeed.url, webFeedID: webFeed.webFeedID, homePageURL: webFeed.homePageURL, name: webFeed.name, editedName: webFeed.editedName, accountID: webFeed.account?.accountID, accountType: webFeed.account?.type)
|
||||
}
|
||||
|
||||
var exportDictionary: PasteboardFeedDictionary {
|
||||
var exportDictionary: PasteboardWebFeedDictionary {
|
||||
return pasteboardFeed.exportDictionary()
|
||||
}
|
||||
|
||||
var internalDictionary: PasteboardFeedDictionary {
|
||||
var internalDictionary: PasteboardWebFeedDictionary {
|
||||
return pasteboardFeed.internalDictionary()
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ import Account
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardWebFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
@@ -91,7 +91,7 @@ import Account
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardWebFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return false
|
||||
}
|
||||
@@ -136,7 +136,7 @@ private extension SidebarOutlineDataSource {
|
||||
// Don’t allow PseudoFeed to be dragged.
|
||||
// This will have to be revisited later. For instance,
|
||||
// user-created smart feeds should be draggable, maybe.
|
||||
return node.representedObject is Folder || node.representedObject is Feed
|
||||
return node.representedObject is Folder || node.representedObject is WebFeed
|
||||
}
|
||||
|
||||
// MARK: - Drag and Drop
|
||||
@@ -145,7 +145,7 @@ private extension SidebarOutlineDataSource {
|
||||
case empty, singleLocal, singleNonLocal, multipleLocal, multipleNonLocal, mixed
|
||||
}
|
||||
|
||||
func draggedFeedContentsType(_ draggedFeeds: Set<PasteboardFeed>) -> DraggedFeedsContentsType {
|
||||
func draggedFeedContentsType(_ draggedFeeds: Set<PasteboardWebFeed>) -> DraggedFeedsContentsType {
|
||||
if draggedFeeds.isEmpty {
|
||||
return .empty
|
||||
}
|
||||
@@ -173,14 +173,14 @@ private extension SidebarOutlineDataSource {
|
||||
return .multipleNonLocal
|
||||
}
|
||||
|
||||
func singleNonLocalFeed(from feeds: Set<PasteboardFeed>) -> PasteboardFeed? {
|
||||
func singleNonLocalFeed(from feeds: Set<PasteboardWebFeed>) -> PasteboardWebFeed? {
|
||||
guard feeds.count == 1, let feed = feeds.first else {
|
||||
return nil
|
||||
}
|
||||
return feed.isLocalFeed ? nil : feed
|
||||
}
|
||||
|
||||
func validateSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
func validateSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
// A non-local feed should always drag on to an Account or Folder node, with NSOutlineViewDropOnItemIndex — since we don’t know where it would sort till we read the feed.
|
||||
guard let dropTargetNode = ancestorThatCanAcceptNonLocalFeed(parentNode) else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
@@ -191,7 +191,7 @@ private extension SidebarOutlineDataSource {
|
||||
return .copy
|
||||
}
|
||||
|
||||
func validateSingleLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
func validateSingleLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
// A local feed should always drag on to an Account or Folder node, and we can provide an index.
|
||||
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
@@ -212,7 +212,7 @@ private extension SidebarOutlineDataSource {
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardWebFeed>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
// Local feeds should always drag on to an Account or Folder node, and index should be NSOutlineViewDropOnItemIndex since we can’t provide multiple indexes.
|
||||
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
@@ -244,7 +244,7 @@ private extension SidebarOutlineDataSource {
|
||||
if let folder = node.representedObject as? Folder {
|
||||
return folder.account
|
||||
}
|
||||
if let feed = node.representedObject as? Feed {
|
||||
if let feed = node.representedObject as? WebFeed {
|
||||
return feed.account
|
||||
}
|
||||
return nil
|
||||
@@ -303,12 +303,12 @@ private extension SidebarOutlineDataSource {
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func copyFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
|
||||
func copyWebFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? WebFeed, let destination = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
destination.account?.addFeed(feed, to: destination) { result in
|
||||
destination.account?.addWebFeed(feed, to: destination) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
@@ -318,15 +318,15 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed,
|
||||
func moveWebFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? WebFeed,
|
||||
let source = node.parent?.representedObject as? Container,
|
||||
let destination = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
source.account?.moveFeed(feed, from: source, to: destination) { result in
|
||||
source.account?.moveWebFeed(feed, from: source, to: destination) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -337,15 +337,15 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
func copyFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed,
|
||||
func copyWebFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? WebFeed,
|
||||
let destinationAccount = nodeAccount(parentNode),
|
||||
let destinationContainer = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
if let existingFeed = destinationAccount.existingWebFeed(withURL: feed.url) {
|
||||
destinationAccount.addWebFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
@@ -354,7 +354,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
||||
destinationAccount.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
@@ -365,8 +365,8 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed,
|
||||
func moveWebFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? WebFeed,
|
||||
let sourceAccount = nodeAccount(node),
|
||||
let sourceContainer = node.parent?.representedObject as? Container,
|
||||
let destinationAccount = nodeAccount(parentNode),
|
||||
@@ -374,13 +374,13 @@ private extension SidebarOutlineDataSource {
|
||||
return
|
||||
}
|
||||
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
if let existingFeed = destinationAccount.existingWebFeed(withURL: feed.url) {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
destinationAccount.addWebFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceAccount.removeFeed(feed, from: sourceContainer) { result in
|
||||
sourceAccount.removeWebFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -398,10 +398,10 @@ private extension SidebarOutlineDataSource {
|
||||
} else {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
||||
destinationAccount.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceAccount.removeFeed(feed, from: sourceContainer) { result in
|
||||
sourceAccount.removeWebFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -419,7 +419,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
func acceptLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
func acceptLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardWebFeed>, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
guard let draggedNodes = draggedNodes else {
|
||||
return false
|
||||
}
|
||||
@@ -427,15 +427,15 @@ private extension SidebarOutlineDataSource {
|
||||
draggedNodes.forEach { node in
|
||||
if sameAccount(node, parentNode) {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
copyFeedInAccount(node: node, to: parentNode)
|
||||
copyWebFeedInAccount(node: node, to: parentNode)
|
||||
} else {
|
||||
moveFeedInAccount(node: node, to: parentNode)
|
||||
moveWebFeedInAccount(node: node, to: parentNode)
|
||||
}
|
||||
} else {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
copyFeedBetweenAccounts(node: node, to: parentNode)
|
||||
copyWebFeedBetweenAccounts(node: node, to: parentNode)
|
||||
} else {
|
||||
moveFeedBetweenAccounts(node: node, to: parentNode)
|
||||
moveWebFeedBetweenAccounts(node: node, to: parentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,10 +509,10 @@ private extension SidebarOutlineDataSource {
|
||||
switch result {
|
||||
case .success(let destinationFolder):
|
||||
let group = DispatchGroup()
|
||||
for feed in folder.topLevelFeeds {
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
if let existingFeed = destinationAccount.existingWebFeed(withURL: feed.url) {
|
||||
group.enter()
|
||||
destinationAccount.addFeed(existingFeed, to: destinationFolder) { result in
|
||||
destinationAccount.addWebFeed(existingFeed, to: destinationFolder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -523,7 +523,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
} else {
|
||||
group.enter()
|
||||
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationFolder) { result in
|
||||
destinationAccount.createWebFeed(url: feed.url, name: feed.editedName, container: destinationFolder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -563,7 +563,7 @@ private extension SidebarOutlineDataSource {
|
||||
return true
|
||||
}
|
||||
|
||||
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
||||
return false
|
||||
}
|
||||
@@ -580,12 +580,12 @@ private extension SidebarOutlineDataSource {
|
||||
return true
|
||||
}
|
||||
|
||||
func nodeHasChildRepresentingDraggedFeed(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
|
||||
func nodeHasChildRepresentingDraggedFeed(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Bool {
|
||||
return nodeHasChildRepresentingAnyDraggedFeed(parentNode, Set([draggedFeed]))
|
||||
}
|
||||
|
||||
func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
guard let feed = node.representedObject as? Feed else {
|
||||
func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||
guard let feed = node.representedObject as? WebFeed else {
|
||||
return false
|
||||
}
|
||||
for draggedFeed in draggedFeeds {
|
||||
@@ -610,8 +610,8 @@ private extension SidebarOutlineDataSource {
|
||||
return account
|
||||
} else if let folder = node.representedObject as? Folder {
|
||||
return folder.account
|
||||
} else if let feed = node.representedObject as? Feed {
|
||||
return feed.account
|
||||
} else if let webFeed = node.representedObject as? WebFeed {
|
||||
return webFeed.account
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@@ -622,7 +622,7 @@ private extension SidebarOutlineDataSource {
|
||||
return nodeAccount(node)?.accountID
|
||||
}
|
||||
|
||||
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||
for node in parentNode.childNodes {
|
||||
if nodeRepresentsAnyDraggedFeed(node, draggedFeeds) {
|
||||
return true
|
||||
@@ -631,11 +631,11 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
|
||||
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Bool {
|
||||
return violatesAccountSpecificBehavior(parentNode, Set([draggedFeed]))
|
||||
}
|
||||
|
||||
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||
if violatesDisallowFeedInRootFolder(parentNode) {
|
||||
return true
|
||||
}
|
||||
@@ -659,7 +659,7 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
func violatesDisallowFeedCopyInRootFolder(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
func violatesDisallowFeedCopyInRootFolder(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||
guard let parentAccount = nodeAccount(parentNode), parentAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else {
|
||||
return false
|
||||
}
|
||||
@@ -677,7 +677,7 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Int {
|
||||
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Int {
|
||||
let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed)
|
||||
let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil)
|
||||
let nodes = parentNode.childNodes + [draggedFeedNode]
|
||||
@@ -706,9 +706,9 @@ final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
||||
var nameForDisplay: String {
|
||||
return pasteboardFeed.editedName ?? pasteboardFeed.name ?? ""
|
||||
}
|
||||
let pasteboardFeed: PasteboardFeed
|
||||
let pasteboardFeed: PasteboardWebFeed
|
||||
|
||||
init(pasteboardFeed: PasteboardFeed) {
|
||||
init(pasteboardFeed: PasteboardWebFeed) {
|
||||
self.pasteboardFeed = pasteboardFeed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ extension SidebarViewController {
|
||||
let object = objects.first!
|
||||
|
||||
switch object {
|
||||
case is Feed:
|
||||
return menuForFeed(object as! Feed)
|
||||
case is WebFeed:
|
||||
return menuForWebFeed(object as! WebFeed)
|
||||
case is Folder:
|
||||
return menuForFolder(object as! Folder)
|
||||
case is PseudoFeed:
|
||||
@@ -83,7 +83,7 @@ extension SidebarViewController {
|
||||
|
||||
@objc func renameFromContextualMenu(_ sender: Any?) {
|
||||
|
||||
guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is Feed || object is Folder else {
|
||||
guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is WebFeed || object is Folder else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ extension SidebarViewController: RenameWindowControllerDelegate {
|
||||
|
||||
func renameWindowController(_ windowController: RenameWindowController, didRenameObject object: Any, withNewName name: String) {
|
||||
|
||||
if let feed = object as? Feed {
|
||||
if let feed = object as? WebFeed {
|
||||
feed.rename(to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -135,32 +135,32 @@ private extension SidebarViewController {
|
||||
return menu
|
||||
}
|
||||
|
||||
func menuForFeed(_ feed: Feed) -> NSMenu? {
|
||||
func menuForWebFeed(_ webFeed: WebFeed) -> NSMenu? {
|
||||
|
||||
let menu = NSMenu(title: "")
|
||||
|
||||
if feed.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([feed]))
|
||||
if webFeed.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([webFeed]))
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
if let homePageURL = feed.homePageURL, let _ = URL(string: homePageURL) {
|
||||
if let homePageURL = webFeed.homePageURL, let _ = URL(string: homePageURL) {
|
||||
let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL)
|
||||
menu.addItem(item)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), feed.url)
|
||||
let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), webFeed.url)
|
||||
menu.addItem(copyFeedURLItem)
|
||||
|
||||
if let homePageURL = feed.homePageURL {
|
||||
if let homePageURL = webFeed.homePageURL {
|
||||
let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL)
|
||||
menu.addItem(item)
|
||||
}
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
menu.addItem(renameMenuItem(feed))
|
||||
menu.addItem(deleteMenuItem([feed]))
|
||||
menu.addItem(renameMenuItem(webFeed))
|
||||
menu.addItem(deleteMenuItem([webFeed]))
|
||||
|
||||
return menu
|
||||
}
|
||||
@@ -245,7 +245,7 @@ private extension SidebarViewController {
|
||||
|
||||
func objectIsFeedOrFolder(_ object: Any) -> Bool {
|
||||
|
||||
return object is Feed || object is Folder
|
||||
return object is WebFeed || object is Folder
|
||||
}
|
||||
|
||||
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {
|
||||
|
||||
@@ -23,7 +23,7 @@ protocol SidebarDelegate: class {
|
||||
|
||||
weak var delegate: SidebarDelegate?
|
||||
|
||||
let treeControllerDelegate = FeedTreeControllerDelegate()
|
||||
let treeControllerDelegate = WebFeedTreeControllerDelegate()
|
||||
lazy var treeController: TreeController = {
|
||||
return TreeController(delegate: treeControllerDelegate)
|
||||
}()
|
||||
@@ -49,7 +49,7 @@ protocol SidebarDelegate: class {
|
||||
outlineView.dataSource = dataSource
|
||||
outlineView.doubleAction = #selector(doubleClickedSidebar(_:))
|
||||
outlineView.setDraggingSourceOperationMask([.move, .copy], forLocal: true)
|
||||
outlineView.registerForDraggedTypes([FeedPasteboardWriter.feedUTIInternalType, FeedPasteboardWriter.feedUTIType, .URL, .string])
|
||||
outlineView.registerForDraggedTypes([WebFeedPasteboardWriter.webFeedUTIInternalType, WebFeedPasteboardWriter.webFeedUTIType, .URL, .string])
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||
@@ -59,7 +59,7 @@ protocol SidebarDelegate: class {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDidRequestSidebarSelection(_:)), name: .UserDidRequestSidebarSelection, object: nil)
|
||||
|
||||
@@ -110,7 +110,7 @@ protocol SidebarDelegate: class {
|
||||
}
|
||||
|
||||
@objc func userDidAddFeed(_ notification: Notification) {
|
||||
guard let feed = notification.userInfo?[UserInfoKey.feed] else {
|
||||
guard let feed = notification.userInfo?[UserInfoKey.webFeed] else {
|
||||
return
|
||||
}
|
||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||
@@ -120,12 +120,12 @@ protocol SidebarDelegate: class {
|
||||
applyToAvailableCells(configureFavicon)
|
||||
}
|
||||
|
||||
@objc func feedSettingDidChange(_ note: Notification) {
|
||||
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
|
||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||
guard let webFeed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
||||
return
|
||||
}
|
||||
if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
|
||||
configureCellsForRepresentedObject(feed)
|
||||
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
||||
configureCellsForRepresentedObject(webFeed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ protocol SidebarDelegate: class {
|
||||
}
|
||||
|
||||
@objc func userDidRequestSidebarSelection(_ note: Notification) {
|
||||
guard let feed = note.userInfo?[UserInfoKey.feed] else {
|
||||
guard let feed = note.userInfo?[UserInfoKey.webFeed] else {
|
||||
return
|
||||
}
|
||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||
@@ -370,11 +370,11 @@ private extension SidebarViewController {
|
||||
return selectedNodes.first!
|
||||
}
|
||||
|
||||
var singleSelectedFeed: Feed? {
|
||||
var singleSelectedFeed: WebFeed? {
|
||||
guard let node = singleSelectedNode else {
|
||||
return nil
|
||||
}
|
||||
return node.representedObject as? Feed
|
||||
return node.representedObject as? WebFeed
|
||||
}
|
||||
|
||||
func rebuildTreeAndReloadDataIfNeeded() {
|
||||
@@ -413,11 +413,11 @@ private extension SidebarViewController {
|
||||
// For feeds, actually fetch from database.
|
||||
|
||||
for object in objects {
|
||||
if let feed = object as? Feed, let account = feed.account {
|
||||
if let feed = object as? WebFeed, let account = feed.account {
|
||||
account.updateUnreadCounts(for: Set([feed]))
|
||||
}
|
||||
else if let folder = object as? Folder, let account = folder.account {
|
||||
account.updateUnreadCounts(for: folder.flattenedFeeds())
|
||||
account.updateUnreadCounts(for: folder.flattenedWebFeeds())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,7 +484,7 @@ private extension SidebarViewController {
|
||||
}
|
||||
|
||||
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
|
||||
guard let accountID = userInfo?[DeepLinkKey.accountID.rawValue] as? String else {
|
||||
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -492,7 +492,7 @@ private extension SidebarViewController {
|
||||
return node
|
||||
}
|
||||
|
||||
guard let accountName = userInfo?[DeepLinkKey.accountName.rawValue] as? String else {
|
||||
guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -504,10 +504,10 @@ private extension SidebarViewController {
|
||||
}
|
||||
|
||||
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
||||
guard let feedID = userInfo?[DeepLinkKey.feedID.rawValue] as? String else {
|
||||
guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else {
|
||||
return nil
|
||||
}
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) {
|
||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) {
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
@@ -526,14 +526,14 @@ private extension SidebarViewController {
|
||||
}
|
||||
|
||||
func configureFavicon(_ cell: SidebarCell, _ node: Node) {
|
||||
cell.image = imageFor(node)
|
||||
cell.image = imageFor(node)?.image
|
||||
}
|
||||
|
||||
func configureGroupCell(_ cell: NSTableCellView, _ node: Node) {
|
||||
cell.textField?.stringValue = nameFor(node)
|
||||
}
|
||||
|
||||
func imageFor(_ node: Node) -> NSImage? {
|
||||
func imageFor(_ node: Node) -> IconImage? {
|
||||
if let smallIconProvider = node.representedObject as? SmallIconProvider {
|
||||
return smallIconProvider.smallIcon
|
||||
}
|
||||
@@ -620,7 +620,7 @@ private extension Node {
|
||||
if representedObject === object {
|
||||
return true
|
||||
}
|
||||
if let feed1 = object as? Feed, let feed2 = representedObject as? Feed {
|
||||
if let feed1 = object as? WebFeed, let feed2 = representedObject as? WebFeed {
|
||||
return feed1 == feed2
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -91,7 +91,7 @@ private extension ArticlePasteboardWriter {
|
||||
|
||||
s += "Date: \(article.logicalDatePublished)\n\n"
|
||||
|
||||
if let feed = article.feed {
|
||||
if let feed = article.webFeed {
|
||||
s += "Feed: \(feed.nameForDisplay)\n"
|
||||
if let homePageURL = feed.homePageURL {
|
||||
s += "Home page: \(homePageURL)\n"
|
||||
@@ -106,7 +106,7 @@ private extension ArticlePasteboardWriter {
|
||||
static let articleID = "articleID" // database ID, unique per account
|
||||
static let uniqueID = "uniqueID" // unique ID, unique per feed (guid, or possibly calculated)
|
||||
static let feedURL = "feedURL"
|
||||
static let feedID = "feedID" // may differ from feedURL if coming from a syncing system
|
||||
static let webFeedID = "webFeedID" // may differ from feedURL if coming from a syncing system
|
||||
static let title = "title"
|
||||
static let contentHTML = "contentHTML"
|
||||
static let contentText = "contentText"
|
||||
@@ -147,11 +147,11 @@ private extension ArticlePasteboardWriter {
|
||||
d[Key.articleID] = article.articleID
|
||||
d[Key.uniqueID] = article.uniqueID
|
||||
|
||||
if let feed = article.feed {
|
||||
if let feed = article.webFeed {
|
||||
d[Key.feedURL] = feed.url
|
||||
}
|
||||
|
||||
d[Key.feedID] = article.feedID
|
||||
d[Key.webFeedID] = article.webFeedID
|
||||
d[Key.title] = article.title ?? nil
|
||||
d[Key.contentHTML] = article.contentHTML ?? nil
|
||||
d[Key.contentText] = article.contentText ?? nil
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
|
||||
struct TimelineCellAppearance: Equatable {
|
||||
|
||||
let showAvatar: Bool
|
||||
let showIcon: Bool
|
||||
|
||||
let cellPadding = NSEdgeInsets(top: 8.0, left: 18.0, bottom: 10.0, right: 18.0)
|
||||
|
||||
@@ -35,15 +35,15 @@ struct TimelineCellAppearance: Equatable {
|
||||
|
||||
let drawsGrid = false
|
||||
|
||||
let avatarSize = NSSize(width: 48, height: 48)
|
||||
let avatarMarginLeft: CGFloat = 8.0
|
||||
let avatarMarginRight: CGFloat = 8.0
|
||||
let avatarAdjustmentTop: CGFloat = 4.0
|
||||
let avatarCornerRadius: CGFloat = 4.0
|
||||
let iconSize = NSSize(width: 48, height: 48)
|
||||
let iconMarginLeft: CGFloat = 8.0
|
||||
let iconMarginRight: CGFloat = 8.0
|
||||
let iconAdjustmentTop: CGFloat = 4.0
|
||||
let iconCornerRadius: CGFloat = 4.0
|
||||
|
||||
let boxLeftMargin: CGFloat
|
||||
|
||||
init(showAvatar: Bool, fontSize: FontSize) {
|
||||
init(showIcon: Bool, fontSize: FontSize) {
|
||||
|
||||
let actualFontSize = AppDefaults.actualFontSize(for: fontSize)
|
||||
let smallItemFontSize = actualFontSize //floor(actualFontSize * 0.95)
|
||||
@@ -55,7 +55,7 @@ struct TimelineCellAppearance: Equatable {
|
||||
self.textFont = NSFont.systemFont(ofSize: largeItemFontSize)
|
||||
self.textOnlyFont = NSFont.systemFont(ofSize: largeItemFontSize)
|
||||
|
||||
self.showAvatar = showAvatar
|
||||
self.showIcon = showIcon
|
||||
|
||||
let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight
|
||||
self.boxLeftMargin = margin
|
||||
|
||||
@@ -16,13 +16,13 @@ struct TimelineCellData {
|
||||
let dateString: String
|
||||
let feedName: String
|
||||
let showFeedName: Bool
|
||||
let avatar: NSImage? // feed icon, user avatar, or favicon
|
||||
let showAvatar: Bool // Make space even when avatar is nil
|
||||
let iconImage: IconImage? // feed icon, user avatar, or favicon
|
||||
let showIcon: Bool // Make space even when icon is nil
|
||||
let featuredImage: NSImage? // image from within the article
|
||||
let read: Bool
|
||||
let starred: Bool
|
||||
|
||||
init(article: Article, showFeedName: Bool, feedName: String?, avatar: NSImage?, showAvatar: Bool, featuredImage: NSImage?) {
|
||||
init(article: Article, showFeedName: Bool, feedName: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
|
||||
|
||||
self.title = ArticleStringFormatter.truncatedTitle(article)
|
||||
self.text = ArticleStringFormatter.truncatedSummary(article)
|
||||
@@ -38,8 +38,8 @@ struct TimelineCellData {
|
||||
|
||||
self.showFeedName = showFeedName
|
||||
|
||||
self.showAvatar = showAvatar
|
||||
self.avatar = avatar
|
||||
self.showIcon = showIcon
|
||||
self.iconImage = iconImage
|
||||
self.featuredImage = featuredImage
|
||||
|
||||
self.read = article.status.read
|
||||
@@ -52,8 +52,8 @@ struct TimelineCellData {
|
||||
self.dateString = ""
|
||||
self.feedName = ""
|
||||
self.showFeedName = false
|
||||
self.showAvatar = false
|
||||
self.avatar = nil
|
||||
self.showIcon = false
|
||||
self.iconImage = nil
|
||||
self.featuredImage = nil
|
||||
self.read = true
|
||||
self.starred = false
|
||||
|
||||
@@ -21,11 +21,11 @@ struct TimelineCellLayout {
|
||||
let textRect: NSRect
|
||||
let unreadIndicatorRect: NSRect
|
||||
let starRect: NSRect
|
||||
let avatarImageRect: NSRect
|
||||
let iconImageRect: NSRect
|
||||
let separatorRect: NSRect
|
||||
let paddingBottom: CGFloat
|
||||
|
||||
init(width: CGFloat, height: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, avatarImageRect: NSRect, separatorRect: NSRect, paddingBottom: CGFloat) {
|
||||
init(width: CGFloat, height: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, iconImageRect: NSRect, separatorRect: NSRect, paddingBottom: CGFloat) {
|
||||
|
||||
self.width = width
|
||||
self.feedNameRect = feedNameRect
|
||||
@@ -36,7 +36,7 @@ struct TimelineCellLayout {
|
||||
self.textRect = textRect
|
||||
self.unreadIndicatorRect = unreadIndicatorRect
|
||||
self.starRect = starRect
|
||||
self.avatarImageRect = avatarImageRect
|
||||
self.iconImageRect = iconImageRect
|
||||
self.separatorRect = separatorRect
|
||||
self.paddingBottom = paddingBottom
|
||||
|
||||
@@ -44,16 +44,16 @@ struct TimelineCellLayout {
|
||||
self.height = height
|
||||
}
|
||||
else {
|
||||
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
|
||||
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, iconImageRect].maxY() + paddingBottom
|
||||
}
|
||||
}
|
||||
|
||||
init(width: CGFloat, height: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance, hasAvatar: Bool) {
|
||||
init(width: CGFloat, height: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance, hasIcon: Bool) {
|
||||
|
||||
// If height == 0.0, then height is calculated.
|
||||
|
||||
let showAvatar = cellData.showAvatar
|
||||
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, showAvatar, width)
|
||||
let showIcon = cellData.showIcon
|
||||
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, showIcon, width)
|
||||
|
||||
let (titleRect, numberOfLinesForTitle) = TimelineCellLayout.rectForTitle(textBoxRect, appearance, cellData)
|
||||
let summaryRect = numberOfLinesForTitle > 0 ? TimelineCellLayout.rectForSummary(textBoxRect, titleRect, numberOfLinesForTitle, appearance, cellData) : NSRect.zero
|
||||
@@ -72,19 +72,19 @@ struct TimelineCellLayout {
|
||||
let feedNameRect = TimelineCellLayout.rectForFeedName(textBoxRect, dateRect, appearance, cellData)
|
||||
|
||||
textBoxRect.size.height = ceil([titleRect, summaryRect, textRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
|
||||
let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, showAvatar, textBoxRect, width, height)
|
||||
let iconImageRect = TimelineCellLayout.rectForIcon(cellData, appearance, showIcon, textBoxRect, width, height)
|
||||
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, textBoxRect)
|
||||
let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect)
|
||||
let separatorRect = TimelineCellLayout.rectForSeparator(cellData, appearance, showAvatar ? avatarImageRect : titleRect, width, height)
|
||||
let separatorRect = TimelineCellLayout.rectForSeparator(cellData, appearance, showIcon ? iconImageRect : titleRect, width, height)
|
||||
|
||||
let paddingBottom = appearance.cellPadding.bottom
|
||||
|
||||
self.init(width: width, height: height, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, separatorRect: separatorRect, paddingBottom: paddingBottom)
|
||||
self.init(width: width, height: height, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, iconImageRect: iconImageRect, separatorRect: separatorRect, paddingBottom: paddingBottom)
|
||||
}
|
||||
|
||||
static func height(for width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat {
|
||||
|
||||
let layout = TimelineCellLayout(width: width, height: 0.0, cellData: cellData, appearance: appearance, hasAvatar: true)
|
||||
let layout = TimelineCellLayout(width: width, height: 0.0, cellData: cellData, appearance: appearance, hasIcon: true)
|
||||
return layout.height
|
||||
}
|
||||
}
|
||||
@@ -93,12 +93,12 @@ struct TimelineCellLayout {
|
||||
|
||||
private extension TimelineCellLayout {
|
||||
|
||||
static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: TimelineCellData, _ showAvatar: Bool, _ width: CGFloat) -> NSRect {
|
||||
static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: TimelineCellData, _ showIcon: Bool, _ width: CGFloat) -> NSRect {
|
||||
|
||||
// Returned height is a placeholder. Not needed when this is calculated.
|
||||
|
||||
let avatarSpace = showAvatar ? appearance.avatarSize.width + appearance.avatarMarginRight : 0.0
|
||||
let textBoxOriginX = appearance.cellPadding.left + appearance.unreadCircleDimension + appearance.unreadCircleMarginRight + avatarSpace
|
||||
let iconSpace = showIcon ? appearance.iconSize.width + appearance.iconMarginRight : 0.0
|
||||
let textBoxOriginX = appearance.cellPadding.left + appearance.unreadCircleDimension + appearance.unreadCircleMarginRight + iconSpace
|
||||
let textBoxMaxX = floor(width - appearance.cellPadding.right)
|
||||
let textBoxWidth = floor(textBoxMaxX - textBoxOriginX)
|
||||
let textBoxRect = NSRect(x: textBoxOriginX, y: appearance.cellPadding.top, width: textBoxWidth, height: 1000000)
|
||||
@@ -201,15 +201,15 @@ private extension TimelineCellLayout {
|
||||
return r
|
||||
}
|
||||
|
||||
static func rectForAvatar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ showAvatar: Bool, _ textBoxRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect {
|
||||
static func rectForIcon(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ showIcon: Bool, _ textBoxRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect {
|
||||
|
||||
var r = NSRect.zero
|
||||
if !showAvatar {
|
||||
if !showIcon {
|
||||
return r
|
||||
}
|
||||
r.size = appearance.avatarSize
|
||||
r.size = appearance.iconSize
|
||||
r.origin.x = appearance.cellPadding.left + appearance.unreadCircleDimension + appearance.unreadCircleMarginRight
|
||||
r.origin.y = textBoxRect.origin.y + appearance.avatarAdjustmentTop
|
||||
r.origin.y = textBoxRect.origin.y + appearance.iconAdjustmentTop
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class TimelineTableCellView: NSTableCellView {
|
||||
private let dateView = TimelineTableCellView.singleLineTextField()
|
||||
private let feedNameView = TimelineTableCellView.singleLineTextField()
|
||||
|
||||
private lazy var avatarView = TimelineAvatarView()
|
||||
private lazy var iconView = IconView()
|
||||
|
||||
private let starView = TimelineTableCellView.imageView(with: AppAssets.timelineStar, scaling: .scaleNone)
|
||||
private let separatorView = TimelineTableCellView.separatorView()
|
||||
@@ -37,7 +37,7 @@ class TimelineTableCellView: NSTableCellView {
|
||||
didSet {
|
||||
if cellAppearance != oldValue {
|
||||
updateTextFieldFonts()
|
||||
avatarView.layer?.cornerRadius = cellAppearance.avatarCornerRadius
|
||||
iconView.layer?.cornerRadius = cellAppearance.iconCornerRadius
|
||||
needsLayout = true
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ class TimelineTableCellView: NSTableCellView {
|
||||
dateView.rs_setFrameIfNotEqual(layoutRects.dateRect)
|
||||
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
|
||||
feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect)
|
||||
avatarView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect)
|
||||
iconView.rs_setFrameIfNotEqual(layoutRects.iconImageRect)
|
||||
starView.rs_setFrameIfNotEqual(layoutRects.starRect)
|
||||
separatorView.rs_setFrameIfNotEqual(layoutRects.separatorRect)
|
||||
}
|
||||
@@ -207,7 +207,7 @@ private extension TimelineTableCellView {
|
||||
addSubviewAtInit(unreadIndicatorView, hidden: true)
|
||||
addSubviewAtInit(dateView, hidden: false)
|
||||
addSubviewAtInit(feedNameView, hidden: true)
|
||||
addSubviewAtInit(avatarView, hidden: true)
|
||||
addSubviewAtInit(iconView, hidden: true)
|
||||
addSubviewAtInit(starView, hidden: true)
|
||||
addSubviewAtInit(separatorView, hidden: !AppDefaults.timelineShowsSeparators)
|
||||
|
||||
@@ -216,7 +216,7 @@ private extension TimelineTableCellView {
|
||||
|
||||
func updatedLayoutRects() -> TimelineCellLayout {
|
||||
|
||||
return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasAvatar: avatarView.image != nil)
|
||||
return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasIcon: iconView.iconImage != nil)
|
||||
}
|
||||
|
||||
func updateTitleView() {
|
||||
@@ -265,25 +265,25 @@ private extension TimelineTableCellView {
|
||||
showOrHideView(starView, !cellData.starred)
|
||||
}
|
||||
|
||||
func updateAvatar() {
|
||||
guard let image = cellData.avatar, cellData.showAvatar else {
|
||||
makeAvatarEmpty()
|
||||
func updateIcon() {
|
||||
guard let iconImage = cellData.iconImage, cellData.showIcon else {
|
||||
makeIconEmpty()
|
||||
return
|
||||
}
|
||||
|
||||
showView(avatarView)
|
||||
if avatarView.image !== image {
|
||||
avatarView.image = image
|
||||
showView(iconView)
|
||||
if iconView.iconImage !== iconImage {
|
||||
iconView.iconImage = iconImage
|
||||
needsLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
func makeAvatarEmpty() {
|
||||
if avatarView.image != nil {
|
||||
avatarView.image = nil
|
||||
func makeIconEmpty() {
|
||||
if iconView.iconImage != nil {
|
||||
iconView.iconImage = nil
|
||||
needsLayout = true
|
||||
}
|
||||
hideView(avatarView)
|
||||
hideView(iconView)
|
||||
}
|
||||
|
||||
func hideView(_ view: NSView) {
|
||||
@@ -310,7 +310,7 @@ private extension TimelineTableCellView {
|
||||
updateFeedNameView()
|
||||
updateUnreadIndicator()
|
||||
updateStarView()
|
||||
updateAvatar()
|
||||
updateIcon()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,12 +74,12 @@ extension TimelineViewController {
|
||||
|
||||
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
|
||||
|
||||
guard let menuItem = sender as? NSMenuItem, let feed = menuItem.representedObject as? Feed else {
|
||||
guard let menuItem = sender as? NSMenuItem, let feed = menuItem.representedObject as? WebFeed else {
|
||||
return
|
||||
}
|
||||
|
||||
var userInfo = UserInfoDictionary()
|
||||
userInfo[UserInfoKey.feed] = feed
|
||||
userInfo[UserInfoKey.webFeed] = feed
|
||||
|
||||
NotificationCenter.default.post(name: .UserDidRequestSidebarSelection, object: self, userInfo: userInfo)
|
||||
|
||||
@@ -174,7 +174,7 @@ private extension TimelineViewController {
|
||||
|
||||
menu.addSeparatorIfNeeded()
|
||||
|
||||
if articles.count == 1, let feed = articles.first!.feed {
|
||||
if articles.count == 1, let feed = articles.first!.webFeed {
|
||||
menu.addItem(selectFeedInSidebarMenuItem(feed))
|
||||
if let markAllMenuItem = markAllAsReadMenuItem(feed) {
|
||||
menu.addItem(markAllMenuItem)
|
||||
@@ -247,13 +247,13 @@ private extension TimelineViewController {
|
||||
return menuItem(NSLocalizedString("Mark Older as Read", comment: "Command"), #selector(markOlderArticlesReadFromContextualMenu(_:)), articles)
|
||||
}
|
||||
|
||||
func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
|
||||
func selectFeedInSidebarMenuItem(_ feed: WebFeed) -> NSMenuItem {
|
||||
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
|
||||
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
|
||||
return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed)
|
||||
}
|
||||
|
||||
func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? {
|
||||
func markAllAsReadMenuItem(_ feed: WebFeed) -> NSMenuItem? {
|
||||
|
||||
let articles = Array(feed.fetchArticles())
|
||||
guard articles.canMarkAllAsRead() else {
|
||||
|
||||
@@ -25,7 +25,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
|
||||
unreadCount = 0
|
||||
if let representedObjects = representedObjects {
|
||||
if representedObjects.count == 1 && representedObjects.first is Feed {
|
||||
if representedObjects.count == 1 && representedObjects.first is WebFeed {
|
||||
showFeedNames = false
|
||||
}
|
||||
else {
|
||||
@@ -79,7 +79,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
reloadVisibleCells()
|
||||
return
|
||||
}
|
||||
updateShowAvatars()
|
||||
updateShowIcons()
|
||||
articleRowMap = [String: Int]()
|
||||
tableView.reloadData()
|
||||
}
|
||||
@@ -98,18 +98,18 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
private let fetchRequestQueue = FetchRequestQueue()
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
private var cellAppearance: TimelineCellAppearance!
|
||||
private var cellAppearanceWithAvatar: TimelineCellAppearance!
|
||||
private var cellAppearanceWithIcon: TimelineCellAppearance!
|
||||
private var showFeedNames = false {
|
||||
didSet {
|
||||
if showFeedNames != oldValue {
|
||||
updateShowAvatars()
|
||||
updateShowIcons()
|
||||
updateTableViewRowHeight()
|
||||
reloadVisibleCells()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var showAvatars = false
|
||||
private var showIcons = false
|
||||
private var rowHeightWithFeedName: CGFloat = 0.0
|
||||
private var rowHeightWithoutFeedName: CGFloat = 0.0
|
||||
|
||||
@@ -156,8 +156,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
cellAppearance = TimelineCellAppearance(showAvatar: false, fontSize: fontSize)
|
||||
cellAppearanceWithAvatar = TimelineCellAppearance(showAvatar: true, fontSize: fontSize)
|
||||
cellAppearance = TimelineCellAppearance(showIcon: false, fontSize: fontSize)
|
||||
cellAppearanceWithIcon = TimelineCellAppearance(showIcon: true, fontSize: fontSize)
|
||||
|
||||
updateRowHeights()
|
||||
tableView.rowHeight = currentRowHeight
|
||||
@@ -168,7 +168,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
if !didRegisterForNotifications {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
@@ -388,7 +388,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
// MARK: - Navigation
|
||||
|
||||
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
|
||||
guard let articleID = userInfo[DeepLinkKey.articleID.rawValue] as? String else { return }
|
||||
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
|
||||
guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return }
|
||||
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
@@ -437,15 +437,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
updateUnreadCount()
|
||||
}
|
||||
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard showAvatars, let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||
guard showIcons, let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
|
||||
return
|
||||
}
|
||||
let indexesToReload = tableView.indexesOfAvailableRowsPassingTest { (row) -> Bool in
|
||||
guard let article = articles.articleAtRow(row) else {
|
||||
return false
|
||||
}
|
||||
return feed == article.feed
|
||||
return feed == article.webFeed
|
||||
}
|
||||
if let indexesToReload = indexesToReload {
|
||||
reloadCells(for: indexesToReload)
|
||||
@@ -453,7 +453,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
guard showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
guard showIcons, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -474,17 +474,17 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
if showAvatars {
|
||||
if showIcons {
|
||||
queueReloadAvailableCells()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
|
||||
let shouldFetchAndMergeArticles = representedObjectsContainsAnyWebFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
|
||||
if shouldFetchAndMergeArticles {
|
||||
queueFetchAndMergeArticles()
|
||||
}
|
||||
@@ -568,9 +568,9 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||
let prototypeID = "prototype"
|
||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, userDeleted: false, dateArrived: Date())
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: nil, dateModified: nil, authors: nil, attachments: nil, status: status)
|
||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: nil, dateModified: nil, authors: nil, attachments: nil, status: status)
|
||||
|
||||
let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: showingFeedNames, feedName: "Prototype Feed Name", avatar: nil, showAvatar: false, featuredImage: nil)
|
||||
let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: showingFeedNames, feedName: "Prototype Feed Name", iconImage: nil, showIcon: false, featuredImage: nil)
|
||||
let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance)
|
||||
return height
|
||||
}
|
||||
@@ -674,7 +674,7 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
|
||||
func configure(_ cell: TimelineTableCellView) {
|
||||
cell.cellAppearance = showAvatars ? cellAppearanceWithAvatar : cellAppearance
|
||||
cell.cellAppearance = showIcons ? cellAppearanceWithIcon : cellAppearance
|
||||
if let article = articles.articleAtRow(row) {
|
||||
configureTimelineCell(cell, article: article)
|
||||
}
|
||||
@@ -716,12 +716,12 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
|
||||
private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
|
||||
cell.objectValue = article
|
||||
let avatar = article.avatarImage()
|
||||
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: nil)
|
||||
let iconImage = article.iconImage()
|
||||
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.webFeed?.nameForDisplay, iconImage: iconImage, showIcon: showIcons, featuredImage: nil)
|
||||
}
|
||||
|
||||
private func avatarFor(_ article: Article) -> NSImage? {
|
||||
if !showAvatars {
|
||||
private func iconFor(_ article: Article) -> IconImage? {
|
||||
if !showIcons {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -733,22 +733,22 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
guard let feed = article.feed else {
|
||||
guard let feed = article.webFeed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let feedIcon = appDelegate.feedIconDownloader.icon(for: feed) {
|
||||
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
|
||||
return feedIcon
|
||||
}
|
||||
|
||||
if let favicon = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
|
||||
if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
|
||||
return favicon
|
||||
}
|
||||
|
||||
return FaviconGenerator.favicon(feed)
|
||||
}
|
||||
|
||||
private func avatarForAuthor(_ author: Author) -> NSImage? {
|
||||
private func avatarForAuthor(_ author: Author) -> IconImage? {
|
||||
return appDelegate.authorAvatarDownloader.image(for: author)
|
||||
}
|
||||
|
||||
@@ -779,19 +779,19 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
|
||||
switch edge {
|
||||
case .leading:
|
||||
let title = article.status.read ? NSLocalizedString("Mark Unread", comment: "mark unread") : NSLocalizedString("Mark Read", comment: "mark read")
|
||||
let action = NSTableViewRowAction(style: .regular, title: title) { (action, row) in
|
||||
let action = NSTableViewRowAction(style: .regular, title: "") { (action, row) in
|
||||
self.toggleArticleRead(article);
|
||||
tableView.rowActionsVisible = false
|
||||
}
|
||||
action.image = article.status.read ? AppAssets.swipeMarkUnreadImage : AppAssets.swipeMarkReadImage
|
||||
return [action]
|
||||
|
||||
case .trailing:
|
||||
let title = article.status.starred ? NSLocalizedString("Mark Unstarred", comment: "mark unstarred") : NSLocalizedString("Mark Starred", comment: "mark starred")
|
||||
let action = NSTableViewRowAction(style: .regular, title: title) { (action, row) in
|
||||
let action = NSTableViewRowAction(style: .regular, title: "") { (action, row) in
|
||||
self.toggleArticleStarred(article);
|
||||
tableView.rowActionsVisible = false
|
||||
}
|
||||
action.image = article.status.starred ? AppAssets.swipeMarkUnstarredImage : AppAssets.swipeMarkStarredImage
|
||||
return [action]
|
||||
|
||||
@unknown default:
|
||||
@@ -842,9 +842,9 @@ private extension TimelineViewController {
|
||||
tableView.rowHeight = currentRowHeight
|
||||
}
|
||||
|
||||
func updateShowAvatars() {
|
||||
func updateShowIcons() {
|
||||
if showFeedNames {
|
||||
self.showAvatars = true
|
||||
self.showIcons = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -852,14 +852,14 @@ private extension TimelineViewController {
|
||||
if let authors = article.authors {
|
||||
for author in authors {
|
||||
if author.avatarURL != nil {
|
||||
self.showAvatars = true
|
||||
self.showIcons = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.showAvatars = false
|
||||
self.showIcons = false
|
||||
}
|
||||
|
||||
func emptyTheTimeline() {
|
||||
@@ -935,8 +935,8 @@ private extension TimelineViewController {
|
||||
// MARK: - Appearance Change
|
||||
|
||||
private func fontSizeDidChange() {
|
||||
cellAppearance = TimelineCellAppearance(showAvatar: false, fontSize: fontSize)
|
||||
cellAppearanceWithAvatar = TimelineCellAppearance(showAvatar: true, fontSize: fontSize)
|
||||
cellAppearance = TimelineCellAppearance(showIcon: false, fontSize: fontSize)
|
||||
cellAppearanceWithIcon = TimelineCellAppearance(showIcon: true, fontSize: fontSize)
|
||||
updateRowHeights()
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.reloadData()
|
||||
@@ -1056,23 +1056,23 @@ private extension TimelineViewController {
|
||||
return representedObjects?.contains(where: { $0 is Folder }) ?? false
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||
func representedObjectsContainsAnyWebFeed(_ webFeeds: Set<WebFeed>) -> Bool {
|
||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
return false
|
||||
}
|
||||
for representedObject in representedObjects {
|
||||
if let feed = representedObject as? Feed {
|
||||
for oneFeed in feeds {
|
||||
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
|
||||
if let feed = representedObject as? WebFeed {
|
||||
for oneFeed in webFeeds {
|
||||
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let folder = representedObject as? Folder {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
for oneFeed in webFeeds {
|
||||
if folder.hasWebFeed(with: oneFeed.webFeedID) || folder.hasWebFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,10 @@ extension AccountsAddViewController: NSTableViewDelegate {
|
||||
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsAddWindowController = accountsReaderAPIWindowController
|
||||
case .feedly:
|
||||
let accountsFeedlyWindowController = AccountsFeedlyWebWindowController()
|
||||
accountsFeedlyWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsAddWindowController = accountsFeedlyWindowController
|
||||
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
|
||||
addAccount.delegate = self
|
||||
addAccount.presentationAnchor = self.view.window!
|
||||
OperationQueue.main.addOperation(addAccount)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -120,3 +121,23 @@ extension AccountsAddViewController: NSTableViewDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: OAuthAccountAuthorizationOperationDelegate
|
||||
|
||||
extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate {
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
|
||||
account.refreshAll { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self?.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
|
||||
view.window?.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,19 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
private var hidesCredentialsButton: Bool {
|
||||
guard let account = account else {
|
||||
return true
|
||||
}
|
||||
switch account.type {
|
||||
case .onMyMac,
|
||||
.feedly:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@@ -35,7 +48,7 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
|
||||
typeLabel.stringValue = account?.defaultName ?? ""
|
||||
nameTextField.stringValue = account?.name ?? ""
|
||||
activeButton.state = account?.isActive ?? false ? .on : .off
|
||||
credentialsButton.isHidden = account?.type ?? .onMyMac == .onMyMac
|
||||
credentialsButton.isHidden = hidesCredentialsButton
|
||||
}
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification) {
|
||||
@@ -66,8 +79,6 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
|
||||
accountsFreshRSSWindowController.account = account
|
||||
accountsFreshRSSWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFreshRSSWindowController
|
||||
case .feedly:
|
||||
assertionFailure("Implement feedly logout window controller")
|
||||
break
|
||||
case .feedWrangler:
|
||||
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14865.1"/>
|
||||
<plugIn identifier="com.apple.WebKit2IBPlugin" version="14865.1"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AccountsFeedlyWebWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="webView" destination="W4c-Xp-rpq" id="l11-5B-8yc"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="708"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="708"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<wkWebView wantsLayer="YES" translatesAutoresizingMaskIntoConstraints="NO" id="W4c-Xp-rpq">
|
||||
<rect key="frame" x="0.0" y="61" width="480" height="647"/>
|
||||
<wkWebViewConfiguration key="configuration">
|
||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||
<wkPreferences key="preferences"/>
|
||||
</wkWebViewConfiguration>
|
||||
<connections>
|
||||
<outlet property="navigationDelegate" destination="-2" id="wAp-Oh-5EK"/>
|
||||
</connections>
|
||||
</wkWebView>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PD2-Zk-3yM">
|
||||
<rect key="frame" x="384" y="13" width="82" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="IEi-N0-sbw">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="cancel:" target="-2" id="5BT-to-e4W"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="W4c-Xp-rpq" secondAttribute="trailing" id="D9j-IU-BZj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PD2-Zk-3yM" secondAttribute="bottom" constant="20" symbolic="YES" id="Qdc-tu-9kO"/>
|
||||
<constraint firstItem="W4c-Xp-rpq" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" id="V7Q-kM-JDA"/>
|
||||
<constraint firstAttribute="trailing" secondItem="PD2-Zk-3yM" secondAttribute="trailing" constant="20" symbolic="YES" id="bQS-L4-jbx"/>
|
||||
<constraint firstItem="W4c-Xp-rpq" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" id="ec6-U0-t8X"/>
|
||||
<constraint firstItem="PD2-Zk-3yM" firstAttribute="top" secondItem="W4c-Xp-rpq" secondAttribute="bottom" constant="20" symbolic="YES" id="zlA-8I-aKr"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="134" y="556"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,119 +0,0 @@
|
||||
//
|
||||
// AccountsFeedlyWebWindowController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Kiel Gillard on 30/8/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Account
|
||||
import WebKit
|
||||
|
||||
class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegate {
|
||||
|
||||
@IBOutlet private weak var webView: WKWebView!
|
||||
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
convenience init() {
|
||||
self.init(windowNibName: NSNib.Name("AccountsFeedlyWeb"))
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) {
|
||||
self.hostWindow = hostWindow
|
||||
hostWindow.beginSheet(window!, completionHandler: handler)
|
||||
beginAuthorization()
|
||||
}
|
||||
|
||||
// MARK: Requesting an Access Token
|
||||
|
||||
private let client = OAuthAuthorizationClient.feedlySandboxClient
|
||||
|
||||
private func beginAuthorization() {
|
||||
let request = Account.oauthAuthorizationCodeGrantRequest(for: .feedly, client: client)
|
||||
webView.load(request)
|
||||
}
|
||||
|
||||
private func requestAccessToken(for response: OAuthAuthorizationResponse) {
|
||||
Account.requestOAuthAccessToken(with: response, client: client, accountType: .feedly) { [weak self] result in
|
||||
switch result {
|
||||
case .success(let tokenResponse):
|
||||
self?.saveAccount(for: tokenResponse)
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
|
||||
do {
|
||||
guard let url = navigationAction.request.url else { return }
|
||||
|
||||
let response = try OAuthAuthorizationResponse(url: url, client: client)
|
||||
|
||||
requestAccessToken(for: response)
|
||||
|
||||
// No point the web view trying to load this.
|
||||
return decisionHandler(.cancel)
|
||||
|
||||
} catch let error as OAuthAuthorizationErrorResponse {
|
||||
NSApplication.shared.presentError(error)
|
||||
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
print(error)
|
||||
}
|
||||
|
||||
private func saveAccount(for grant: OAuthAuthorizationGrant) {
|
||||
// TODO: Find an already existing account for this username?
|
||||
let account = AccountManager.shared.createAccount(type: .feedly)
|
||||
do {
|
||||
|
||||
// Store the refresh token first because it sends this token to the account delegate.
|
||||
if let token = grant.refreshToken {
|
||||
try account.storeCredentials(token)
|
||||
}
|
||||
|
||||
// Now store the access token because we want the account delegate to use it.
|
||||
try account.storeCredentials(grant.accessToken)
|
||||
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension OAuthAuthorizationClient {
|
||||
|
||||
/// Models public sandbox API values found at:
|
||||
/// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw
|
||||
static var feedlySandboxClient: OAuthAuthorizationClient {
|
||||
return OAuthAuthorizationClient(id: "sandbox",
|
||||
redirectUri: "http://localhost",
|
||||
state: nil,
|
||||
secret: "ReVGXA6WekanCxbf")
|
||||
}
|
||||
|
||||
/// Models private NetNewsWire client secrets.
|
||||
/// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code
|
||||
static var netNewsWireClient: OAuthAuthorizationClient {
|
||||
fatalError("This app is not registered as a client with Feedly. Follow the URL in the code comments for this property.")
|
||||
}
|
||||
}
|
||||
6
Mac/Resources/Assets.xcassets/Swipe Images/Contents.json
Normal file
6
Mac/Resources/Assets.xcassets/Swipe Images/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
24
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkRead.imageset/Contents.json
vendored
Normal file
24
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkRead.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "checkmark.circle.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkRead.imageset/checkmark.circle.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkRead.imageset/checkmark.circle.pdf
vendored
Normal file
Binary file not shown.
21
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkStarred.imageset/Contents.json
vendored
Normal file
21
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkStarred.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "star.fill.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkStarred.imageset/star.fill.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkStarred.imageset/star.fill.pdf
vendored
Normal file
Binary file not shown.
24
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnread.imageset/Contents.json
vendored
Normal file
24
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnread.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circle.fill.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnread.imageset/circle.fill.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnread.imageset/circle.fill.pdf
vendored
Normal file
Binary file not shown.
21
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnstarred.imageset/Contents.json
vendored
Normal file
21
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnstarred.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "star.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnstarred.imageset/star.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/Swipe Images/swipeMarkUnstarred.imageset/star.pdf
vendored
Normal file
Binary file not shown.
BIN
Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/ArticleExtractorInactiveDark.pdf
vendored
Normal file
BIN
Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/ArticleExtractorInactiveDark.pdf
vendored
Normal file
Binary file not shown.
12
Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ArticleExtractorInactiveDark.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/Contents.json
vendored
Normal file
12
Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ArticleExtractorInactiveLight.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
</dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>com.ranchero.NetNewsWire.ReadArticle</string>
|
||||
<string>ReadArticle</string>
|
||||
</array>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>NetNewsWire communicates with other apps on your Mac when you choose to share an article.</string>
|
||||
@@ -60,9 +60,9 @@
|
||||
<key>OSAScriptingDefinition</key>
|
||||
<string>NetNewsWire.sdef</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://ranchero.com/downloads/netnewswire-release.xml</string>
|
||||
<string>https://ranchero.com/downloads/netnewswire-5.1-beta.xml</string>
|
||||
<key>FeedURLForTestBuilds</key>
|
||||
<string>https://ranchero.com/downloads/netnewswire-beta.xml</string>
|
||||
<string>https://ranchero.com/downloads/netnewswire-5.1-beta.xml</string>
|
||||
<key>UserAgent</key>
|
||||
<string>NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/)</string>
|
||||
</dict>
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
<element type="account">
|
||||
<cocoa key="accounts"/>
|
||||
</element>
|
||||
<element type="feed">
|
||||
<cocoa key="feeds"/>
|
||||
<element type="webFeed">
|
||||
<cocoa key="webFeeds"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
@@ -88,30 +88,30 @@
|
||||
<property name="active" code="Actv" type="boolean" access="rw" description="Whether or not the account is active">
|
||||
<cocoa key="scriptingIsActive"/>
|
||||
</property>
|
||||
<property name="allFeeds" code="Feds" access="r" description="All feeds, including feeds inside folders">
|
||||
<cocoa key="allFeeds"/>
|
||||
<type type="feed" list="yes"/>
|
||||
<property name="allWebFeeds" code="Feds" access="r" description="All feeds, including feeds inside folders">
|
||||
<cocoa key="allWebFeeds"/>
|
||||
<type type="webFeed" list="yes"/>
|
||||
</property>
|
||||
<property name="opml representation" code="OPML" type="text" access="r" description="OPML representation for the account">
|
||||
<cocoa key="opmlRepresentation"/>
|
||||
</property>
|
||||
<element type="feed">
|
||||
<cocoa key="feeds"/>
|
||||
<element type="webFeed">
|
||||
<cocoa key="webFeeds"/>
|
||||
</element>
|
||||
<element type="folder">
|
||||
<cocoa key="folders"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
<class name="feed" code="Feed" plural="feeds" description="An RSS feeds">
|
||||
<cocoa class="ScriptableFeed"/>
|
||||
<class name="webFeed" code="Feed" plural="webFeeds" description="An RSS feed">
|
||||
<cocoa class="ScriptableWebFeed"/>
|
||||
<property name="name" code="pnam" type="text" access="r" description="The name of the feed">
|
||||
<cocoa key="name"/>
|
||||
</property>
|
||||
<property name="id" code="ID " type="text" access="r" description="The unique id of the account">
|
||||
<property name="id" code="ID " type="text" access="r" description="The unique id of the feed">
|
||||
<cocoa key="uniqueId"/>
|
||||
</property>
|
||||
<property name="url" code="URL " type="text" access="r" description="The type of the account">
|
||||
<property name="url" code="URL " type="text" access="r" description="The type of the feed">
|
||||
<cocoa key="url"/>
|
||||
</property>
|
||||
<property name="homepage url" code="HpUr" type="text" access="r" description="url for the feed homepage (optional)">
|
||||
@@ -164,8 +164,8 @@
|
||||
<property name="opml representation" code="OPML" type="text" access="r" description="OPML representation for the folder">
|
||||
<cocoa key="opmlRepresentation"/>
|
||||
</property>
|
||||
<element type="feed">
|
||||
<cocoa key="feeds"/>
|
||||
<element type="webFeed">
|
||||
<cocoa key="webFeeds"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
account.removeFolder(scriptableFolder.folder) { result in
|
||||
}
|
||||
}
|
||||
} else if let scriptableFeed = element as? ScriptableFeed {
|
||||
} else if let scriptableFeed = element as? ScriptableWebFeed {
|
||||
BatchUpdate.shared.perform {
|
||||
var container: Container? = nil
|
||||
if let scriptableFolder = scriptableFeed.container as? ScriptableFolder {
|
||||
@@ -81,7 +81,7 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
} else {
|
||||
container = account
|
||||
}
|
||||
account.removeFeed(scriptableFeed.feed, from: container!) { result in
|
||||
account.removeWebFeed(scriptableFeed.webFeed, from: container!) { result in
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,23 +94,23 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
|
||||
// MARK: --- Scriptable elements ---
|
||||
|
||||
@objc(feeds)
|
||||
var feeds:NSArray {
|
||||
return account.topLevelFeeds.map { ScriptableFeed($0, container:self) } as NSArray
|
||||
@objc(webFeeds)
|
||||
var webFeeds:NSArray {
|
||||
return account.topLevelWebFeeds.map { ScriptableWebFeed($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInFeedsWithUniqueID:)
|
||||
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
|
||||
let feeds = Array(account.topLevelFeeds)
|
||||
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil }
|
||||
return ScriptableFeed(feed, container:self)
|
||||
@objc(valueInWebFeedsWithUniqueID:)
|
||||
func valueInWebFeeds(withUniqueID id:String) -> ScriptableWebFeed? {
|
||||
let feeds = Array(account.topLevelWebFeeds)
|
||||
guard let feed = feeds.first(where:{$0.webFeedID == id}) else { return nil }
|
||||
return ScriptableWebFeed(feed, container:self)
|
||||
}
|
||||
|
||||
@objc(valueInFeedsWithName:)
|
||||
func valueInFeeds(withName name:String) -> ScriptableFeed? {
|
||||
let feeds = Array(account.topLevelFeeds)
|
||||
@objc(valueInWebFeedsWithName:)
|
||||
func valueInWebFeeds(withName name:String) -> ScriptableWebFeed? {
|
||||
let feeds = Array(account.topLevelWebFeeds)
|
||||
guard let feed = feeds.first(where:{$0.name == name}) else { return nil }
|
||||
return ScriptableFeed(feed, container:self)
|
||||
return ScriptableWebFeed(feed, container:self)
|
||||
}
|
||||
|
||||
@objc(folders)
|
||||
@@ -131,21 +131,21 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
|
||||
// MARK: --- Scriptable properties ---
|
||||
|
||||
@objc(allFeeds)
|
||||
var allFeeds: NSArray {
|
||||
var feeds = [ScriptableFeed]()
|
||||
for feed in account.topLevelFeeds {
|
||||
feeds.append(ScriptableFeed(feed, container: self))
|
||||
@objc(allWebFeeds)
|
||||
var allWebFeeds: NSArray {
|
||||
var webFeeds = [ScriptableWebFeed]()
|
||||
for webFeed in account.topLevelWebFeeds {
|
||||
webFeeds.append(ScriptableWebFeed(webFeed, container: self))
|
||||
}
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
let scriptableFolder = ScriptableFolder(folder, container: self)
|
||||
for feed in folder.topLevelFeeds {
|
||||
feeds.append(ScriptableFeed(feed, container: scriptableFolder))
|
||||
for webFeed in folder.topLevelWebFeeds {
|
||||
webFeeds.append(ScriptableWebFeed(webFeed, container: scriptableFolder))
|
||||
}
|
||||
}
|
||||
}
|
||||
return feeds as NSArray
|
||||
return webFeeds as NSArray
|
||||
}
|
||||
|
||||
@objc(opmlRepresentation)
|
||||
|
||||
@@ -59,8 +59,8 @@ extension AppDelegate : AppDelegateAppleEvents {
|
||||
class NetNewsWireCreateElementCommand : NSCreateCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
let classDescription = self.createClassDescription
|
||||
if (classDescription.className == "feed") {
|
||||
return ScriptableFeed.handleCreateElement(command:self)
|
||||
if (classDescription.className == "webFeed") {
|
||||
return ScriptableWebFeed.handleCreateElement(command:self)
|
||||
} else if (classDescription.className == "folder") {
|
||||
return ScriptableFolder.handleCreateElement(command:self)
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
||||
}
|
||||
|
||||
func deleteElement(_ element:ScriptingObject) {
|
||||
if let scriptableFeed = element as? ScriptableFeed {
|
||||
if let scriptableFeed = element as? ScriptableWebFeed {
|
||||
BatchUpdate.shared.perform {
|
||||
folder.account?.removeFeed(scriptableFeed.feed, from: folder) { result in }
|
||||
folder.account?.removeWebFeed(scriptableFeed.webFeed, from: folder) { result in }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,10 +95,10 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
||||
|
||||
// MARK: --- Scriptable elements ---
|
||||
|
||||
@objc(feeds)
|
||||
var feeds:NSArray {
|
||||
let feeds = Array(folder.topLevelFeeds)
|
||||
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
|
||||
@objc(webFeeds)
|
||||
var webFeeds:NSArray {
|
||||
let feeds = Array(folder.topLevelWebFeeds)
|
||||
return feeds.map { ScriptableWebFeed($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
// MARK: --- Scriptable properties ---
|
||||
|
||||
@@ -30,8 +30,8 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
func currentArticle() -> ScriptableArticle? {
|
||||
var scriptableArticle: ScriptableArticle?
|
||||
if let currentArticle = appDelegate.scriptingCurrentArticle {
|
||||
if let feed = currentArticle.feed {
|
||||
let scriptableFeed = ScriptableFeed(feed, container:self)
|
||||
if let feed = currentArticle.webFeed {
|
||||
let scriptableFeed = ScriptableWebFeed(feed, container:self)
|
||||
scriptableArticle = ScriptableArticle(currentArticle, container:scriptableFeed)
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
func selectedArticles() -> NSArray {
|
||||
let articles = appDelegate.scriptingSelectedArticles
|
||||
let scriptableArticles:[ScriptableArticle] = articles.compactMap { article in
|
||||
if let feed = article.feed {
|
||||
let scriptableFeed = ScriptableFeed(feed, container:self)
|
||||
if let feed = article.webFeed {
|
||||
let scriptableFeed = ScriptableWebFeed(feed, container:self)
|
||||
return ScriptableArticle(article, container:scriptableFeed)
|
||||
} else {
|
||||
return nil
|
||||
@@ -73,26 +73,26 @@ extension NSApplication : ScriptingObjectContainer {
|
||||
for 'articles of feed "The Shape of Everything" of account "On My Mac"'
|
||||
*/
|
||||
|
||||
func allFeeds() -> [Feed] {
|
||||
func allWebFeeds() -> [WebFeed] {
|
||||
let accounts = AccountManager.shared.activeAccounts
|
||||
let emptyFeeds:[Feed] = []
|
||||
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
|
||||
let accountFeeds = Array(nthAccount.topLevelFeeds)
|
||||
let emptyFeeds:[WebFeed] = []
|
||||
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [WebFeed] in
|
||||
let accountFeeds = Array(nthAccount.topLevelWebFeeds)
|
||||
return result + accountFeeds
|
||||
}
|
||||
}
|
||||
|
||||
@objc(feeds)
|
||||
func feeds() -> NSArray {
|
||||
let feeds = self.allFeeds()
|
||||
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
|
||||
@objc(webFeeds)
|
||||
func webFeeds() -> NSArray {
|
||||
let webFeeds = self.allWebFeeds()
|
||||
return webFeeds.map { ScriptableWebFeed($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInFeedsWithUniqueID:)
|
||||
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
|
||||
let feeds = self.allFeeds()
|
||||
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil }
|
||||
return ScriptableFeed(feed, container:self)
|
||||
@objc(valueInWebFeedsWithUniqueID:)
|
||||
func valueInWebFeeds(withUniqueID id:String) -> ScriptableWebFeed? {
|
||||
let webFeeds = self.allWebFeeds()
|
||||
guard let webFeed = webFeeds.first(where:{$0.webFeedID == id}) else { return nil }
|
||||
return ScriptableWebFeed(webFeed, container:self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ import RSParser
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
@objc(ScriptableFeed)
|
||||
class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
@objc(ScriptableWebFeed)
|
||||
class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||
|
||||
let feed:Feed
|
||||
let webFeed:WebFeed
|
||||
let container:ScriptingObjectContainer
|
||||
|
||||
init (_ feed:Feed, container:ScriptingObjectContainer) {
|
||||
self.feed = feed
|
||||
init (_ webFeed:WebFeed, container:ScriptingObjectContainer) {
|
||||
self.webFeed = webFeed
|
||||
self.container = container
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
// MARK: --- ScriptingObject protocol ---
|
||||
|
||||
var scriptingKey: String {
|
||||
return "feeds"
|
||||
return "webFeeds"
|
||||
}
|
||||
|
||||
// MARK: --- UniqueIdScriptingObject protocol ---
|
||||
@@ -45,7 +45,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
|
||||
@objc(uniqueId)
|
||||
var scriptingUniqueId:Any {
|
||||
return feed.feedID
|
||||
return webFeed.webFeedID
|
||||
}
|
||||
|
||||
// MARK: --- ScriptingObjectContainer protocol ---
|
||||
@@ -71,13 +71,13 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
return url
|
||||
}
|
||||
|
||||
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
|
||||
class func scriptableFeed(_ feed:WebFeed, account:Account, folder:Folder?) -> ScriptableWebFeed {
|
||||
let scriptableAccount = ScriptableAccount(account)
|
||||
if let folder = folder {
|
||||
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
||||
return ScriptableFeed(feed, container:scriptableFolder)
|
||||
return ScriptableWebFeed(feed, container:scriptableFolder)
|
||||
} else {
|
||||
return ScriptableFeed(feed, container:scriptableAccount)
|
||||
return ScriptableWebFeed(feed, container:scriptableAccount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
let (account, folder) = command.accountAndFolderForNewChild()
|
||||
guard let url = self.urlForNewFeed(arguments:arguments) else {return nil}
|
||||
|
||||
if let existingFeed = account.existingFeed(withURL:url) {
|
||||
if let existingFeed = account.existingWebFeed(withURL:url) {
|
||||
return scriptableFeed(existingFeed, account:account, folder:folder).objectSpecifier
|
||||
}
|
||||
|
||||
@@ -102,10 +102,10 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
// suspendExecution(). When we get the callback, we supply the event result and call resumeExecution().
|
||||
command.suspendExecution()
|
||||
|
||||
account.createFeed(url: url, name: titleFromArgs, container: container) { result in
|
||||
account.createWebFeed(url: url, name: titleFromArgs, container: container) { result in
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])
|
||||
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
|
||||
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
|
||||
case .failure:
|
||||
@@ -121,51 +121,51 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
|
||||
@objc(url)
|
||||
var url:String {
|
||||
return self.feed.url
|
||||
return self.webFeed.url
|
||||
}
|
||||
|
||||
@objc(name)
|
||||
var name:String {
|
||||
return self.feed.name ?? ""
|
||||
return self.webFeed.name ?? ""
|
||||
}
|
||||
|
||||
@objc(homePageURL)
|
||||
var homePageURL:String {
|
||||
return self.feed.homePageURL ?? ""
|
||||
return self.webFeed.homePageURL ?? ""
|
||||
}
|
||||
|
||||
@objc(iconURL)
|
||||
var iconURL:String {
|
||||
return self.feed.iconURL ?? ""
|
||||
return self.webFeed.iconURL ?? ""
|
||||
}
|
||||
|
||||
@objc(faviconURL)
|
||||
var faviconURL:String {
|
||||
return self.feed.faviconURL ?? ""
|
||||
return self.webFeed.faviconURL ?? ""
|
||||
}
|
||||
|
||||
@objc(opmlRepresentation)
|
||||
var opmlRepresentation:String {
|
||||
return self.feed.OPMLString(indentLevel:0, strictConformance: true)
|
||||
return self.webFeed.OPMLString(indentLevel:0, strictConformance: true)
|
||||
}
|
||||
|
||||
// MARK: --- scriptable elements ---
|
||||
|
||||
@objc(authors)
|
||||
var authors:NSArray {
|
||||
let feedAuthors = feed.authors ?? []
|
||||
let feedAuthors = webFeed.authors ?? []
|
||||
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
|
||||
}
|
||||
|
||||
@objc(valueInAuthorsWithUniqueID:)
|
||||
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
|
||||
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
|
||||
guard let author = webFeed.authors?.first(where:{$0.authorID == id}) else { return nil }
|
||||
return ScriptableAuthor(author, container:self)
|
||||
}
|
||||
|
||||
@objc(articles)
|
||||
var articles:NSArray {
|
||||
let feedArticles = feed.fetchArticles()
|
||||
let feedArticles = webFeed.fetchArticles()
|
||||
// the articles are a set, use the sorting algorithm from the viewer
|
||||
let sortedArticles = feedArticles.sorted(by:{
|
||||
return $0.logicalDatePublished > $1.logicalDatePublished
|
||||
@@ -175,7 +175,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
|
||||
@objc(valueInArticlesWithUniqueID:)
|
||||
func valueInArticles(withUniqueID id:String) -> ScriptableArticle? {
|
||||
let articles = feed.fetchArticles()
|
||||
let articles = webFeed.fetchArticles()
|
||||
guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil }
|
||||
return ScriptableArticle(article, container:self)
|
||||
}
|
||||
Reference in New Issue
Block a user