diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift
index 5e4699fa1..b9f702e03 100644
--- a/Mac/AppAssets.swift
+++ b/Mac/AppAssets.swift
@@ -125,6 +125,10 @@ struct AppAssets {
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!)
}()
+ static var smartFeedImage: RSImage = {
+ return RSImage(named: NSImage.smartBadgeTemplateName)!
+ }()
+
static var starredFeedImage: IconImage = {
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!)
}()
diff --git a/Multiplatform/Shared/AppAssets.swift b/Multiplatform/Shared/AppAssets.swift
new file mode 100644
index 000000000..37998ed7b
--- /dev/null
+++ b/Multiplatform/Shared/AppAssets.swift
@@ -0,0 +1,153 @@
+//
+// AppAssets.swift
+// NetNewsWire
+//
+// Created by Maurice Parker on 6/27/20.
+// Copyright © 2020 Ranchero Software. All rights reserved.
+//
+
+import SwiftUI
+import RSCore
+import Account
+
+struct AppAssets {
+
+ static var accountLocalMacImage: RSImage! = {
+ return RSImage(named: "accountLocalMac")
+ }()
+
+ static var accountLocalPadImage: RSImage = {
+ return RSImage(named: "accountLocalPad")!
+ }()
+
+ static var accountLocalPhoneImage: RSImage = {
+ return RSImage(named: "accountLocalPhone")!
+ }()
+
+ static var accountCloudKitImage: RSImage = {
+ return RSImage(named: "accountCloudKit")!
+ }()
+
+ static var accountFeedbinImage: RSImage = {
+ return RSImage(named: "accountFeedbin")!
+ }()
+
+ static var accountFeedlyImage: RSImage = {
+ return RSImage(named: "accountFeedly")!
+ }()
+
+ static var accountFeedWranglerImage: RSImage = {
+ return RSImage(named: "accountFeedWrangler")!
+ }()
+
+ static var accountFreshRSSImage: RSImage = {
+ return RSImage(named: "accountFreshRSS")!
+ }()
+
+ static var accountNewsBlurImage: RSImage = {
+ return RSImage(named: "accountNewsBlur")!
+ }()
+
+ static var extensionPointMarsEdit: RSImage = {
+ return RSImage(named: "extensionPointMarsEdit")!
+ }()
+
+ static var extensionPointMicroblog: RSImage = {
+ return RSImage(named: "extensionPointMicroblog")!
+ }()
+
+ static var extensionPointReddit: RSImage = {
+ return RSImage(named: "extensionPointReddit")!
+ }()
+
+ static var extensionPointTwitter: RSImage = {
+ return RSImage(named: "extensionPointTwitter")!
+ }()
+
+ static var faviconTemplateImage: RSImage = {
+ return RSImage(named: "faviconTemplateImage")!
+ }()
+
+ static var masterFolderImage: IconImage = {
+ #if os(macOS)
+ return IconImage(NSImage(systemSymbolName: "folder.fill", accessibilityDescription: nil)!)
+ #endif
+ #if os(iOS)
+ return IconImage(UIImage(systemName: "folder.fill")!)
+ #endif
+ }()
+
+ static var searchFeedImage: IconImage = {
+ #if os(macOS)
+ return IconImage(NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: nil)!)
+ #endif
+ #if os(iOS)
+ return IconImage(UIImage(systemName: "magnifyingglass")!)
+ #endif
+ }()
+
+ static var smartFeedImage: RSImage = {
+ #if os(macOS)
+ return NSImage(systemSymbolName: "gear", accessibilityDescription: nil)!
+ #endif
+ #if os(iOS)
+ return UIImage(systemName: "gear")!
+ #endif
+ }()
+
+ static var starredFeedImage: IconImage = {
+ #if os(macOS)
+ return IconImage(NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!)
+ #endif
+ #if os(iOS)
+ return IconImage(UIImage(systemName: "star.fill")!)
+ #endif
+ }()
+
+ static var todayFeedImage: IconImage = {
+ #if os(macOS)
+ return IconImage(NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!)
+ #endif
+ #if os(iOS)
+ return IconImage(UIImage(systemName: "sun.max.fill")!)
+ #endif
+ }()
+
+ static var unreadFeedImage: IconImage = {
+ #if os(macOS)
+ return IconImage(NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)!)
+ #endif
+ #if os(iOS)
+ return IconImage(UIImage(systemName: "largecircle.fill.circle")!)
+ #endif
+ }()
+
+ static func image(for accountType: AccountType) -> RSImage? {
+ switch accountType {
+ case .onMyMac:
+ #if os(macOS)
+ return AppAssets.accountLocalMacImage
+ #endif
+ #if os(iOS)
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ return AppAssets.accountLocalPadImage
+ } else {
+ return AppAssets.accountLocalPhoneImage
+ }
+ #endif
+ case .cloudKit:
+ return AppAssets.accountCloudKitImage
+ case .feedbin:
+ return AppAssets.accountFeedbinImage
+ case .feedly:
+ return AppAssets.accountFeedlyImage
+ case .feedWrangler:
+ return AppAssets.accountFeedWranglerImage
+ case .freshRSS:
+ return AppAssets.accountFreshRSSImage
+ case .newsBlur:
+ return AppAssets.accountNewsBlurImage
+ }
+ }
+
+}
diff --git a/Multiplatform/Shared/AppDefaults.swift b/Multiplatform/Shared/AppDefaults.swift
new file mode 100644
index 000000000..5dc9f1910
--- /dev/null
+++ b/Multiplatform/Shared/AppDefaults.swift
@@ -0,0 +1,307 @@
+//
+// AppDefaults.swift
+// NetNewsWire
+//
+// Created by Maurice Parker on 6/28/20.
+// Copyright © 2020 Ranchero Software. All rights reserved.
+//
+
+import Foundation
+
+enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
+ case automatic = 0
+ case light = 1
+ case dark = 2
+
+ var description: String {
+ switch self {
+ case .automatic:
+ return NSLocalizedString("Automatic", comment: "Automatic")
+ case .light:
+ return NSLocalizedString("Light", comment: "Light")
+ case .dark:
+ return NSLocalizedString("Dark", comment: "Dark")
+ }
+ }
+
+}
+
+struct AppDefaults {
+
+ #if os(macOS)
+ static var shared: UserDefaults = UserDefaults.standard
+ #endif
+
+ #if os(iOS)
+ static var shared: UserDefaults = {
+ let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String
+ let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)"
+ return UserDefaults.init(suiteName: suiteName)!
+ }()
+ #endif
+
+ struct Key {
+ static let refreshInterval = "refreshInterval"
+ static let hideDockUnreadCount = "JustinMillerHideDockUnreadCount"
+ static let userInterfaceColorPalette = "userInterfaceColorPalette"
+ static let activeExtensionPointIDs = "activeExtensionPointIDs"
+ static let lastImageCacheFlushDate = "lastImageCacheFlushDate"
+ static let firstRunDate = "firstRunDate"
+ static let timelineGroupByFeed = "timelineGroupByFeed"
+ static let refreshClearsReadArticles = "refreshClearsReadArticles"
+ static let timelineNumberOfLines = "timelineNumberOfLines"
+ static let timelineIconSize = "timelineIconSize"
+ static let timelineSortDirection = "timelineSortDirection"
+ static let articleFullscreenAvailable = "articleFullscreenAvailable"
+ static let articleFullscreenEnabled = "articleFullscreenEnabled"
+ static let confirmMarkAllAsRead = "confirmMarkAllAsRead"
+ static let lastRefresh = "lastRefresh"
+ static let addWebFeedAccountID = "addWebFeedAccountID"
+ static let addWebFeedFolderName = "addWebFeedFolderName"
+ static let addFolderAccountID = "addFolderAccountID"
+ }
+
+ static let isDeveloperBuild: Bool = {
+ if let dev = Bundle.main.object(forInfoDictionaryKey: "DeveloperEntitlements") as? String, dev == "-dev" {
+ return true
+ }
+ return false
+ }()
+
+ static let isFirstRun: Bool = {
+ if let _ = AppDefaults.shared.object(forKey: Key.firstRunDate) as? Date {
+ return false
+ }
+ firstRunDate = Date()
+ return true
+ }()
+
+ static var refreshInterval: RefreshInterval {
+ get {
+ let rawValue = UserDefaults.standard.integer(forKey: Key.refreshInterval)
+ return RefreshInterval(rawValue: rawValue) ?? RefreshInterval.everyHour
+ }
+ set {
+ UserDefaults.standard.set(newValue.rawValue, forKey: Key.refreshInterval)
+ }
+ }
+
+ static var hideDockUnreadCount: Bool {
+ return bool(for: Key.hideDockUnreadCount)
+ }
+
+ static var userInterfaceColorPalette: UserInterfaceColorPalette {
+ get {
+ if let result = UserInterfaceColorPalette(rawValue: int(for: Key.userInterfaceColorPalette)) {
+ return result
+ }
+ return .automatic
+ }
+ set {
+ setInt(for: Key.userInterfaceColorPalette, newValue.rawValue)
+ }
+ }
+
+ static var addWebFeedAccountID: String? {
+ get {
+ return string(for: Key.addWebFeedAccountID)
+ }
+ set {
+ 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)
+ }
+ set {
+ setString(for: Key.addFolderAccountID, newValue)
+ }
+ }
+
+ static var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? {
+ get {
+ return UserDefaults.standard.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]]
+ }
+ set {
+ UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs)
+ }
+ }
+
+ static var lastImageCacheFlushDate: Date? {
+ get {
+ return date(for: Key.lastImageCacheFlushDate)
+ }
+ set {
+ setDate(for: Key.lastImageCacheFlushDate, newValue)
+ }
+ }
+
+ static var timelineGroupByFeed: Bool {
+ get {
+ return bool(for: Key.timelineGroupByFeed)
+ }
+ set {
+ setBool(for: Key.timelineGroupByFeed, newValue)
+ }
+ }
+
+ static var refreshClearsReadArticles: Bool {
+ get {
+ return bool(for: Key.refreshClearsReadArticles)
+ }
+ set {
+ setBool(for: Key.refreshClearsReadArticles, newValue)
+ }
+ }
+
+ static var timelineSortDirection: ComparisonResult {
+ get {
+ return sortDirection(for: Key.timelineSortDirection)
+ }
+ set {
+ setSortDirection(for: Key.timelineSortDirection, newValue)
+ }
+ }
+
+ static var articleFullscreenAvailable: Bool {
+ get {
+ return bool(for: Key.articleFullscreenAvailable)
+ }
+ set {
+ setBool(for: Key.articleFullscreenAvailable, newValue)
+ }
+ }
+
+ static var articleFullscreenEnabled: Bool {
+ get {
+ return bool(for: Key.articleFullscreenEnabled)
+ }
+ set {
+ setBool(for: Key.articleFullscreenEnabled, newValue)
+ }
+ }
+
+ static var confirmMarkAllAsRead: Bool {
+ get {
+ return bool(for: Key.confirmMarkAllAsRead)
+ }
+ set {
+ setBool(for: Key.confirmMarkAllAsRead, newValue)
+ }
+ }
+
+ static var lastRefresh: Date? {
+ get {
+ return date(for: Key.lastRefresh)
+ }
+ set {
+ setDate(for: Key.lastRefresh, newValue)
+ }
+ }
+
+ static var timelineNumberOfLines: Int {
+ get {
+ return int(for: Key.timelineNumberOfLines)
+ }
+ set {
+ setInt(for: Key.timelineNumberOfLines, newValue)
+ }
+ }
+
+ static var timelineIconSize: IconSize {
+ get {
+ let rawValue = AppDefaults.shared.integer(forKey: Key.timelineIconSize)
+ return IconSize(rawValue: rawValue) ?? IconSize.medium
+ }
+ set {
+ AppDefaults.shared.set(newValue.rawValue, forKey: Key.timelineIconSize)
+ }
+ }
+
+ static func registerDefaults() {
+ let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
+ Key.timelineGroupByFeed: false,
+ Key.refreshClearsReadArticles: false,
+ Key.timelineNumberOfLines: 2,
+ Key.timelineIconSize: IconSize.medium.rawValue,
+ Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
+ Key.articleFullscreenAvailable: false,
+ Key.articleFullscreenEnabled: false,
+ Key.confirmMarkAllAsRead: true]
+ AppDefaults.shared.register(defaults: defaults)
+ }
+
+}
+
+private extension AppDefaults {
+
+ static var firstRunDate: Date? {
+ get {
+ return date(for: Key.firstRunDate)
+ }
+ set {
+ setDate(for: Key.firstRunDate, newValue)
+ }
+ }
+
+ static func string(for key: String) -> String? {
+ return AppDefaults.shared.string(forKey: key)
+ }
+
+ static func setString(for key: String, _ value: String?) {
+ AppDefaults.shared.set(value, forKey: key)
+ }
+
+ static func bool(for key: String) -> Bool {
+ return AppDefaults.shared.bool(forKey: key)
+ }
+
+ static func setBool(for key: String, _ flag: Bool) {
+ AppDefaults.shared.set(flag, forKey: key)
+ }
+
+ static func int(for key: String) -> Int {
+ return AppDefaults.shared.integer(forKey: key)
+ }
+
+ static func setInt(for key: String, _ x: Int) {
+ AppDefaults.shared.set(x, forKey: key)
+ }
+
+ static func date(for key: String) -> Date? {
+ return AppDefaults.shared.object(forKey: key) as? Date
+ }
+
+ static func setDate(for key: String, _ date: Date?) {
+ AppDefaults.shared.set(date, forKey: key)
+ }
+
+ static func sortDirection(for key:String) -> ComparisonResult {
+ let rawInt = int(for: key)
+ if rawInt == ComparisonResult.orderedAscending.rawValue {
+ return .orderedAscending
+ }
+ return .orderedDescending
+ }
+
+ static func setSortDirection(for key: String, _ value: ComparisonResult) {
+ if value == .orderedAscending {
+ setInt(for: key, ComparisonResult.orderedAscending.rawValue)
+ }
+ else {
+ setInt(for: key, ComparisonResult.orderedDescending.rawValue)
+ }
+ }
+
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json
index eb8789700..bf3d625ad 100644
--- a/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json
+++ b/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -1,6 +1,33 @@
{
"colors" : [
{
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.933",
+ "green" : "0.416",
+ "red" : "0.031"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.945",
+ "green" : "0.502",
+ "red" : "0.176"
+ }
+ },
"idiom" : "universal"
}
],
diff --git a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/Contents.json
new file mode 100644
index 000000000..fc07d2975
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "icloud.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud.pdf b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud.pdf
new file mode 100644
index 000000000..74406f4cb
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/Contents.json
new file mode 100644
index 000000000..5e337ee42
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "outline-512.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/outline-512.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/outline-512.png
new file mode 100644
index 000000000..f7d29faa1
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/outline-512.png differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/Contents.json
new file mode 100644
index 000000000..a6213a3b1
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "accountFeedbin.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/accountFeedbin.pdf b/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/accountFeedbin.pdf
new file mode 100644
index 000000000..d688a02e1
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/accountFeedbin.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/Contents.json
new file mode 100644
index 000000000..4034be795
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "accountFeedly.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/accountFeedly.pdf b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/accountFeedly.pdf
new file mode 100644
index 000000000..907e486bc
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/accountFeedly.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/Contents.json
new file mode 100644
index 000000000..64a29ab45
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "accountFreshRSS.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/accountFreshRSS.pdf b/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/accountFreshRSS.pdf
new file mode 100644
index 000000000..1ff98e115
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/accountFreshRSS.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/Contents.json
new file mode 100644
index 000000000..6d51578a9
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "localAccountMac.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountMac.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountMac.pdf
new file mode 100644
index 000000000..5c3292ce0
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountMac.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/Contents.json
new file mode 100644
index 000000000..0ac81354e
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "localAccountPad.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/localAccountPad.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/localAccountPad.pdf
new file mode 100644
index 000000000..cfc6ca9f0
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/localAccountPad.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/Contents.json
new file mode 100644
index 000000000..1e6c82f37
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "localAccountPhone.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/localAccountPhone.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/localAccountPhone.pdf
new file mode 100644
index 000000000..c807edece
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/localAccountPhone.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Contents.json
new file mode 100644
index 000000000..9b23ec7d3
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "newsblur-512.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/newsblur-512.png b/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/newsblur-512.png
new file mode 100644
index 000000000..5fab67691
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/newsblur-512.png differ
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointMarsEdit.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/extensionPointMarsEdit.imageset/Contents.json
new file mode 100644
index 000000000..432281179
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/extensionPointMarsEdit.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "MarsEditOfficial.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointMarsEdit.imageset/MarsEditOfficial.pdf b/Multiplatform/Shared/Assets.xcassets/extensionPointMarsEdit.imageset/MarsEditOfficial.pdf
new file mode 100644
index 000000000..82396a369
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/extensionPointMarsEdit.imageset/MarsEditOfficial.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointMicroblog.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/extensionPointMicroblog.imageset/Contents.json
new file mode 100644
index 000000000..4db95b5b7
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/extensionPointMicroblog.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "micro-dot-blog.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointMicroblog.imageset/micro-dot-blog.pdf b/Multiplatform/Shared/Assets.xcassets/extensionPointMicroblog.imageset/micro-dot-blog.pdf
new file mode 100644
index 000000000..ad66dd377
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/extensionPointMicroblog.imageset/micro-dot-blog.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/Contents.json
new file mode 100644
index 000000000..1a49be3b3
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "filename" : "reddit-light.pdf",
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "filename" : "reddit-dark.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/reddit-dark.pdf b/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/reddit-dark.pdf
new file mode 100644
index 000000000..b4b6d7419
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/reddit-dark.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/reddit-light.pdf b/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/reddit-light.pdf
new file mode 100644
index 000000000..4d6267b63
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/extensionPointReddit.imageset/reddit-light.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointTwitter.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/extensionPointTwitter.imageset/Contents.json
new file mode 100644
index 000000000..834100cda
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/extensionPointTwitter.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "twitter.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/extensionPointTwitter.imageset/twitter.pdf b/Multiplatform/Shared/Assets.xcassets/extensionPointTwitter.imageset/twitter.pdf
new file mode 100644
index 000000000..e50de4443
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/extensionPointTwitter.imageset/twitter.pdf differ
diff --git a/Multiplatform/Shared/Assets.xcassets/faviconTemplateImage.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/faviconTemplateImage.imageset/Contents.json
new file mode 100644
index 000000000..bd3a2fbbe
--- /dev/null
+++ b/Multiplatform/Shared/Assets.xcassets/faviconTemplateImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "faviconTemplateImage.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Multiplatform/Shared/Assets.xcassets/faviconTemplateImage.imageset/faviconTemplateImage.pdf b/Multiplatform/Shared/Assets.xcassets/faviconTemplateImage.imageset/faviconTemplateImage.pdf
new file mode 100644
index 000000000..d6bcb5b69
Binary files /dev/null and b/Multiplatform/Shared/Assets.xcassets/faviconTemplateImage.imageset/faviconTemplateImage.pdf differ
diff --git a/Multiplatform/Shared/ErrorHandler.swift b/Multiplatform/Shared/ErrorHandler.swift
new file mode 100644
index 000000000..786dff5d2
--- /dev/null
+++ b/Multiplatform/Shared/ErrorHandler.swift
@@ -0,0 +1,31 @@
+//
+// ErrorHandler.swift
+// NetNewsWire
+//
+// Created by Maurice Parker on 6/28/20.
+// Copyright © 2020 Ranchero Software. All rights reserved.
+//
+
+import Foundation
+import RSCore
+import os.log
+
+struct ErrorHandler {
+
+ private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
+
+// public static func present(_ viewController: UIViewController) -> (Error) -> () {
+// return { [weak viewController] error in
+// if UIApplication.shared.applicationState == .active {
+// viewController?.presentError(error)
+// } else {
+// ErrorHandler.log(error)
+// }
+// }
+// }
+
+ public static func log(_ error: Error) {
+ os_log(.error, log: self.log, "%@", error.localizedDescription)
+ }
+
+}
diff --git a/Multiplatform/Shared/NetNewsWire_MultiplatformApp.swift b/Multiplatform/Shared/NetNewsWire.swift
similarity index 50%
rename from Multiplatform/Shared/NetNewsWire_MultiplatformApp.swift
rename to Multiplatform/Shared/NetNewsWire.swift
index 4173d0887..720ac2677 100644
--- a/Multiplatform/Shared/NetNewsWire_MultiplatformApp.swift
+++ b/Multiplatform/Shared/NetNewsWire.swift
@@ -1,5 +1,5 @@
//
-// NetNewsWire_MultiplatformApp.swift
+// NetNewsWire.swift
// Shared
//
// Created by Maurice Parker on 6/27/20.
@@ -9,7 +9,15 @@
import SwiftUI
@main
-struct NetNewsWire_MultiplatformApp: App {
+struct NetNewsWire: App {
+
+ #if os(macOS)
+ @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
+ #endif
+ #if os(iOS)
+ @UIApplicationDelegateAdaptor(AppDelegate.self) private var delegate
+ #endif
+
var body: some Scene {
WindowGroup {
ContentView()
diff --git a/Multiplatform/iOS/AppDelegate.swift b/Multiplatform/iOS/AppDelegate.swift
new file mode 100644
index 000000000..6124a75bb
--- /dev/null
+++ b/Multiplatform/iOS/AppDelegate.swift
@@ -0,0 +1,390 @@
+//
+// AppDelegate.swift
+// Multiplatform iOS
+//
+// Created by Maurice Parker on 6/28/20.
+// Copyright © 2020 Ranchero Software. All rights reserved.
+//
+
+import UIKit
+import RSCore
+import RSWeb
+import Account
+import BackgroundTasks
+import os.log
+
+var appDelegate: AppDelegate!
+
+class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {
+
+ private var bgTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler")
+
+ private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
+ private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
+
+ var syncTimer: ArticleStatusSyncTimer?
+
+ var shuttingDown = false {
+ didSet {
+ if shuttingDown {
+ syncTimer?.shuttingDown = shuttingDown
+ syncTimer?.invalidate()
+ }
+ }
+ }
+
+ var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
+
+ var userNotificationManager: UserNotificationManager!
+ var faviconDownloader: FaviconDownloader!
+ var imageDownloader: ImageDownloader!
+ var authorAvatarDownloader: AuthorAvatarDownloader!
+ var webFeedIconDownloader: WebFeedIconDownloader!
+ // TODO: Add Extension back in
+// var extensionContainersFile: ExtensionContainersFile!
+// var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
+
+ var unreadCount = 0 {
+ didSet {
+ if unreadCount != oldValue {
+ postUnreadCountDidChangeNotification()
+ UIApplication.shared.applicationIconBadgeNumber = unreadCount
+ }
+ }
+ }
+
+ var isSyncArticleStatusRunning = false
+ var isWaitingForSyncTasks = false
+
+ override init() {
+ super.init()
+ appDelegate = self
+
+ let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts").absoluteString
+ let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
+ AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath)
+ FeedProviderManager.shared.delegate = ExtensionPointManager.shared
+
+ NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
+ }
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ AppDefaults.registerDefaults()
+
+ let isFirstRun = AppDefaults.isFirstRun
+ if isFirstRun {
+ os_log("Is first run.", log: log, type: .info)
+ }
+
+ if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
+ let localAccount = AccountManager.shared.defaultAccount
+ DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
+ }
+
+ registerBackgroundTasks()
+ CacheCleaner.purgeIfNecessary()
+ initializeDownloaders()
+ initializeHomeScreenQuickActions()
+
+ DispatchQueue.main.async {
+ self.unreadCount = AccountManager.shared.unreadCount
+ }
+
+ UNUserNotificationCenter.current().getNotificationSettings { (settings) in
+ if settings.authorizationStatus == .authorized {
+ DispatchQueue.main.async {
+ UIApplication.shared.registerForRemoteNotifications()
+ }
+ }
+ }
+
+ UNUserNotificationCenter.current().delegate = self
+ userNotificationManager = UserNotificationManager()
+
+// extensionContainersFile = ExtensionContainersFile()
+// extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
+
+ syncTimer = ArticleStatusSyncTimer()
+
+ #if DEBUG
+ syncTimer!.update()
+ #endif
+
+ return true
+
+ }
+
+ func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
+ DispatchQueue.main.async {
+ self.resumeDatabaseProcessingIfNecessary()
+ AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
+ self.suspendApplication()
+ completionHandler(.newData)
+ }
+ }
+ }
+
+ func applicationWillTerminate(_ application: UIApplication) {
+ shuttingDown = true
+ }
+
+ // MARK: Notifications
+
+ @objc func unreadCountDidChange(_ note: Notification) {
+ if note.object is AccountManager {
+ unreadCount = AccountManager.shared.unreadCount
+ }
+ }
+
+ @objc func accountRefreshDidFinish(_ note: Notification) {
+ AppDefaults.lastRefresh = Date()
+ }
+
+ // MARK: - API
+
+ func resumeDatabaseProcessingIfNecessary() {
+ if AccountManager.shared.isSuspended {
+ AccountManager.shared.resumeAll()
+ os_log("Application processing resumed.", log: self.log, type: .info)
+ }
+ }
+
+ func prepareAccountsForBackground() {
+// extensionFeedAddRequestFile.suspend()
+ syncTimer?.invalidate()
+ scheduleBackgroundFeedRefresh()
+ syncArticleStatus()
+ waitForSyncTasksToFinish()
+ }
+
+ func prepareAccountsForForeground() {
+// extensionFeedAddRequestFile.resume()
+ syncTimer?.update()
+
+ if let lastRefresh = AppDefaults.lastRefresh {
+ if Date() > lastRefresh.addingTimeInterval(15 * 60) {
+ AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
+ } else {
+ AccountManager.shared.syncArticleStatusAll()
+ }
+ } else {
+ AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
+ }
+ }
+
+ func logMessage(_ message: String, type: LogItem.ItemType) {
+ print("logMessage: \(message) - \(type)")
+
+ }
+
+ func logDebugMessage(_ message: String) {
+ logMessage(message, type: .debug)
+ }
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+ completionHandler([.banner, .badge, .sound])
+ }
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
+ defer { completionHandler() }
+
+ // TODO: Add back in User Notification handling
+// if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate {
+// sceneDelegate.handle(response)
+// }
+
+ }
+
+}
+
+// MARK: App Initialization
+
+private extension AppDelegate {
+
+ private func initializeDownloaders() {
+ let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+ let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
+ let imagesFolderURL = tempDir.appendingPathComponent("Images")
+
+ try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
+ let faviconsFolder = faviconsFolderURL.absoluteString
+ let faviconsFolderPath = faviconsFolder.suffix(from: faviconsFolder.index(faviconsFolder.startIndex, offsetBy: 7))
+ faviconDownloader = FaviconDownloader(folder: String(faviconsFolderPath))
+
+ let imagesFolder = imagesFolderURL.absoluteString
+ let imagesFolderPath = imagesFolder.suffix(from: imagesFolder.index(imagesFolder.startIndex, offsetBy: 7))
+ try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
+ imageDownloader = ImageDownloader(folder: String(imagesFolderPath))
+
+ authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
+
+ let tempFolder = tempDir.absoluteString
+ let tempFolderPath = tempFolder.suffix(from: tempFolder.index(tempFolder.startIndex, offsetBy: 7))
+ webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: String(tempFolderPath))
+ }
+
+ private func initializeHomeScreenQuickActions() {
+ let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread")
+ let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle")
+ let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil)
+
+ let searchTitle = NSLocalizedString("Search", comment: "Search")
+ let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass")
+ let searchItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowSearch", localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil)
+
+ let addTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
+ let addIcon = UIApplicationShortcutIcon(systemImageName: "plus")
+ let addItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowAdd", localizedTitle: addTitle, localizedSubtitle: nil, icon: addIcon, userInfo: nil)
+
+ UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem]
+ }
+
+}
+
+// MARK: Go To Background
+
+private extension AppDelegate {
+
+ func waitForSyncTasksToFinish() {
+ guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return }
+
+ isWaitingForSyncTasks = true
+
+ self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in
+ guard let self = self else { return }
+ self.completeProcessing(true)
+ os_log("Accounts wait for progress terminated for running too long.", log: self.log, type: .info)
+ }
+
+ DispatchQueue.main.async { [weak self] in
+ self?.waitToComplete() { [weak self] suspend in
+ self?.completeProcessing(suspend)
+ }
+ }
+ }
+
+ func waitToComplete(completion: @escaping (Bool) -> Void) {
+ guard UIApplication.shared.applicationState == .background else {
+ os_log("App came back to forground, no longer waiting.", log: self.log, type: .info)
+ completion(false)
+ return
+ }
+
+ if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning {
+ os_log("Waiting for sync to finish...", log: self.log, type: .info)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ self?.waitToComplete(completion: completion)
+ }
+ } else {
+ os_log("Refresh progress complete.", log: self.log, type: .info)
+ completion(true)
+ }
+ }
+
+ func completeProcessing(_ suspend: Bool) {
+ if suspend {
+ suspendApplication()
+ }
+ UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask)
+ self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
+ isWaitingForSyncTasks = false
+ }
+
+ func syncArticleStatus() {
+ guard !isSyncArticleStatusRunning else { return }
+
+ isSyncArticleStatusRunning = true
+
+ let completeProcessing = { [unowned self] in
+ self.isSyncArticleStatusRunning = false
+ UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
+ self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
+ }
+
+ self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
+ completeProcessing()
+ os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info)
+ }
+
+ DispatchQueue.main.async {
+ AccountManager.shared.syncArticleStatusAll() {
+ completeProcessing()
+ }
+ }
+ }
+
+ func suspendApplication() {
+ guard UIApplication.shared.applicationState == .background else { return }
+
+ AccountManager.shared.suspendNetworkAll()
+ AccountManager.shared.suspendDatabaseAll()
+ CoalescingQueue.standard.performCallsImmediately()
+
+ os_log("Application processing suspended.", log: self.log, type: .info)
+ }
+
+}
+
+// MARK: Background Tasks
+
+private extension AppDelegate {
+
+ /// Register all background tasks.
+ func registerBackgroundTasks() {
+ // Register background feed refresh.
+ BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.ranchero.NetNewsWire.FeedRefresh", using: nil) { (task) in
+ self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask)
+ }
+ }
+
+ /// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
+ func scheduleBackgroundFeedRefresh() {
+ let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh")
+ request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
+
+ // We send this to a dedicated serial queue because as of 11/05/19 on iOS 13.2 the call to the
+ // task scheduler can hang indefinitely.
+ bgTaskDispatchQueue.async {
+ do {
+ try BGTaskScheduler.shared.submit(request)
+ } catch {
+ os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription)
+ }
+ }
+ }
+
+ /// Performs background feed refresh.
+ /// - Parameter task: `BGAppRefreshTask`
+ /// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work.
+ func performBackgroundFeedRefresh(with task: BGAppRefreshTask) {
+
+ scheduleBackgroundFeedRefresh() // schedule next refresh
+
+ os_log("Woken to perform account refresh.", log: self.log, type: .info)
+
+ DispatchQueue.main.async {
+ if AccountManager.shared.isSuspended {
+ AccountManager.shared.resumeAll()
+ }
+ AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in
+ if !AccountManager.shared.isSuspended {
+ self.suspendApplication()
+ os_log("Account refresh operation completed.", log: self.log, type: .info)
+ task.setTaskCompleted(success: true)
+ }
+ }
+ }
+
+ // set expiration handler
+ task.expirationHandler = { [weak task] in
+ DispatchQueue.main.sync {
+ self.suspendApplication()
+ }
+ os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info)
+ task?.setTaskCompleted(success: false)
+ }
+ }
+
+}
diff --git a/Multiplatform/iOS/Info.plist b/Multiplatform/iOS/Info.plist
index efc211a0c..0db629280 100644
--- a/Multiplatform/iOS/Info.plist
+++ b/Multiplatform/iOS/Info.plist
@@ -46,5 +46,40 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ BGTaskSchedulerPermittedIdentifiers
+
+ com.ranchero.NetNewsWire.FeedRefresh
+
+ NSPhotoLibraryAddUsageDescription
+ Grant permission to save images from the article.
+ NSUserActivityTypes
+
+ AddWebFeedIntent
+ NextUnread
+ ReadArticle
+ Restoration
+ SelectFeed
+
+ UIBackgroundModes
+
+ fetch
+ processing
+ remote-notification
+
+ UserAgent
+ NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/)
+ OrganizationIdentifier
+ $(ORGANIZATION_IDENTIFIER)
+ DeveloperEntitlements
+ $(DEVELOPER_ENTITLEMENTS)
+ AppGroup
+ group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS
+ AppIdentifierPrefix
+ $(AppIdentifierPrefix)
diff --git a/Multiplatform/macOS/AppDelegate.swift b/Multiplatform/macOS/AppDelegate.swift
new file mode 100644
index 000000000..f642ce2e8
--- /dev/null
+++ b/Multiplatform/macOS/AppDelegate.swift
@@ -0,0 +1,286 @@
+//
+// AppDelegate.swift
+// Multiplatform macOS
+//
+// Created by Maurice Parker on 6/28/20.
+// Copyright © 2020 Ranchero Software. All rights reserved.
+//
+
+import AppKit
+import os.log
+import UserNotifications
+import Articles
+import RSTree
+import RSWeb
+import Account
+import RSCore
+
+// If we're not going to import Sparkle, provide dummy protocols to make it easy
+// for AppDelegate to comply
+#if MAC_APP_STORE || TEST
+protocol SPUStandardUserDriverDelegate {}
+protocol SPUUpdaterDelegate {}
+#else
+import Sparkle
+#endif
+
+var appDelegate: AppDelegate!
+
+class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate
+{
+
+ private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
+
+ var userNotificationManager: UserNotificationManager!
+ var faviconDownloader: FaviconDownloader!
+ var imageDownloader: ImageDownloader!
+ var authorAvatarDownloader: AuthorAvatarDownloader!
+ var webFeedIconDownloader: WebFeedIconDownloader!
+
+ var refreshTimer: AccountRefreshTimer?
+ var syncTimer: ArticleStatusSyncTimer?
+
+ var shuttingDown = false {
+ didSet {
+ if shuttingDown {
+ refreshTimer?.shuttingDown = shuttingDown
+ refreshTimer?.invalidate()
+ syncTimer?.shuttingDown = shuttingDown
+ syncTimer?.invalidate()
+ }
+ }
+ }
+
+ var unreadCount = 0 {
+ didSet {
+ if unreadCount != oldValue {
+ CoalescingQueue.standard.add(self, #selector(updateDockBadge))
+ postUnreadCountDidChangeNotification()
+ }
+ }
+ }
+
+ var appName: String!
+
+ private let appNewsURLString = "https://nnw.ranchero.com/feed.json"
+ private let appMovementMonitor = RSAppMovementMonitor()
+ #if !MAC_APP_STORE && !TEST
+ private var softwareUpdater: SPUUpdater!
+ #endif
+
+ override init() {
+ NSWindow.allowsAutomaticWindowTabbing = false
+ super.init()
+
+ AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
+ FeedProviderManager.shared.delegate = ExtensionPointManager.shared
+
+ NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
+ NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
+
+ appDelegate = self
+ }
+
+ // MARK: - NSApplicationDelegate
+
+ func applicationWillFinishLaunching(_ notification: Notification) {
+ // TODO: add Apple Events back in
+// installAppleEventHandlers()
+
+ CacheCleaner.purgeIfNecessary()
+
+ // 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)
+ faviconDownloader = FaviconDownloader(folder: faviconsFolder)
+
+ let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images")
+ let imagesFolderURL = URL(fileURLWithPath: imagesFolder)
+ try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
+ imageDownloader = ImageDownloader(folder: imagesFolder)
+
+ authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
+ webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
+
+ appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String)
+ }
+
+ func applicationDidFinishLaunching(_ note: Notification) {
+
+ #if MAC_APP_STORE || TEST
+ checkForUpdatesMenuItem.isHidden = true
+ #else
+ // Initialize Sparkle...
+ let hostBundle = Bundle.main
+ let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self)
+ self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self)
+
+ do {
+ try self.softwareUpdater.start()
+ }
+ catch {
+ NSLog("Failed to start software updater with error: \(error)")
+ }
+ #endif
+
+ AppDefaults.registerDefaults()
+ let isFirstRun = AppDefaults.isFirstRun
+ if isFirstRun {
+ os_log(.debug, log: log, "Is first run.")
+ }
+ let localAccount = AccountManager.shared.defaultAccount
+
+ if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
+ DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
+ }
+
+
+ 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 {
+ self.unreadCount = AccountManager.shared.unreadCount
+ }
+
+ refreshTimer = AccountRefreshTimer()
+ syncTimer = ArticleStatusSyncTimer()
+
+ UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in }
+ NSApplication.shared.registerForRemoteNotifications()
+
+ UNUserNotificationCenter.current().delegate = self
+ userNotificationManager = UserNotificationManager()
+
+ // TODO: Add a debug menu
+// if AppDefaults.showDebugMenu {
+// refreshTimer!.update()
+// syncTimer!.update()
+//
+// // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build.
+// #if MAC_APP_STORE
+// let debugMenu = debugMenuItem.submenu!
+// let toggleWebInspectorItemIndex = debugMenu.indexOfItem(withTarget: self, andAction: #selector(toggleWebInspectorEnabled(_:)))
+// if toggleWebInspectorItemIndex != -1 {
+// debugMenu.removeItem(at: toggleWebInspectorItemIndex)
+// }
+// #endif
+// } else {
+// debugMenuItem.menu?.removeItem(debugMenuItem)
+// DispatchQueue.main.async {
+// self.refreshTimer!.timedRefresh(nil)
+// self.syncTimer!.timedRefresh(nil)
+// }
+// }
+
+ // TODO: Add back in crash reporter
+// #if !MAC_APP_STORE
+// DispatchQueue.main.async {
+// CrashReporter.check(appName: "NetNewsWire")
+// }
+// #endif
+
+ }
+
+ func applicationDidBecomeActive(_ notification: Notification) {
+ fireOldTimers()
+ }
+
+ func applicationDidResignActive(_ notification: Notification) {
+ ArticleStringFormatter.emptyCaches()
+ }
+
+ func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
+ AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
+ }
+
+ func applicationWillTerminate(_ notification: Notification) {
+ shuttingDown = true
+ }
+
+ // MARK: Notifications
+ @objc func unreadCountDidChange(_ note: Notification) {
+ if note.object is AccountManager {
+ unreadCount = AccountManager.shared.unreadCount
+ }
+ }
+
+ @objc func webFeedSettingDidChange(_ note: Notification) {
+ guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
+ return
+ }
+ if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
+ let _ = faviconDownloader.favicon(for: feed)
+ }
+ }
+
+ @objc func userDefaultsDidChange(_ note: Notification) {
+ refreshTimer?.update()
+ updateDockBadge()
+ }
+
+ @objc func didWakeNotification(_ note: Notification) {
+ fireOldTimers()
+ }
+
+ // MARK: UNUserNotificationCenterDelegate
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+ completionHandler([.banner, .badge, .sound])
+ }
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
+// TODO: Add back in Notification handling
+// mainWindowController?.handle(response)
+ completionHandler()
+ }
+
+ // MARK: - Dock Badge
+ @objc func updateDockBadge() {
+ let label = unreadCount > 0 && !AppDefaults.hideDockUnreadCount ? "\(unreadCount)" : ""
+ NSApplication.shared.dockTile.badgeLabel = label
+ }
+
+}
+
+private extension AppDelegate {
+
+ func fireOldTimers() {
+ // It’s possible there’s a refresh timer set to go off in the past.
+ // In that case, refresh now and update the timer.
+ refreshTimer?.fireOldTimer()
+ syncTimer?.fireOldTimer()
+ }
+
+}
+
+/*
+ the ScriptingAppDelegate protocol exposes a narrow set of accessors with
+ internal visibility which are very similar to some private vars.
+
+ These would be unnecessary if the similar accessors were marked internal rather than private,
+ but for now, we'll keep the stratification of visibility
+*/
+//extension AppDelegate : ScriptingAppDelegate {
+//
+// internal var scriptingMainWindowController: ScriptingMainWindowController? {
+// return mainWindowController
+// }
+//
+// internal var scriptingCurrentArticle: Article? {
+// return self.scriptingMainWindowController?.scriptingCurrentArticle
+// }
+//
+// internal var scriptingSelectedArticles: [Article] {
+// return self.scriptingMainWindowController?.scriptingSelectedArticles ?? []
+// }
+//}
diff --git a/Multiplatform/macOS/Info.plist b/Multiplatform/macOS/Info.plist
index 705980986..a4af5e913 100644
--- a/Multiplatform/macOS/Info.plist
+++ b/Multiplatform/macOS/Info.plist
@@ -24,5 +24,16 @@
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
Copyright © 2020 Ranchero Software. All rights reserved.
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ UserAgent
+ NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/)
+ OrganizationIdentifier
+ $(ORGANIZATION_IDENTIFIER)
+ DeveloperEntitlements
+ $(DEVELOPER_ENTITLEMENTS)
diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj
index bd14b6619..00b2b37e8 100644
--- a/NetNewsWire.xcodeproj/project.pbxproj
+++ b/NetNewsWire.xcodeproj/project.pbxproj
@@ -251,8 +251,8 @@
51BC4B00247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
51BC4B01247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
51BEB22D2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */; };
- 51C0515E24A77DF800194D5E /* NetNewsWire_MultiplatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513624A77DF700194D5E /* NetNewsWire_MultiplatformApp.swift */; };
- 51C0515F24A77DF800194D5E /* NetNewsWire_MultiplatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513624A77DF700194D5E /* NetNewsWire_MultiplatformApp.swift */; };
+ 51C0515E24A77DF800194D5E /* NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513624A77DF700194D5E /* NetNewsWire.swift */; };
+ 51C0515F24A77DF800194D5E /* NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513624A77DF700194D5E /* NetNewsWire.swift */; };
51C0516024A77DF800194D5E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513724A77DF700194D5E /* ContentView.swift */; };
51C0516124A77DF800194D5E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513724A77DF700194D5E /* ContentView.swift */; };
51C0516224A77DF800194D5E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51C0513824A77DF800194D5E /* Assets.xcassets */; };
@@ -344,6 +344,189 @@
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; };
51E43962238037C400015C31 /* AddWebFeedFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E43961238037C400015C31 /* AddWebFeedFolderViewController.swift */; };
51E4398023805EBC00015C31 /* AddComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4397F23805EBC00015C31 /* AddComboTableViewCell.swift */; };
+ 51E4987F24A8061400B667CB /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; };
+ 51E4988024A8061400B667CB /* Account.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988124A8061400B667CB /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; };
+ 51E4988224A8061400B667CB /* Articles.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988324A8061400B667CB /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; };
+ 51E4988424A8061400B667CB /* ArticlesDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988524A8061400B667CB /* OAuthSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 517A755324451BD500B553B9 /* OAuthSwift.framework */; };
+ 51E4988624A8061400B667CB /* OAuthSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 517A755324451BD500B553B9 /* OAuthSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988724A8061400B667CB /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8520DD8CF200CA8CF5 /* RSCore.framework */; };
+ 51E4988824A8061400B667CB /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8520DD8CF200CA8CF5 /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988924A8061400B667CB /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FC420DD8E0C00CA8CF5 /* RSDatabase.framework */; };
+ 51E4988A24A8061400B667CB /* RSDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FC420DD8E0C00CA8CF5 /* RSDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988B24A8061400B667CB /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8C20DD8CF800CA8CF5 /* RSParser.framework */; };
+ 51E4988C24A8061400B667CB /* RSParser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8C20DD8CF800CA8CF5 /* RSParser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988D24A8061400B667CB /* RSTree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; };
+ 51E4988E24A8061400B667CB /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4988F24A8061400B667CB /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FA320DD8D0500CA8CF5 /* RSWeb.framework */; };
+ 51E4989024A8061400B667CB /* RSWeb.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FA320DD8D0500CA8CF5 /* RSWeb.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4989124A8061400B667CB /* Secrets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5102FD7B244008A700534F17 /* Secrets.framework */; };
+ 51E4989224A8061400B667CB /* Secrets.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5102FD7B244008A700534F17 /* Secrets.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4989324A8061400B667CB /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
+ 51E4989424A8061400B667CB /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4989624A8065700B667CB /* CloudKit.framework */; };
+ 51E4989924A8067000B667CB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4989824A8067000B667CB /* WebKit.framework */; };
+ 51E4989A24A8069300B667CB /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; };
+ 51E4989B24A8069300B667CB /* Account.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4989C24A8069300B667CB /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; };
+ 51E4989D24A8069300B667CB /* Articles.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E4989E24A8069300B667CB /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; };
+ 51E4989F24A8069300B667CB /* ArticlesDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498A024A8069300B667CB /* OAuthSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 517A755524451BD500B553B9 /* OAuthSwift.framework */; };
+ 51E498A124A8069300B667CB /* OAuthSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 517A755524451BD500B553B9 /* OAuthSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498A224A8069300B667CB /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8120DD8CF200CA8CF5 /* RSCore.framework */; };
+ 51E498A324A8069300B667CB /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8120DD8CF200CA8CF5 /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498A424A8069300B667CB /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FC020DD8E0C00CA8CF5 /* RSDatabase.framework */; };
+ 51E498A524A8069300B667CB /* RSDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FC020DD8E0C00CA8CF5 /* RSDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498A624A8069300B667CB /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8C20DD8CF800CA8CF5 /* RSParser.framework */; };
+ 51E498A724A8069300B667CB /* RSParser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8C20DD8CF800CA8CF5 /* RSParser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498A824A8069300B667CB /* RSTree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; };
+ 51E498A924A8069300B667CB /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498AA24A8069300B667CB /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9F20DD8D0500CA8CF5 /* RSWeb.framework */; };
+ 51E498AB24A8069300B667CB /* RSWeb.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9F20DD8D0500CA8CF5 /* RSWeb.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498AC24A8069300B667CB /* Secrets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5102FD7B244008A700534F17 /* Secrets.framework */; };
+ 51E498AD24A8069300B667CB /* Secrets.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5102FD7B244008A700534F17 /* Secrets.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498AE24A8069300B667CB /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
+ 51E498AF24A8069300B667CB /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 51E498B124A806A400B667CB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DAEC2425F6940091EB5B /* CloudKit.framework */; };
+ 51E498B324A806AA00B667CB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E498B224A806AA00B667CB /* WebKit.framework */; };
+ 51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; };
+ 51E498C824A8085D00B667CB /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; };
+ 51E498C924A8085D00B667CB /* PseudoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */; };
+ 51E498CA24A8085D00B667CB /* SmartFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */; };
+ 51E498CB24A8085D00B667CB /* TodayFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */; };
+ 51E498CC24A8085D00B667CB /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
+ 51E498CD24A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
+ 51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5391FC2308B00998D64 /* UnreadFeed.swift */; };
+ 51E498CF24A8085D00B667CB /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; };
+ 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; };
+ 51E498F224A8085D00B667CB /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; };
+ 51E498F324A8085D00B667CB /* PseudoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */; };
+ 51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */; };
+ 51E498F524A8085D00B667CB /* TodayFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */; };
+ 51E498F624A8085D00B667CB /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
+ 51E498F724A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
+ 51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5391FC2308B00998D64 /* UnreadFeed.swift */; };
+ 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; };
+ 51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; };
+ 51E498FB24A808BA00B667CB /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; };
+ 51E498FC24A808BA00B667CB /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
+ 51E498FD24A808BA00B667CB /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; };
+ 51E498FE24A808BA00B667CB /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; };
+ 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; };
+ 51E4990024A808BB00B667CB /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; };
+ 51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
+ 51E4990224A808BB00B667CB /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; };
+ 51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; };
+ 51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* WebFeedIconDownloader.swift */; };
+ 51E4990524A808C300B667CB /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; };
+ 51E4990624A808C300B667CB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; };
+ 51E4990724A808C300B667CB /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; };
+ 51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; };
+ 51E4990924A808C500B667CB /* WebFeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* WebFeedIconDownloader.swift */; };
+ 51E4990A24A808C500B667CB /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; };
+ 51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; };
+ 51E4990C24A808C500B667CB /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; };
+ 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; };
+ 51E4990E24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; };
+ 51E4990F24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; };
+ 51E4991024A808DE00B667CB /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; };
+ 51E4991124A808DE00B667CB /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; };
+ 51E4991224A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
+ 51E4991324A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
+ 51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */; };
+ 51E4991524A808FF00B667CB /* ArticleStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */; };
+ 51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */; };
+ 51E4991724A8090400B667CB /* ArticleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */; };
+ 51E4991824A8090A00B667CB /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
+ 51E4991924A8090A00B667CB /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
+ 51E4991A24A8090F00B667CB /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; };
+ 51E4991B24A8091000B667CB /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; };
+ 51E4991C24A8092000B667CB /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
+ 51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
+ 51E4991E24A8094300B667CB /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
+ 51E4991F24A8094300B667CB /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
+ 51E4992024A8095000B667CB /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
+ 51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
+ 51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
+ 51E4992324A8095700B667CB /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
+ 51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; };
+ 51E4992624A80AAB00B667CB /* AppAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4992524A80AAB00B667CB /* AppAssets.swift */; };
+ 51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4992524A80AAB00B667CB /* AppAssets.swift */; };
+ 51E4992924A866F000B667CB /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4992824A866F000B667CB /* AppDefaults.swift */; };
+ 51E4992A24A866F000B667CB /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4992824A866F000B667CB /* AppDefaults.swift */; };
+ 51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
+ 51E4992C24A8676300B667CB /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
+ 51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */; };
+ 51E4992E24A8676300B667CB /* FetchRequestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */; };
+ 51E4992F24A8676400B667CB /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
+ 51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
+ 51E4993124A8676400B667CB /* FetchRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */; };
+ 51E4993224A8676400B667CB /* FetchRequestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */; };
+ 51E4993324A867E700B667CB /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */; };
+ 51E4993424A867E700B667CB /* UserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9805237DCAC90028BCAA /* UserInfoKey.swift */; };
+ 51E4993524A867E800B667CB /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */; };
+ 51E4993624A867E800B667CB /* UserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9805237DCAC90028BCAA /* UserInfoKey.swift */; };
+ 51E4993724A8680E00B667CB /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513228F2233037620033D4ED /* Reachability.swift */; };
+ 51E4993824A8680E00B667CB /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513228F2233037620033D4ED /* Reachability.swift */; };
+ 51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4993924A8708800B667CB /* AppDelegate.swift */; };
+ 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4993B24A8709900B667CB /* AppDelegate.swift */; };
+ 51E4993D24A870F800B667CB /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
+ 51E4993E24A870F900B667CB /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
+ 51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
+ 51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; };
+ 51E4994124A8713B00B667CB /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
+ 51E4994224A8713C00B667CB /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
+ 51E4994424A8713C00B667CB /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; };
+ 51E4994524A872AD00B667CB /* org.sparkle-project.Downloader.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 65ED42BC235E71B40081F399 /* org.sparkle-project.Downloader.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 51E4994624A872AD00B667CB /* org.sparkle-project.InstallerConnection.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 65ED42B8235E71B40081F399 /* org.sparkle-project.InstallerConnection.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 51E4994724A872AD00B667CB /* org.sparkle-project.InstallerLauncher.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 65ED42B6235E71B40081F399 /* org.sparkle-project.InstallerLauncher.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 51E4994824A872AD00B667CB /* org.sparkle-project.InstallerStatus.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 65ED42BA235E71B40081F399 /* org.sparkle-project.InstallerStatus.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; };
+ 51E4994B24A8734C00B667CB /* SendToMicroBlogCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */; };
+ 51E4994C24A8734C00B667CB /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
+ 51E4994D24A8734C00B667CB /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; };
+ 51E4994E24A8734C00B667CB /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; };
+ 51E4994F24A8734C00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; };
+ 51E4995024A8734C00B667CB /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; };
+ 51E4995124A8734D00B667CB /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; };
+ 51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
+ 51E4995424A8734D00B667CB /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; };
+ 51E4995624A8734D00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; };
+ 51E4995724A8734D00B667CB /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; };
+ 51E4995924A873F900B667CB /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4995824A873F900B667CB /* ErrorHandler.swift */; };
+ 51E4995A24A873F900B667CB /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4995824A873F900B667CB /* ErrorHandler.swift */; };
+ 51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */; };
+ 51E4995C24A875F300B667CB /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; };
+ 51E4995D24A875F300B667CB /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
+ 51E4995E24A875F300B667CB /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
+ 51E4995F24A875F300B667CB /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; };
+ 51E4996024A875F300B667CB /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
+ 51E4996124A875F400B667CB /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; };
+ 51E4996224A875F400B667CB /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
+ 51E4996324A875F400B667CB /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
+ 51E4996424A875F400B667CB /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; };
+ 51E4996524A875F400B667CB /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
+ 51E4996624A8760B00B667CB /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; };
+ 51E4996724A8760B00B667CB /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; };
+ 51E4996824A8760C00B667CB /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; };
+ 51E4996924A8760C00B667CB /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; };
+ 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; };
+ 51E4996B24A8762D00B667CB /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; };
+ 51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; };
+ 51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; };
+ 51E4996E24A8764C00B667CB /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
+ 51E4996F24A8764C00B667CB /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
+ 51E4997024A8764C00B667CB /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
+ 51E4997124A8764C00B667CB /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
+ 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; };
+ 51E4997324A8784300B667CB /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
+ 51E4997424A8784400B667CB /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; };
+ 51E4997524A8784400B667CB /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
+ 51E4997624A87FFC00B667CB /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65ED42B0235E71B40081F399 /* Sparkle.framework */; };
+ 51E4997724A87FFC00B667CB /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 65ED42B0235E71B40081F399 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DAEC2425F6940091EB5B /* CloudKit.framework */; };
51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DB072425F9EB0091EB5B /* CloudKit.framework */; };
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
@@ -1320,6 +1503,63 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
+ 51E4989524A8061400B667CB /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 51E4989024A8061400B667CB /* RSWeb.framework in Embed Frameworks */,
+ 51E4988A24A8061400B667CB /* RSDatabase.framework in Embed Frameworks */,
+ 51E4988E24A8061400B667CB /* RSTree.framework in Embed Frameworks */,
+ 51E4988024A8061400B667CB /* Account.framework in Embed Frameworks */,
+ 51E4988224A8061400B667CB /* Articles.framework in Embed Frameworks */,
+ 51E4988624A8061400B667CB /* OAuthSwift.framework in Embed Frameworks */,
+ 51E4989424A8061400B667CB /* SyncDatabase.framework in Embed Frameworks */,
+ 51E4988824A8061400B667CB /* RSCore.framework in Embed Frameworks */,
+ 51E4988C24A8061400B667CB /* RSParser.framework in Embed Frameworks */,
+ 51E4989224A8061400B667CB /* Secrets.framework in Embed Frameworks */,
+ 51E4988424A8061400B667CB /* ArticlesDatabase.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 51E498B024A8069300B667CB /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 51E498AB24A8069300B667CB /* RSWeb.framework in Embed Frameworks */,
+ 51E498A524A8069300B667CB /* RSDatabase.framework in Embed Frameworks */,
+ 51E498A924A8069300B667CB /* RSTree.framework in Embed Frameworks */,
+ 51E4989B24A8069300B667CB /* Account.framework in Embed Frameworks */,
+ 51E4989D24A8069300B667CB /* Articles.framework in Embed Frameworks */,
+ 51E498A124A8069300B667CB /* OAuthSwift.framework in Embed Frameworks */,
+ 51E498AF24A8069300B667CB /* SyncDatabase.framework in Embed Frameworks */,
+ 51E4997724A87FFC00B667CB /* Sparkle.framework in Embed Frameworks */,
+ 51E498A324A8069300B667CB /* RSCore.framework in Embed Frameworks */,
+ 51E498A724A8069300B667CB /* RSParser.framework in Embed Frameworks */,
+ 51E498AD24A8069300B667CB /* Secrets.framework in Embed Frameworks */,
+ 51E4989F24A8069300B667CB /* ArticlesDatabase.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 51E4994924A872AD00B667CB /* Embed XPC Services */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
+ dstSubfolderSpec = 16;
+ files = (
+ 51E4994824A872AD00B667CB /* org.sparkle-project.InstallerStatus.xpc in Embed XPC Services */,
+ 51E4994624A872AD00B667CB /* org.sparkle-project.InstallerConnection.xpc in Embed XPC Services */,
+ 51E4994724A872AD00B667CB /* org.sparkle-project.InstallerLauncher.xpc in Embed XPC Services */,
+ 51E4994524A872AD00B667CB /* org.sparkle-project.Downloader.xpc in Embed XPC Services */,
+ );
+ name = "Embed XPC Services";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
6581C75720CED60100F4AD34 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -1565,7 +1805,7 @@
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = ""; };
51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL-Extensions.swift"; sourceTree = ""; };
51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterEnterDetailTableViewController.swift; sourceTree = ""; };
- 51C0513624A77DF700194D5E /* NetNewsWire_MultiplatformApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire_MultiplatformApp.swift; sourceTree = ""; };
+ 51C0513624A77DF700194D5E /* NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire.swift; sourceTree = ""; };
51C0513724A77DF700194D5E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
51C0513824A77DF800194D5E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
51C0513D24A77DF800194D5E /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1613,6 +1853,14 @@
51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; };
51E43961238037C400015C31 /* AddWebFeedFolderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedFolderViewController.swift; sourceTree = ""; };
51E4397F23805EBC00015C31 /* AddComboTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddComboTableViewCell.swift; sourceTree = ""; };
+ 51E4989624A8065700B667CB /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; };
+ 51E4989824A8067000B667CB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
+ 51E498B224A806AA00B667CB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
+ 51E4992524A80AAB00B667CB /* AppAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAssets.swift; sourceTree = ""; };
+ 51E4992824A866F000B667CB /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; };
+ 51E4993924A8708800B667CB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 51E4993B24A8709900B667CB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 51E4995824A873F900B667CB /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; };
51E4DAEC2425F6940091EB5B /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
51E4DB072425F9EB0091EB5B /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; };
51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = ""; };
@@ -1910,6 +2158,19 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 51E4988F24A8061400B667CB /* RSWeb.framework in Frameworks */,
+ 51E4988924A8061400B667CB /* RSDatabase.framework in Frameworks */,
+ 51E4988D24A8061400B667CB /* RSTree.framework in Frameworks */,
+ 51E4987F24A8061400B667CB /* Account.framework in Frameworks */,
+ 51E4988124A8061400B667CB /* Articles.framework in Frameworks */,
+ 51E4988524A8061400B667CB /* OAuthSwift.framework in Frameworks */,
+ 51E4989324A8061400B667CB /* SyncDatabase.framework in Frameworks */,
+ 51E4988724A8061400B667CB /* RSCore.framework in Frameworks */,
+ 51E4988B24A8061400B667CB /* RSParser.framework in Frameworks */,
+ 51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */,
+ 51E4989124A8061400B667CB /* Secrets.framework in Frameworks */,
+ 51E4989924A8067000B667CB /* WebKit.framework in Frameworks */,
+ 51E4988324A8061400B667CB /* ArticlesDatabase.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1917,6 +2178,20 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 51E498AA24A8069300B667CB /* RSWeb.framework in Frameworks */,
+ 51E498A424A8069300B667CB /* RSDatabase.framework in Frameworks */,
+ 51E498A824A8069300B667CB /* RSTree.framework in Frameworks */,
+ 51E4997624A87FFC00B667CB /* Sparkle.framework in Frameworks */,
+ 51E4989A24A8069300B667CB /* Account.framework in Frameworks */,
+ 51E4989C24A8069300B667CB /* Articles.framework in Frameworks */,
+ 51E498A024A8069300B667CB /* OAuthSwift.framework in Frameworks */,
+ 51E498AE24A8069300B667CB /* SyncDatabase.framework in Frameworks */,
+ 51E498A224A8069300B667CB /* RSCore.framework in Frameworks */,
+ 51E498A624A8069300B667CB /* RSParser.framework in Frameworks */,
+ 51E498B124A806A400B667CB /* CloudKit.framework in Frameworks */,
+ 51E498AC24A8069300B667CB /* Secrets.framework in Frameworks */,
+ 51E498B324A806AA00B667CB /* WebKit.framework in Frameworks */,
+ 51E4989E24A8069300B667CB /* ArticlesDatabase.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2269,6 +2544,7 @@
51C0513F24A77DF800194D5E /* Info.plist */,
51C051CE24A7A72100194D5E /* iOS.entitlements */,
51C051CF24A7A72100194D5E /* iOS-dev.entitlements */,
+ 51E4993B24A8709900B667CB /* AppDelegate.swift */,
);
path = iOS;
sourceTree = "";
@@ -2279,6 +2555,7 @@
51C0514624A77DF800194D5E /* Info.plist */,
51C0514724A77DF800194D5E /* macOS.entitlements */,
51C051CD24A7A6DB00194D5E /* macOS-dev.entitlements */,
+ 51E4993924A8708800B667CB /* AppDelegate.swift */,
);
path = macOS;
sourceTree = "";
@@ -2286,8 +2563,11 @@
51C0519524A77E8B00194D5E /* Shared */ = {
isa = PBXGroup;
children = (
- 51C0513624A77DF700194D5E /* NetNewsWire_MultiplatformApp.swift */,
+ 51C0513624A77DF700194D5E /* NetNewsWire.swift */,
51C0513724A77DF700194D5E /* ContentView.swift */,
+ 51E4992524A80AAB00B667CB /* AppAssets.swift */,
+ 51E4992824A866F000B667CB /* AppDefaults.swift */,
+ 51E4995824A873F900B667CB /* ErrorHandler.swift */,
51C0513824A77DF800194D5E /* Assets.xcassets */,
);
path = Shared;
@@ -2451,8 +2731,11 @@
51C452B22265141B00C03939 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 51E4989824A8067000B667CB /* WebKit.framework */,
+ 51E498B224A806AA00B667CB /* WebKit.framework */,
51E4DB072425F9EB0091EB5B /* CloudKit.framework */,
51E4DAEC2425F6940091EB5B /* CloudKit.framework */,
+ 51E4989624A8065700B667CB /* CloudKit.framework */,
51C452B32265141B00C03939 /* WebKit.framework */,
);
name = Frameworks;
@@ -3276,6 +3559,7 @@
51C0513924A77DF800194D5E /* Sources */,
51C0513A24A77DF800194D5E /* Frameworks */,
51C0513B24A77DF800194D5E /* Resources */,
+ 51E4989524A8061400B667CB /* Embed Frameworks */,
);
buildRules = (
);
@@ -3293,6 +3577,8 @@
51C0514024A77DF800194D5E /* Sources */,
51C0514124A77DF800194D5E /* Frameworks */,
51C0514224A77DF800194D5E /* Resources */,
+ 51E498B024A8069300B667CB /* Embed Frameworks */,
+ 51E4994924A872AD00B667CB /* Embed XPC Services */,
);
buildRules = (
);
@@ -3944,7 +4230,12 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 51E4995F24A875F300B667CB /* shared.css in Resources */,
+ 51E4997324A8784300B667CB /* DefaultFeeds.opml in Resources */,
51C0516224A77DF800194D5E /* Assets.xcassets in Resources */,
+ 51E4996024A875F300B667CB /* template.html in Resources */,
+ 51E4995E24A875F300B667CB /* newsfoot.js in Resources */,
+ 51E4995D24A875F300B667CB /* main.js in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3952,7 +4243,12 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 51E4996424A875F400B667CB /* shared.css in Resources */,
+ 51E4997524A8784400B667CB /* DefaultFeeds.opml in Resources */,
51C0516324A77DF800194D5E /* Assets.xcassets in Resources */,
+ 51E4996524A875F400B667CB /* template.html in Resources */,
+ 51E4996324A875F400B667CB /* newsfoot.js in Resources */,
+ 51E4996224A875F400B667CB /* main.js in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4273,8 +4569,65 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 51E4995924A873F900B667CB /* ErrorHandler.swift in Sources */,
+ 51E4992F24A8676400B667CB /* ArticleArray.swift in Sources */,
+ 51E4994424A8713C00B667CB /* RefreshInterval.swift in Sources */,
+ 51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */,
+ 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
+ 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
+ 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */,
+ 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */,
+ 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */,
+ 51E4995C24A875F300B667CB /* ArticleRenderer.swift in Sources */,
+ 51E4992324A8095700B667CB /* URL-Extensions.swift in Sources */,
+ 51E4993624A867E800B667CB /* UserInfoKey.swift in Sources */,
+ 51E4990924A808C500B667CB /* WebFeedIconDownloader.swift in Sources */,
+ 51E498F524A8085D00B667CB /* TodayFeedDelegate.swift in Sources */,
+ 51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */,
+ 51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */,
+ 51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */,
+ 51E4990A24A808C500B667CB /* FeaturedImageDownloader.swift in Sources */,
+ 51E4993824A8680E00B667CB /* Reachability.swift in Sources */,
+ 51E4993224A8676400B667CB /* FetchRequestQueue.swift in Sources */,
+ 51E4991724A8090400B667CB /* ArticleUtilities.swift in Sources */,
+ 51E4991B24A8091000B667CB /* IconImage.swift in Sources */,
+ 51E4995424A8734D00B667CB /* ExtensionPointIdentifer.swift in Sources */,
51C0516024A77DF800194D5E /* ContentView.swift in Sources */,
- 51C0515E24A77DF800194D5E /* NetNewsWire_MultiplatformApp.swift in Sources */,
+ 51E4996924A8760C00B667CB /* ArticleStylesManager.swift in Sources */,
+ 51E498F324A8085D00B667CB /* PseudoFeed.swift in Sources */,
+ 51E4996B24A8762D00B667CB /* ArticleExtractor.swift in Sources */,
+ 51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */,
+ 51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */,
+ 51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */,
+ 51E4994224A8713C00B667CB /* ArticleStatusSyncTimer.swift in Sources */,
+ 51E498F624A8085D00B667CB /* SearchFeedDelegate.swift in Sources */,
+ 51E498F224A8085D00B667CB /* SmartFeedsController.swift in Sources */,
+ 51E4997024A8764C00B667CB /* ActivityManager.swift in Sources */,
+ 51E4990F24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */,
+ 51E4993124A8676400B667CB /* FetchRequestOperation.swift in Sources */,
+ 51E4992624A80AAB00B667CB /* AppAssets.swift in Sources */,
+ 51E4995624A8734D00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */,
+ 51E4992924A866F000B667CB /* AppDefaults.swift in Sources */,
+ 51E4996824A8760C00B667CB /* ArticleStyle.swift in Sources */,
+ 51E4990024A808BB00B667CB /* FaviconGenerator.swift in Sources */,
+ 51E4997124A8764C00B667CB /* ActivityType.swift in Sources */,
+ 51E4991E24A8094300B667CB /* RSImage-AppIcons.swift in Sources */,
+ 51E4991324A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */,
+ 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */,
+ 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */,
+ 51E4995124A8734D00B667CB /* ExtensionPointManager.swift in Sources */,
+ 51E4990C24A808C500B667CB /* AuthorAvatarDownloader.swift in Sources */,
+ 51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */,
+ 51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */,
+ 51E4990224A808BB00B667CB /* ColorHash.swift in Sources */,
+ 51E4991924A8090A00B667CB /* CacheCleaner.swift in Sources */,
+ 51E498F724A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */,
+ 51E4993524A867E800B667CB /* AppNotifications.swift in Sources */,
+ 51C0515E24A77DF800194D5E /* NetNewsWire.swift in Sources */,
+ 51E4993D24A870F800B667CB /* UserNotificationManager.swift in Sources */,
+ 51E4991524A808FF00B667CB /* ArticleStringFormatter.swift in Sources */,
+ 51E4995724A8734D00B667CB /* ExtensionPoint.swift in Sources */,
+ 51E4991124A808DE00B667CB /* SmallIconProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4282,8 +4635,70 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */,
+ 51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */,
+ 51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
+ 51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */,
+ 51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */,
+ 51E4993724A8680E00B667CB /* Reachability.swift in Sources */,
+ 51E4994B24A8734C00B667CB /* SendToMicroBlogCommand.swift in Sources */,
+ 51E4996F24A8764C00B667CB /* ActivityType.swift in Sources */,
+ 51E4994E24A8734C00B667CB /* SendToMarsEditCommand.swift in Sources */,
+ 51E4996624A8760B00B667CB /* ArticleStyle.swift in Sources */,
+ 51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
+ 51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */,
+ 51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */,
+ 51E4994D24A8734C00B667CB /* ExtensionPointIdentifer.swift in Sources */,
+ 51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */,
+ 51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */,
+ 51E498CB24A8085D00B667CB /* TodayFeedDelegate.swift in Sources */,
+ 51E4993324A867E700B667CB /* AppNotifications.swift in Sources */,
+ 51E4990624A808C300B667CB /* ImageDownloader.swift in Sources */,
+ 51E4994F24A8734C00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */,
+ 51E498CA24A8085D00B667CB /* SmartFeedDelegate.swift in Sources */,
+ 51E4990524A808C300B667CB /* FeaturedImageDownloader.swift in Sources */,
+ 51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */,
+ 51E4991A24A8090F00B667CB /* IconImage.swift in Sources */,
+ 51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */,
+ 51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */,
+ 51E4993424A867E700B667CB /* UserInfoKey.swift in Sources */,
+ 51E4994C24A8734C00B667CB /* RedditFeedProvider-Extensions.swift in Sources */,
+ 51E4994124A8713B00B667CB /* RefreshInterval.swift in Sources */,
51C0516124A77DF800194D5E /* ContentView.swift in Sources */,
- 51C0515F24A77DF800194D5E /* NetNewsWire_MultiplatformApp.swift in Sources */,
+ 51E498C924A8085D00B667CB /* PseudoFeed.swift in Sources */,
+ 51E498FC24A808BA00B667CB /* FaviconURLFinder.swift in Sources */,
+ 51E4991C24A8092000B667CB /* NSAttributedString+NetNewsWire.swift in Sources */,
+ 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */,
+ 51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */,
+ 51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */,
+ 51E498CC24A8085D00B667CB /* SearchFeedDelegate.swift in Sources */,
+ 51E498C824A8085D00B667CB /* SmartFeedsController.swift in Sources */,
+ 51E4992C24A8676300B667CB /* ArticleSorter.swift in Sources */,
+ 51E4995024A8734C00B667CB /* ExtensionPoint.swift in Sources */,
+ 51E4990E24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */,
+ 51E498FB24A808BA00B667CB /* FaviconGenerator.swift in Sources */,
+ 51E4996724A8760B00B667CB /* ArticleStylesManager.swift in Sources */,
+ 51E4996E24A8764C00B667CB /* ActivityManager.swift in Sources */,
+ 51E4995A24A873F900B667CB /* ErrorHandler.swift in Sources */,
+ 51E4991F24A8094300B667CB /* RSImage-AppIcons.swift in Sources */,
+ 51E4991224A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */,
+ 51E4993E24A870F900B667CB /* UserNotificationManager.swift in Sources */,
+ 51E4992E24A8676300B667CB /* FetchRequestQueue.swift in Sources */,
+ 51E498CF24A8085D00B667CB /* SmartFeed.swift in Sources */,
+ 51E4990724A808C300B667CB /* AuthorAvatarDownloader.swift in Sources */,
+ 51E4997424A8784400B667CB /* DefaultFeedsImporter.swift in Sources */,
+ 51E4992024A8095000B667CB /* RSImage-Extensions.swift in Sources */,
+ 51E498FE24A808BA00B667CB /* FaviconDownloader.swift in Sources */,
+ 51E498FD24A808BA00B667CB /* ColorHash.swift in Sources */,
+ 51E4992A24A866F000B667CB /* AppDefaults.swift in Sources */,
+ 51E4991824A8090A00B667CB /* CacheCleaner.swift in Sources */,
+ 51E498CD24A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */,
+ 51E4996124A875F400B667CB /* ArticleRenderer.swift in Sources */,
+ 51C0515F24A77DF800194D5E /* NetNewsWire.swift in Sources */,
+ 51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */,
+ 51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */,
+ 51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */,
+ 51E4991024A808DE00B667CB /* SmallIconProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Shared/Favicons/SingleFaviconDownloader.swift b/Shared/Favicons/SingleFaviconDownloader.swift
index 9ef2b61df..1b82802f6 100644
--- a/Shared/Favicons/SingleFaviconDownloader.swift
+++ b/Shared/Favicons/SingleFaviconDownloader.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import os.log
import RSCore
import RSWeb
@@ -28,6 +29,8 @@ final class SingleFaviconDownloader {
var iconImage: IconImage?
let homePageURL: String?
+ private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SingleFaviconDownloader")
+
private var lastDownloadAttemptDate: Date
private var diskStatus = DiskStatus.unknown
private var diskCache: BinaryDiskCache
@@ -145,7 +148,7 @@ private extension SingleFaviconDownloader {
}
if let error = error {
- appDelegate.logMessage("Error downloading favicon at \(url): \(error)", type: .warning)
+ os_log(.info, log: self.log, "Error downloading image at %@: %@.", url.absoluteString, error.localizedDescription)
}
completion(nil)
diff --git a/Shared/Images/ImageDownloader.swift b/Shared/Images/ImageDownloader.swift
index 7ea33c0ac..9a549d3ef 100644
--- a/Shared/Images/ImageDownloader.swift
+++ b/Shared/Images/ImageDownloader.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import os.log
import RSCore
import RSWeb
@@ -17,6 +18,8 @@ extension Notification.Name {
final class ImageDownloader {
+ private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ImageDownloader")
+
private let folder: String
private var diskCache: BinaryDiskCache
private let queue: DispatchQueue
@@ -112,7 +115,7 @@ private extension ImageDownloader {
self.badURLs.insert(url)
}
if let error = error {
- appDelegate.logMessage("Error downloading image at \(url): \(error)", type: .warning)
+ os_log(.info, log: self.log, "Error downloading image at %@: %@.", url, error.localizedDescription)
}
completion(nil)
diff --git a/Shared/Importers/DefaultFeedsImporter.swift b/Shared/Importers/DefaultFeedsImporter.swift
index b86d39e36..8ed67d3b9 100644
--- a/Shared/Importers/DefaultFeedsImporter.swift
+++ b/Shared/Importers/DefaultFeedsImporter.swift
@@ -13,9 +13,9 @@ import RSCore
struct DefaultFeedsImporter {
static func importDefaultFeeds(account: Account) {
- appDelegate.logDebugMessage("Importing default feeds.")
let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")!
AccountManager.shared.defaultAccount.importOPML(defaultFeedsURL) { result in }
}
+
}
diff --git a/Shared/SmartFeeds/PseudoFeed.swift b/Shared/SmartFeeds/PseudoFeed.swift
index 5c0dc0660..2fd0d9d6c 100644
--- a/Shared/SmartFeeds/PseudoFeed.swift
+++ b/Shared/SmartFeeds/PseudoFeed.swift
@@ -17,16 +17,6 @@ protocol PseudoFeed: class, Feed, SmallIconProvider, PasteboardWriterOwner {
}
-private var smartFeedIcon: RSImage = {
- return RSImage(named: NSImage.smartBadgeTemplateName)!
-}()
-
-extension PseudoFeed {
-
- var smallIcon: RSImage? {
- return smartFeedIcon
- }
-}
#else
import UIKit
@@ -38,14 +28,10 @@ protocol PseudoFeed: class, Feed, SmallIconProvider {
}
-private var smartFeedIcon: UIImage = {
- return AppAssets.smartFeedImage
-}()
+#endif
extension PseudoFeed {
- var smallIcon: UIImage? {
- return smartFeedIcon
+ var smallIcon: RSImage? {
+ return AppAssets.smartFeedImage
}
}
-
-#endif