Merge branch 'main' into ios-ui-settings-localised
# Conflicts: # NetNewsWire.xcodeproj/project.pbxproj # Shared/Timer/AccountRefreshTimer.swift # iOS/Account/ReaderAPIAccountViewController.swift
@@ -653,6 +653,22 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
fetchUnreadCounts(for: webFeeds, completion: completion)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(limit: Int?, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
return try fetchUnreadArticlesBetween(forContainer: self, limit: limit, before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(folder: Folder, limit: Int?, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
return try fetchUnreadArticlesBetween(forContainer: folder, limit: limit, before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(webFeeds: Set<WebFeed>, limit: Int?, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
return try fetchUnreadArticlesBetween(feeds: webFeeds, limit: limit, before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchArticlesBetween(articleIDs: Set<String>, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
return try database.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> {
|
||||
switch fetchType {
|
||||
case .starred(let limit):
|
||||
@@ -1150,6 +1166,17 @@ private extension Account {
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(forContainer container: Container, limit: Int?, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
let feeds = container.flattenedWebFeeds()
|
||||
let articles = try database.fetchUnreadArticlesBetween(feeds.webFeedIDs(), limit, before, after)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(feeds: Set<WebFeed>, limit: Int?, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
let articles = try database.fetchUnreadArticlesBetween(feeds.webFeedIDs(), limit, before, after)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
let webFeeds = container.flattenedWebFeeds()
|
||||
database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in
|
||||
|
||||
@@ -11,6 +11,7 @@ import RSCore
|
||||
import RSWeb
|
||||
import Articles
|
||||
import ArticlesDatabase
|
||||
import RSDatabase
|
||||
|
||||
// Main thread only.
|
||||
|
||||
@@ -325,6 +326,26 @@ public final class AccountManager: UnreadCountProvider {
|
||||
return false
|
||||
}
|
||||
|
||||
public func anyLocalOriCloudAccountHasAtLeastOneTwitterFeed() -> Bool {
|
||||
// We removed our Twitter code, and the ability to read feeds from Twitter,
|
||||
// when Twitter announced the end of the free tier for the Twitter API.
|
||||
// We are cheering on Twitter’s increasing irrelevancy.
|
||||
|
||||
for account in accounts {
|
||||
if account.type == .cloudKit || account.type == .onMyMac {
|
||||
for webfeed in account.flattenedWebFeeds() {
|
||||
if let components = URLComponents(string: webfeed.url), let host = components.host {
|
||||
if host == "twitter.com" { // Allow, for instance, blog.twitter.com, which might have an actual RSS feed
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Fetching Articles
|
||||
|
||||
// These fetch articles from active accounts and return a merged Set<Article>.
|
||||
@@ -340,33 +361,53 @@ public final class AccountManager: UnreadCountProvider {
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) {
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
guard activeAccounts.count > 0 else {
|
||||
completion(.success(Set<Article>()))
|
||||
return
|
||||
}
|
||||
|
||||
var allFetchedArticles = Set<Article>()
|
||||
var databaseError: DatabaseError?
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for account in activeAccounts {
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
account.fetchArticlesAsync(fetchType) { (articleSetResult) in
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
allFetchedArticles.formUnion(articles)
|
||||
case .failure(let error):
|
||||
databaseError = error
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let databaseError {
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
else {
|
||||
completion(.success(allFetchedArticles))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(limit: Int? = nil, before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
var allFetchedArticles = Set<Article>()
|
||||
let numberOfAccounts = activeAccounts.count
|
||||
var accountsReporting = 0
|
||||
|
||||
guard numberOfAccounts > 0 else {
|
||||
completion(.success(allFetchedArticles))
|
||||
return
|
||||
}
|
||||
|
||||
var articles = Set<Article>()
|
||||
for account in activeAccounts {
|
||||
account.fetchArticlesAsync(fetchType) { (articleSetResult) in
|
||||
accountsReporting += 1
|
||||
|
||||
switch articleSetResult {
|
||||
case .success(let articles):
|
||||
allFetchedArticles.formUnion(articles)
|
||||
if accountsReporting == numberOfAccounts {
|
||||
completion(.success(allFetchedArticles))
|
||||
}
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
return
|
||||
}
|
||||
}
|
||||
articles.formUnion(try account.fetchUnreadArticlesBetween(limit: limit, before: before, after: after))
|
||||
}
|
||||
return articles
|
||||
}
|
||||
|
||||
// MARK: - Fetching Article Counts
|
||||
|
||||
@@ -15,6 +15,7 @@ public protocol ArticleFetcher {
|
||||
func fetchArticles() throws -> Set<Article>
|
||||
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
|
||||
func fetchUnreadArticles() throws -> Set<Article>
|
||||
func fetchUnreadArticlesBetween(before: Date?, after: Date?) throws -> Set<Article>
|
||||
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
|
||||
}
|
||||
|
||||
@@ -37,6 +38,10 @@ extension WebFeed: ArticleFetcher {
|
||||
return try fetchArticles().unreadArticles()
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
return try account?.fetchUnreadArticlesBetween(webFeeds: [self], limit: nil, before: before, after: after) ?? Set<Article>()
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
guard let account = account else {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
@@ -81,6 +86,14 @@ extension Folder: ArticleFetcher {
|
||||
return try account.fetchArticles(.folder(self, true))
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
guard let account = account else {
|
||||
assertionFailure("Expected folder.account, but got nil.")
|
||||
return Set<Article>()
|
||||
}
|
||||
return try account.fetchUnreadArticlesBetween(folder: self, limit: nil, before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
guard let account = account else {
|
||||
assertionFailure("Expected folder.account, but got nil.")
|
||||
|
||||
@@ -136,7 +136,7 @@ private extension CloudKitArticlesZoneDelegate {
|
||||
account?.markAsStarred(updateableStarredArticleIDs) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
errorOccurred = true
|
||||
self.logger.error("Error occurred while stroing starred records: \(databaseError.localizedDescription, privacy: .public)")
|
||||
self.logger.error("Error occurred while storing starred records: \(databaseError.localizedDescription, privacy: .public)")
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
//
|
||||
// TwitterEntities.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol TwitterEntity {
|
||||
var indices: [Int]? { get }
|
||||
func renderAsHTML() -> String
|
||||
}
|
||||
|
||||
extension TwitterEntity {
|
||||
|
||||
var startIndex: Int {
|
||||
if let indices = indices, indices.count > 0 {
|
||||
return indices[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var endIndex: Int {
|
||||
if let indices = indices, indices.count > 1 {
|
||||
return indices[1]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TwitterEntities: Codable {
|
||||
|
||||
let hashtags: [TwitterHashtag]?
|
||||
let urls: [TwitterURL]?
|
||||
let userMentions: [TwitterMention]?
|
||||
let symbols: [TwitterSymbol]?
|
||||
let media: [TwitterMedia]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case hashtags = "hashtags"
|
||||
case urls = "urls"
|
||||
case userMentions = "user_mentions"
|
||||
case symbols = "symbols"
|
||||
case media = "media"
|
||||
}
|
||||
|
||||
func combineAndSort() -> [TwitterEntity] {
|
||||
var entities = [TwitterEntity]()
|
||||
if let hashtags = hashtags {
|
||||
entities.append(contentsOf: hashtags)
|
||||
}
|
||||
if let urls = urls {
|
||||
entities.append(contentsOf: urls)
|
||||
}
|
||||
if let userMentions = userMentions {
|
||||
entities.append(contentsOf: userMentions)
|
||||
}
|
||||
if let symbols = symbols {
|
||||
entities.append(contentsOf: symbols)
|
||||
}
|
||||
if let media = media {
|
||||
entities.append(contentsOf: media)
|
||||
}
|
||||
return entities.sorted(by: { $0.startIndex < $1.startIndex })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// TwitterExtendedEntities.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterExtendedEntities: Codable {
|
||||
|
||||
let medias: [TwitterExtendedMedia]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case medias = "media"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
var html = String()
|
||||
if let medias = medias {
|
||||
for media in medias {
|
||||
html += media.renderAsHTML()
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
//
|
||||
// TwitterExtendedMedia.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterExtendedMedia: Codable {
|
||||
|
||||
let idStr: String?
|
||||
let indices: [Int]?
|
||||
let mediaURL: String?
|
||||
let httpsMediaURL: String?
|
||||
let url: String?
|
||||
let displayURL: String?
|
||||
let type: String?
|
||||
let video: TwitterVideo?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case idStr = "idStr"
|
||||
case indices = "indices"
|
||||
case mediaURL = "media_url"
|
||||
case httpsMediaURL = "media_url_https"
|
||||
case url = "url"
|
||||
case displayURL = "display_url"
|
||||
case type = "type"
|
||||
case video = "video_info"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
var html = String()
|
||||
|
||||
switch type {
|
||||
case "photo":
|
||||
html += renderPhotoAsHTML()
|
||||
case "video":
|
||||
html += renderVideoAsHTML()
|
||||
case "animated_gif":
|
||||
html += renderAnimatedGIFAsHTML()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension TwitterExtendedMedia {
|
||||
|
||||
func renderPhotoAsHTML() -> String {
|
||||
if let httpsMediaURL = httpsMediaURL {
|
||||
return "<figure><img src=\"\(httpsMediaURL)\"></figure>"
|
||||
}
|
||||
if let mediaURL = mediaURL {
|
||||
return "<figure><img src=\"\(mediaURL)\"></figure>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func renderVideoAsHTML() -> String {
|
||||
guard let bestVariantURL = findBestVariant()?.url else { return "" }
|
||||
|
||||
var html = "<video "
|
||||
|
||||
if let httpsMediaURL = httpsMediaURL {
|
||||
html += "poster=\"\(httpsMediaURL)\" "
|
||||
} else if let mediaURL = mediaURL {
|
||||
html += "poster=\"\(mediaURL)\" "
|
||||
}
|
||||
|
||||
html += "src=\"\(bestVariantURL)\"></video>"
|
||||
return html
|
||||
}
|
||||
|
||||
func renderAnimatedGIFAsHTML() -> String {
|
||||
guard let bestVariantURL = findBestVariant()?.url else { return "" }
|
||||
|
||||
var html = "<video class=\"nnwAnimatedGIF\" "
|
||||
|
||||
if let httpsMediaURL = httpsMediaURL {
|
||||
html += "poster=\"\(httpsMediaURL)\" "
|
||||
} else if let mediaURL = mediaURL {
|
||||
html += "poster=\"\(mediaURL)\" "
|
||||
}
|
||||
|
||||
html += "src=\"\(bestVariantURL)\" autoplay muted loop></video>"
|
||||
return html
|
||||
}
|
||||
|
||||
func findBestVariant() -> TwitterVideo.Variant? {
|
||||
var best: TwitterVideo.Variant? = nil
|
||||
if let variants = video?.variants {
|
||||
for variant in variants {
|
||||
if let currentBest = best {
|
||||
if variant.bitrate ?? 0 > currentBest.bitrate ?? 0 {
|
||||
best = variant
|
||||
}
|
||||
} else {
|
||||
best = variant
|
||||
}
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
//
|
||||
// TwitterFeedProvider.swift
|
||||
// FeedProvider
|
||||
//
|
||||
// Created by Maurice Parker on 4/7/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Secrets
|
||||
import OAuthSwift
|
||||
import RSParser
|
||||
import RSWeb
|
||||
|
||||
public enum TwitterFeedProviderError: LocalizedError {
|
||||
case rateLimitExceeded
|
||||
case screenNameNotFound
|
||||
case unknown
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .rateLimitExceeded:
|
||||
return NSLocalizedString("Twitter API rate limit has been exceeded. Please wait a short time and try again.", comment: "Rate Limit")
|
||||
case .screenNameNotFound:
|
||||
return NSLocalizedString("Unable to determine screen name.", comment: "Screen name")
|
||||
case .unknown:
|
||||
return NSLocalizedString("An unknown Twitter Feed Provider error has occurred.", comment: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TwitterFeedType: Int {
|
||||
case homeTimeline = 0
|
||||
case mentions = 1
|
||||
case screenName = 2
|
||||
case search = 3
|
||||
}
|
||||
|
||||
public final class TwitterFeedProvider: FeedProvider {
|
||||
|
||||
private static let homeURL = "https://www.twitter.com"
|
||||
private static let iconURL = "https://abs.twimg.com/favicons/twitter.ico"
|
||||
private static let server = "api.twitter.com"
|
||||
private static let apiBase = "https://api.twitter.com/1.1/"
|
||||
private static let userAgentHeaders = UserAgent.headers() as! [String: String]
|
||||
private static let dateFormat = "EEE MMM dd HH:mm:ss Z yyyy"
|
||||
|
||||
private static let userPaths = ["/", "/home", "/notifications"]
|
||||
private static let reservedPaths = ["/search", "/explore", "/messages", "/i", "/compose", "/notifications/mentions"]
|
||||
|
||||
private var parsingQueue = DispatchQueue(label: "TwitterFeedProvider parse queue")
|
||||
|
||||
public var screenName: String
|
||||
|
||||
private var oauthToken: String
|
||||
private var oauthTokenSecret: String
|
||||
|
||||
private var client: OAuthSwiftClient
|
||||
|
||||
private var rateLimitRemaining: Int?
|
||||
private var rateLimitReset: Date?
|
||||
|
||||
public init?(tokenSuccess: OAuthSwift.TokenSuccess) {
|
||||
guard let screenName = tokenSuccess.parameters["screen_name"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.screenName = screenName
|
||||
self.oauthToken = tokenSuccess.credential.oauthToken
|
||||
self.oauthTokenSecret = tokenSuccess.credential.oauthTokenSecret
|
||||
|
||||
let tokenCredentials = Credentials(type: .oauthAccessToken, username: screenName, secret: oauthToken)
|
||||
try? CredentialsManager.storeCredentials(tokenCredentials, server: Self.server)
|
||||
|
||||
let tokenSecretCredentials = Credentials(type: .oauthAccessTokenSecret, username: screenName, secret: oauthTokenSecret)
|
||||
try? CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server)
|
||||
|
||||
client = OAuthSwiftClient(consumerKey: SecretsManager.provider.twitterConsumerKey,
|
||||
consumerSecret: SecretsManager.provider.twitterConsumerSecret,
|
||||
oauthToken: oauthToken,
|
||||
oauthTokenSecret: oauthTokenSecret,
|
||||
version: .oauth1)
|
||||
}
|
||||
|
||||
public init?(screenName: String) {
|
||||
self.screenName = screenName
|
||||
|
||||
guard let tokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessToken, server: Self.server, username: screenName),
|
||||
let tokenSecretCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessTokenSecret, server: Self.server, username: screenName) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.oauthToken = tokenCredentials.secret
|
||||
self.oauthTokenSecret = tokenSecretCredentials.secret
|
||||
|
||||
client = OAuthSwiftClient(consumerKey: SecretsManager.provider.twitterConsumerKey,
|
||||
consumerSecret: SecretsManager.provider.twitterConsumerSecret,
|
||||
oauthToken: oauthToken,
|
||||
oauthTokenSecret: oauthTokenSecret,
|
||||
version: .oauth1)
|
||||
}
|
||||
|
||||
public func ability(_ urlComponents: URLComponents) -> FeedProviderAbility {
|
||||
guard urlComponents.host?.hasSuffix("twitter.com") ?? false else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if let username = urlComponents.user {
|
||||
if username == screenName {
|
||||
return .owner
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
return .available
|
||||
}
|
||||
|
||||
public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
if let screenName = deriveScreenName(urlComponents) {
|
||||
retrieveUser(screenName: screenName) { result in
|
||||
switch result {
|
||||
case .success(let user):
|
||||
if let avatarURL = user.avatarURL {
|
||||
completion(.success(avatarURL))
|
||||
} else {
|
||||
completion(.failure(TwitterFeedProviderError.screenNameNotFound))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.success(Self.iconURL))
|
||||
}
|
||||
}
|
||||
|
||||
public func metaData(_ urlComponents: URLComponents, completion: @escaping (Result<FeedProviderFeedMetaData, Error>) -> Void) {
|
||||
switch urlComponents.path {
|
||||
|
||||
case "", "/", "/home":
|
||||
let name = NSLocalizedString("Twitter Timeline", comment: "Twitter Timeline")
|
||||
completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL)))
|
||||
|
||||
case "/notifications/mentions":
|
||||
let name = NSLocalizedString("Twitter Mentions", comment: "Twitter Mentions")
|
||||
completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL)))
|
||||
|
||||
case "/search":
|
||||
if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value {
|
||||
let localized = NSLocalizedString("Twitter Search: %@", comment: "Twitter Search")
|
||||
let name = NSString.localizedStringWithFormat(localized as NSString, query) as String
|
||||
completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL)))
|
||||
} else {
|
||||
let name = NSLocalizedString("Twitter Search", comment: "Twitter Search")
|
||||
completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL)))
|
||||
}
|
||||
|
||||
default:
|
||||
if let hashtag = deriveHashtag(urlComponents) {
|
||||
completion(.success(FeedProviderFeedMetaData(name: "#\(hashtag)", homePageURL: Self.homeURL)))
|
||||
} else if let listID = deriveListID(urlComponents) {
|
||||
retrieveList(listID: listID) { result in
|
||||
switch result {
|
||||
case .success(let list):
|
||||
if let userName = list.name {
|
||||
var urlComponents = URLComponents(string: Self.homeURL)
|
||||
urlComponents?.path = "/i/lists/\(listID)"
|
||||
completion(.success(FeedProviderFeedMetaData(name: userName, homePageURL: urlComponents?.url?.absoluteString)))
|
||||
} else {
|
||||
completion(.failure(TwitterFeedProviderError.screenNameNotFound))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else if let screenName = deriveScreenName(urlComponents) {
|
||||
retrieveUser(screenName: screenName) { result in
|
||||
switch result {
|
||||
case .success(let user):
|
||||
if let userName = user.name {
|
||||
var urlComponents = URLComponents(string: Self.homeURL)
|
||||
urlComponents?.path = "/\(screenName)"
|
||||
completion(.success(FeedProviderFeedMetaData(name: userName, homePageURL: urlComponents?.url?.absoluteString)))
|
||||
} else {
|
||||
completion(.failure(TwitterFeedProviderError.screenNameNotFound))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.failure(TwitterFeedProviderError.unknown))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func refresh(_ webFeed: WebFeed, completion: @escaping (Result<Set<ParsedItem>, Error>) -> Void) {
|
||||
guard let urlComponents = URLComponents(string: webFeed.url) else {
|
||||
completion(.failure(TwitterFeedProviderError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
let api: String
|
||||
var parameters = [String: Any]()
|
||||
var isSearch = false
|
||||
|
||||
parameters["count"] = 20
|
||||
|
||||
switch urlComponents.path {
|
||||
case "", "/", "/home":
|
||||
parameters["count"] = 100
|
||||
if let sinceToken = webFeed.sinceToken, let sinceID = Int(sinceToken) {
|
||||
parameters["since_id"] = sinceID
|
||||
}
|
||||
api = "statuses/home_timeline.json"
|
||||
case "/notifications/mentions":
|
||||
api = "statuses/mentions_timeline.json"
|
||||
case "/search":
|
||||
api = "search/tweets.json"
|
||||
if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value {
|
||||
parameters["q"] = query
|
||||
}
|
||||
isSearch = true
|
||||
default:
|
||||
if let hashtag = deriveHashtag(urlComponents) {
|
||||
api = "search/tweets.json"
|
||||
parameters["q"] = "#\(hashtag)"
|
||||
isSearch = true
|
||||
} else if let listID = deriveListID(urlComponents) {
|
||||
api = "lists/statuses.json"
|
||||
parameters["list_id"] = listID
|
||||
} else if let screenName = deriveScreenName(urlComponents) {
|
||||
api = "statuses/user_timeline.json"
|
||||
parameters["exclude_replies"] = true
|
||||
parameters["screen_name"] = screenName
|
||||
} else {
|
||||
completion(.failure(TwitterFeedProviderError.unknown))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
retrieveTweets(api: api, parameters: parameters, isSearch: isSearch) { result in
|
||||
switch result {
|
||||
case .success(let statuses):
|
||||
if let sinceID = statuses.first?.idStr {
|
||||
webFeed.sinceToken = sinceID
|
||||
}
|
||||
self.parsingQueue.async {
|
||||
let parsedItems = self.makeParsedItems(webFeed.url, statuses)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(parsedItems))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func buildURL(_ type: TwitterFeedType, username: String?, screenName: String?, searchField: String?) -> URL? {
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
components.host = "twitter.com"
|
||||
|
||||
switch type {
|
||||
case .homeTimeline:
|
||||
guard let username = username else {
|
||||
return nil
|
||||
}
|
||||
components.user = username
|
||||
case .mentions:
|
||||
guard let username = username else {
|
||||
return nil
|
||||
}
|
||||
components.user = username
|
||||
components.path = "/notifications/mentions"
|
||||
case .screenName:
|
||||
guard let screenName = screenName else {
|
||||
return nil
|
||||
}
|
||||
components.path = "/\(screenName)"
|
||||
case .search:
|
||||
guard let searchField = searchField else {
|
||||
return nil
|
||||
}
|
||||
components.path = "/search"
|
||||
components.queryItems = [URLQueryItem(name: "q", value: searchField)]
|
||||
}
|
||||
|
||||
return components.url
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: OAuth1SwiftProvider
|
||||
|
||||
extension TwitterFeedProvider: OAuth1SwiftProvider {
|
||||
|
||||
public static var callbackURL: URL {
|
||||
return URL(string: "netnewswire://")!
|
||||
}
|
||||
|
||||
public static var oauth1Swift: OAuth1Swift {
|
||||
return OAuth1Swift(
|
||||
consumerKey: SecretsManager.provider.twitterConsumerKey,
|
||||
consumerSecret: SecretsManager.provider.twitterConsumerSecret,
|
||||
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
|
||||
authorizeUrl: "https://api.twitter.com/oauth/authorize",
|
||||
accessTokenUrl: "https://api.twitter.com/oauth/access_token"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension TwitterFeedProvider {
|
||||
|
||||
func deriveHashtag(_ urlComponents: URLComponents) -> String? {
|
||||
let path = urlComponents.path
|
||||
if path.starts(with: "/hashtag/"), let startIndex = path.index(path.startIndex, offsetBy: 9, limitedBy: path.endIndex), startIndex < path.endIndex {
|
||||
return String(path[startIndex..<path.endIndex])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deriveScreenName(_ urlComponents: URLComponents) -> String? {
|
||||
let path = urlComponents.path
|
||||
guard !Self.reservedPaths.contains(path) else { return nil }
|
||||
|
||||
if path.isEmpty || Self.userPaths.contains(path) {
|
||||
return screenName
|
||||
} else {
|
||||
return String(path.suffix(from: path.index(path.startIndex, offsetBy: 1)))
|
||||
}
|
||||
}
|
||||
|
||||
func deriveListID(_ urlComponents: URLComponents) -> String? {
|
||||
let path = urlComponents.path
|
||||
guard path.starts(with: "/i/lists/") else { return nil }
|
||||
return String(path.suffix(from: path.index(path.startIndex, offsetBy: 9)))
|
||||
}
|
||||
|
||||
func retrieveUser(screenName: String, completion: @escaping (Result<TwitterUser, Error>) -> Void) {
|
||||
let url = "\(Self.apiBase)users/show.json"
|
||||
let parameters = ["screen_name": screenName]
|
||||
|
||||
client.get(url, parameters: parameters, headers: Self.userAgentHeaders) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
let user = try decoder.decode(TwitterUser.self, from: response.data)
|
||||
completion(.success(user))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveList(listID: String, completion: @escaping (Result<TwitterList, Error>) -> Void) {
|
||||
let url = "\(Self.apiBase)lists/show.json"
|
||||
let parameters = ["list_id": listID]
|
||||
|
||||
client.get(url, parameters: parameters, headers: Self.userAgentHeaders) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
let collection = try decoder.decode(TwitterList.self, from: response.data)
|
||||
completion(.success(collection))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveTweets(api: String, parameters: [String: Any], isSearch: Bool, completion: @escaping (Result<[TwitterStatus], Error>) -> Void) {
|
||||
let url = "\(Self.apiBase)\(api)"
|
||||
var expandedParameters = parameters
|
||||
expandedParameters["tweet_mode"] = "extended"
|
||||
|
||||
if let remaining = rateLimitRemaining, let reset = rateLimitReset, remaining < 1 && reset > Date() {
|
||||
completion(.failure(TwitterFeedProviderError.rateLimitExceeded))
|
||||
return
|
||||
}
|
||||
|
||||
client.get(url, parameters: expandedParameters, headers: Self.userAgentHeaders) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale.init(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = Self.dateFormat
|
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
if let remaining = response.response.value(forHTTPHeaderField: "x-rate-limit-remaining") {
|
||||
self.rateLimitRemaining = Int(remaining) ?? 0
|
||||
}
|
||||
if let reset = response.response.value(forHTTPHeaderField: "x-rate-limit-reset") {
|
||||
self.rateLimitReset = Date(timeIntervalSince1970: Double(reset) ?? 0)
|
||||
}
|
||||
|
||||
self.parsingQueue.async {
|
||||
do {
|
||||
let tweets: [TwitterStatus]
|
||||
if isSearch {
|
||||
let searchResult = try decoder.decode(TwitterSearchResult.self, from: response.data)
|
||||
if let statuses = searchResult.statuses {
|
||||
tweets = statuses
|
||||
} else {
|
||||
tweets = [TwitterStatus]()
|
||||
}
|
||||
} else {
|
||||
tweets = try decoder.decode([TwitterStatus].self, from: response.data)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(tweets))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
if error.errorCode == -11 {
|
||||
// Eat these errors. They are old or invalid URL requests.
|
||||
completion(.success([TwitterStatus]()))
|
||||
} else {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeParsedItems(_ webFeedURL: String, _ statuses: [TwitterStatus]) -> Set<ParsedItem> {
|
||||
var parsedItems = Set<ParsedItem>()
|
||||
|
||||
for status in statuses {
|
||||
guard let idStr = status.idStr, let statusURL = status.url else { continue }
|
||||
|
||||
let parsedItem = ParsedItem(syncServiceID: nil,
|
||||
uniqueID: idStr,
|
||||
feedURL: webFeedURL,
|
||||
url: statusURL,
|
||||
externalURL: nil,
|
||||
title: nil,
|
||||
language: nil,
|
||||
contentHTML: status.renderAsHTML(),
|
||||
contentText: status.renderAsText(),
|
||||
summary: nil,
|
||||
imageURL: nil,
|
||||
bannerImageURL: nil,
|
||||
datePublished: status.createdAt,
|
||||
dateModified: nil,
|
||||
authors: makeParsedAuthors(status.user),
|
||||
tags: nil,
|
||||
attachments: nil)
|
||||
parsedItems.insert(parsedItem)
|
||||
}
|
||||
|
||||
return parsedItems
|
||||
}
|
||||
|
||||
func makeUserURL(_ screenName: String) -> String {
|
||||
return "https://twitter.com/\(screenName)"
|
||||
}
|
||||
|
||||
func makeParsedAuthors(_ user: TwitterUser?) -> Set<ParsedAuthor>? {
|
||||
guard let user = user else { return nil }
|
||||
return Set([ParsedAuthor(name: user.name, url: user.url, avatarURL: user.avatarURL, emailAddress: nil)])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// TwitterHashtag.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterHashtag: Codable, TwitterEntity {
|
||||
|
||||
let text: String?
|
||||
let indices: [Int]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
case indices = "indices"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
var html = String()
|
||||
if let text = text {
|
||||
html += "<a href=\"https://twitter.com/search?q=%23\(text)\">#\(text)</a>"
|
||||
}
|
||||
return html
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// TwitterList.swift
|
||||
//
|
||||
//
|
||||
// Created by Maurice Parker on 8/14/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterList: Codable {
|
||||
|
||||
let name: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name = "name"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// TwitterMedia.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/20/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterMedia: Codable, TwitterEntity {
|
||||
|
||||
let indices: [Int]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case indices = "indices"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
return String()
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// TwitterMention.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterMention: Codable, TwitterEntity {
|
||||
|
||||
let name: String?
|
||||
let indices: [Int]?
|
||||
let screenName: String?
|
||||
let idStr: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name = "url"
|
||||
case indices = "indices"
|
||||
case screenName = "screen_name"
|
||||
case idStr = "idStr"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
var html = String()
|
||||
if let screenName = screenName {
|
||||
html += "<a href=\"https://twitter.com/\(screenName)\">@\(screenName)</a>"
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// TwitterSearchResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterSearchResult: Codable {
|
||||
|
||||
let statuses: [TwitterStatus]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case statuses = "statuses"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
//
|
||||
// TwitterStatus.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/16/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class TwitterStatus: Codable {
|
||||
|
||||
let createdAt: Date?
|
||||
let idStr: String?
|
||||
let fullText: String?
|
||||
let displayTextRange: [Int]?
|
||||
let user: TwitterUser?
|
||||
let truncated: Bool?
|
||||
let retweeted: Bool?
|
||||
let retweetedStatus: TwitterStatus?
|
||||
let quotedStatus: TwitterStatus?
|
||||
let entities: TwitterEntities?
|
||||
let extendedEntities: TwitterExtendedEntities?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case createdAt = "created_at"
|
||||
case idStr = "id_str"
|
||||
case fullText = "full_text"
|
||||
case displayTextRange = "display_text_range"
|
||||
case user = "user"
|
||||
case truncated = "truncated"
|
||||
case retweeted = "retweeted"
|
||||
case retweetedStatus = "retweeted_status"
|
||||
case quotedStatus = "quoted_status"
|
||||
case entities = "entities"
|
||||
case extendedEntities = "extended_entities"
|
||||
}
|
||||
|
||||
var url: String? {
|
||||
guard let userURL = user?.url, let idStr = idStr else { return nil }
|
||||
return "\(userURL)/status/\(idStr)"
|
||||
}
|
||||
|
||||
func renderAsText() -> String? {
|
||||
let statusToRender = retweetedStatus != nil ? retweetedStatus! : self
|
||||
return statusToRender.displayText
|
||||
}
|
||||
|
||||
func renderAsHTML(topLevel: Bool = true) -> String {
|
||||
if let retweetedStatus = retweetedStatus {
|
||||
return renderAsRetweetHTML(retweetedStatus)
|
||||
}
|
||||
if let quotedStatus = quotedStatus {
|
||||
return renderAsQuoteHTML(quotedStatus, topLevel: topLevel)
|
||||
}
|
||||
return renderAsOriginalHTML(topLevel: topLevel)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension TwitterStatus {
|
||||
|
||||
var displayText: String? {
|
||||
if let text = fullText, let displayRange = displayTextRange, displayRange.count > 1,
|
||||
let startIndex = text.index(text.startIndex, offsetBy: displayRange[0], limitedBy: text.endIndex),
|
||||
let endIndex = text.index(text.startIndex, offsetBy: displayRange[1], limitedBy: text.endIndex) {
|
||||
return String(text[startIndex..<endIndex])
|
||||
} else {
|
||||
return fullText
|
||||
}
|
||||
}
|
||||
|
||||
var displayHTML: String? {
|
||||
if let text = fullText, let displayRange = displayTextRange, displayRange.count > 1, let entities = entities?.combineAndSort() {
|
||||
|
||||
let displayStartIndex = text.index(text.startIndex, offsetBy: displayRange[0], limitedBy: text.endIndex) ?? text.startIndex
|
||||
let displayEndIndex = text.index(text.startIndex, offsetBy: displayRange[1], limitedBy: text.endIndex) ?? text.endIndex
|
||||
|
||||
var html = String()
|
||||
var prevIndex = displayStartIndex
|
||||
var unicodeScalarOffset = 0
|
||||
|
||||
for entity in entities {
|
||||
|
||||
// The twitter indices are messed up by characters with more than one scalar, we are going to adjust for that here.
|
||||
let endIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
|
||||
if prevIndex < endIndex {
|
||||
let characters = String(text[prevIndex..<endIndex])
|
||||
for character in characters {
|
||||
unicodeScalarOffset += character.unicodeScalars.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
let offsetStartIndex = unicodeScalarOffset < entity.startIndex ? entity.startIndex - unicodeScalarOffset : entity.startIndex
|
||||
let offsetEndIndex = unicodeScalarOffset < entity.endIndex ? entity.endIndex - unicodeScalarOffset : entity.endIndex
|
||||
|
||||
let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex
|
||||
let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex
|
||||
|
||||
if prevIndex < entityStartIndex {
|
||||
html += String(text[prevIndex..<entityStartIndex]).replacingOccurrences(of: "\n", with: "<br>")
|
||||
}
|
||||
|
||||
// We drop off any URL which is just pointing to the quoted status. It is redundant.
|
||||
if let twitterURL = entity as? TwitterURL, let expandedURL = twitterURL.expandedURL, let quotedURL = quotedStatus?.url {
|
||||
if expandedURL.caseInsensitiveCompare(quotedURL) != .orderedSame {
|
||||
html += entity.renderAsHTML()
|
||||
}
|
||||
} else {
|
||||
html += entity.renderAsHTML()
|
||||
}
|
||||
|
||||
prevIndex = entityEndIndex
|
||||
|
||||
}
|
||||
|
||||
if prevIndex < displayEndIndex {
|
||||
html += String(text[prevIndex..<displayEndIndex]).replacingOccurrences(of: "\n", with: "<br>")
|
||||
}
|
||||
|
||||
return html
|
||||
} else {
|
||||
return displayText
|
||||
}
|
||||
}
|
||||
|
||||
func renderAsTweetHTML(_ status: TwitterStatus, topLevel: Bool) -> String {
|
||||
var html = "<div>\(status.displayHTML ?? "")</div>"
|
||||
|
||||
if !topLevel, let createdAt = status.createdAt, let url = status.url {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .short
|
||||
html += "<a href=\"\(url)\" class=\"twitterTimestamp\">\(dateFormatter.string(from: createdAt))</a>"
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
func renderAsOriginalHTML(topLevel: Bool) -> String {
|
||||
var html = renderAsTweetHTML(self, topLevel: topLevel)
|
||||
html += extendedEntities?.renderAsHTML() ?? ""
|
||||
return html
|
||||
}
|
||||
|
||||
func renderAsRetweetHTML(_ status: TwitterStatus) -> String {
|
||||
var html = "<blockquote>"
|
||||
if let userHTML = status.user?.renderAsHTML() {
|
||||
html += userHTML
|
||||
}
|
||||
html += status.renderAsHTML(topLevel: false)
|
||||
html += "</blockquote>"
|
||||
return html
|
||||
}
|
||||
|
||||
func renderAsQuoteHTML(_ quotedStatus: TwitterStatus, topLevel: Bool) -> String {
|
||||
var html = String()
|
||||
html += renderAsTweetHTML(self, topLevel: topLevel)
|
||||
html += extendedEntities?.renderAsHTML() ?? ""
|
||||
html += "<blockquote>"
|
||||
if let userHTML = quotedStatus.user?.renderAsHTML() {
|
||||
html += userHTML
|
||||
}
|
||||
html += quotedStatus.renderAsHTML(topLevel: false)
|
||||
html += "</blockquote>"
|
||||
return html
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// TwitterSymbol.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterSymbol: Codable, TwitterEntity {
|
||||
|
||||
let text: String?
|
||||
let indices: [Int]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
case indices = "indices"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
var html = String()
|
||||
if let text = text {
|
||||
html += "<a href=\"https://twitter.com/search?q=%24\(text)\">$\(text)</a>"
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// TwitterURL.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterURL: Codable, TwitterEntity {
|
||||
|
||||
let url: String?
|
||||
let indices: [Int]?
|
||||
let displayURL: String?
|
||||
let expandedURL: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "url"
|
||||
case indices = "indices"
|
||||
case displayURL = "display_url"
|
||||
case expandedURL = "expanded_url"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String {
|
||||
var html = String()
|
||||
if let expandedURL = expandedURL, let displayURL = displayURL {
|
||||
html += "<a href=\"\(expandedURL)\">\(displayURL)</a>"
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// TwitterUser.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/16/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TwitterUser: Codable {
|
||||
|
||||
let name: String?
|
||||
let screenName: String?
|
||||
let avatarURL: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name = "name"
|
||||
case screenName = "screen_name"
|
||||
case avatarURL = "profile_image_url_https"
|
||||
}
|
||||
|
||||
var url: String {
|
||||
return "https://twitter.com/\(screenName ?? "")"
|
||||
}
|
||||
|
||||
func renderAsHTML() -> String? {
|
||||
var html = String()
|
||||
html += "<div><a href=\"\(url)\">"
|
||||
if let avatarURL = avatarURL {
|
||||
html += "<img class=\"twitterAvatar nnw-nozoom\" src=\"\(avatarURL)\">"
|
||||
}
|
||||
html += "<div class=\"twitterUsername\">"
|
||||
if let name = name {
|
||||
html += " \(name)"
|
||||
}
|
||||
html += "<br><span class=\"twitterScreenName\">"
|
||||
if let screenName = screenName {
|
||||
html += " @\(screenName)"
|
||||
}
|
||||
html += "</span></div></a></div>"
|
||||
return html
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// TwitterVideoInfo.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/18/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct TwitterVideo: Codable {
|
||||
|
||||
let variants: [Variant]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case variants = "variants"
|
||||
}
|
||||
|
||||
struct Variant: Codable {
|
||||
|
||||
let bitrate: Int?
|
||||
let contentType: String?
|
||||
let url: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case bitrate = "bitrate"
|
||||
case contentType = "content_type"
|
||||
case url = "url"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProv
|
||||
service.getCollections { result in
|
||||
switch result {
|
||||
case .success(let collections):
|
||||
self.logger.debug("Receving collections: \(collections.map({ $0.id }), privacy: .public)")
|
||||
self.logger.debug("Receiving collections: \(collections.map({ $0.id }), privacy: .public)")
|
||||
self.collections = collections
|
||||
self.didFinish()
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ public struct SingleArticleFetcher: ArticleFetcher {
|
||||
public func fetchUnreadArticles() throws -> Set<Article> {
|
||||
return try account.fetchArticles(.articleIDs(Set([articleID])))
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
return try account.fetchArticlesBetween(articleIDs: Set([articleID]), before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion)
|
||||
|
||||
@@ -253,17 +253,13 @@ public final class WebFeed: Feed, Renamable, Hashable, ObservableObject {
|
||||
// MARK: - NotificationDisplayName
|
||||
public var notificationDisplayName: String {
|
||||
#if os(macOS)
|
||||
if self.url.contains("twitter.com") {
|
||||
return NSLocalizedString("Show notifications for new tweets", comment: "notifyNameDisplay / Twitter")
|
||||
} else if self.url.contains("www.reddit.com") {
|
||||
if self.url.contains("www.reddit.com") {
|
||||
return NSLocalizedString("Show notifications for new posts", comment: "notifyNameDisplay / Reddit")
|
||||
} else {
|
||||
return NSLocalizedString("Show notifications for new articles", comment: "notifyNameDisplay / Default")
|
||||
}
|
||||
#else
|
||||
if self.url.contains("twitter.com") {
|
||||
return NSLocalizedString("Notify about new tweets", comment: "notifyNameDisplay / Twitter")
|
||||
} else if self.url.contains("www.reddit.com") {
|
||||
if self.url.contains("www.reddit.com") {
|
||||
return NSLocalizedString("Notify about new posts", comment: "notifyNameDisplay / Reddit")
|
||||
} else {
|
||||
return NSLocalizedString("Notify about new articles", comment: "notifyNameDisplay / Default")
|
||||
|
||||
@@ -13,8 +13,6 @@ struct FeedlyTestSecrets: SecretsProvider {
|
||||
var mercuryClientSecret = ""
|
||||
var feedlyClientId = ""
|
||||
var feedlyClientSecret = ""
|
||||
var twitterConsumerKey = ""
|
||||
var twitterConsumerSecret = ""
|
||||
var redditConsumerKey = ""
|
||||
var inoreaderAppId = ""
|
||||
var inoreaderAppKey = ""
|
||||
|
||||
@@ -106,6 +106,14 @@ public final class ArticlesDatabase {
|
||||
return try articlesTable.fetchUnreadArticles(webFeedIDs, limit)
|
||||
}
|
||||
|
||||
public func fetchArticlesBetween(articleIDs: Set<String>, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesBetween(_ webFeedIDs: Set<String>, _ limit: Int?, _ before: Date?, _ after: Date?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchUnreadArticlesBetween(webFeedIDs, limit, before, after)
|
||||
}
|
||||
|
||||
public func fetchTodayArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
|
||||
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ final class ArticlesTable: DatabaseTable {
|
||||
return try fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) }
|
||||
}
|
||||
|
||||
func fetchArticlesBetween(articleIDs: Set<String>, before: Date?, after: Date?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after, $0) }
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
|
||||
return fetchArticlesAsync({ self.fetchArticles(articleIDs: articleIDs, $0) }, completion)
|
||||
}
|
||||
@@ -80,6 +84,10 @@ final class ArticlesTable: DatabaseTable {
|
||||
return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) }
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(_ webFeedIDs: Set<String>, _ limit: Int?, _ before: Date?, _ after: Date?) throws -> Set<Article> {
|
||||
return try fetchArticles{ self.fetchUnreadArticlesBetween(webFeedIDs, limit, $0, before, after) }
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion)
|
||||
}
|
||||
@@ -845,6 +853,30 @@ private extension ArticlesTable {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(_ webFeedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase, _ before: Date?, _ after: Date?) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
|
||||
if webFeedIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
var parameters = webFeedIDs.map { $0 as AnyObject }
|
||||
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
var whereClause = "feedID in \(placeholders) and read=0"
|
||||
|
||||
if let before = before {
|
||||
whereClause.append(" and (datePublished < ? or (datePublished is null and dateArrived < ?))")
|
||||
parameters = parameters + [before as AnyObject, before as AnyObject]
|
||||
}
|
||||
if let after = after {
|
||||
whereClause.append(" and (datePublished > ? or (datePublished is null and dateArrived > ?))")
|
||||
parameters = parameters + [after as AnyObject, after as AnyObject]
|
||||
}
|
||||
if let limit = limit {
|
||||
whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)")
|
||||
}
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchArticlesForFeedID(_ webFeedID: String, _ database: FMDatabase) -> Set<Article> {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject])
|
||||
}
|
||||
@@ -859,6 +891,26 @@ private extension ArticlesTable {
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchArticlesBetween(articleIDs: Set<String>, before: Date?, after: Date?, _ database: FMDatabase) -> Set<Article> {
|
||||
if articleIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
var parameters = articleIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
|
||||
var whereClause = "articleID in \(placeholders)"
|
||||
|
||||
if let before = before {
|
||||
whereClause.append(" and (datePublished < ? or (datePublished is null and dateArrived < ?))")
|
||||
parameters = parameters + [before as AnyObject, before as AnyObject]
|
||||
}
|
||||
if let after = after {
|
||||
whereClause.append(" and (datePublished > ? or (datePublished is null and dateArrived > ?))")
|
||||
parameters = parameters + [after as AnyObject, after as AnyObject]
|
||||
}
|
||||
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||
}
|
||||
|
||||
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?)
|
||||
//
|
||||
|
||||
@@ -84,10 +84,6 @@ struct AppAssets {
|
||||
return RSImage(named: "extensionPointReddit")!
|
||||
}()
|
||||
|
||||
static var extensionPointTwitter: RSImage = {
|
||||
return RSImage(named: "extensionPointTwitter")!
|
||||
}()
|
||||
|
||||
static var faviconTemplateImage: RSImage = {
|
||||
return RSImage(named: "faviconTemplateImage")!
|
||||
}()
|
||||
|
||||
@@ -43,6 +43,7 @@ final class AppDefaults {
|
||||
static let defaultBrowserID = "defaultBrowserID"
|
||||
static let currentThemeName = "currentThemeName"
|
||||
static let hasSeenNotAllArticlesHaveURLsAlert = "hasSeenNotAllArticlesHaveURLsAlert"
|
||||
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
|
||||
|
||||
// Hidden prefs
|
||||
static let showDebugMenu = "ShowDebugMenu"
|
||||
@@ -318,6 +319,16 @@ final class AppDefaults {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: Key.refreshInterval)
|
||||
}
|
||||
}
|
||||
|
||||
var twitterDeprecationAlertShown: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.twitterDeprecationAlertShown)
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.twitterDeprecationAlertShown, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func registerDefaults() {
|
||||
#if DEBUG
|
||||
|
||||
@@ -131,6 +131,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
|
||||
|
||||
appDelegate = self
|
||||
|
||||
presentTwitterDeprecationAlertIfRequired()
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
@@ -520,26 +522,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
return ExtensionPointManager.shared.isRedditEnabled
|
||||
}
|
||||
|
||||
if item.action == #selector(showAddTwitterFeedWindow(_:)) {
|
||||
guard !isDisplayingSheet && isSpecialAccountAvailable && ExtensionPointManager.shared.isTwitterEnabled else {
|
||||
return false
|
||||
}
|
||||
return ExtensionPointManager.shared.isTwitterEnabled
|
||||
}
|
||||
|
||||
#if !DEBUG
|
||||
if item.action == #selector(debugDropConditionalGetInfo(_:)) {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
|
||||
if item.action == #selector(debugTestCrashReporterWindow(_:)) ||
|
||||
item.action == #selector(debugTestCrashReportSending(_:)) ||
|
||||
item.action == #selector(forceCrash(_:)) {
|
||||
let appIDPrefix = Bundle.main.infoDictionary?["AppIdentifierPrefix"] as! String
|
||||
return appIDPrefix == "M8L2WTLA8W."
|
||||
}
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
if item.action == #selector(toggleWebInspectorEnabled(_:)) {
|
||||
(item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off
|
||||
@@ -619,12 +601,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
addFeedController?.showAddFeedSheet(.redditFeed)
|
||||
}
|
||||
|
||||
@IBAction func showAddTwitterFeedWindow(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
addFeedController = AddFeedController(hostWindow: mainWindowController!.window!)
|
||||
addFeedController?.showAddFeedSheet(.twitterFeed)
|
||||
}
|
||||
|
||||
@IBAction func showAddFolderWindow(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
showAddFolderSheetOnWindow(mainWindowController!.window!)
|
||||
@@ -703,35 +679,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
Browser.open("https://netnewswire.com/", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func openReleaseNotes(_ sender: Any?) {
|
||||
Browser.open(URL.releaseNotes.absoluteString, inBackground: false)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func openHowToSupport(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func openRepository(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://github.com/brentsimmons/NetNewsWire", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func openBugTracker(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://github.com/brentsimmons/NetNewsWire/issues", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func openSlackGroup(_ sender: Any?) {
|
||||
Browser.open("https://netnewswire.com/slack", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func openTechnotes(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func showHelp(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://netnewswire.com/help/mac/6.1/en/", inBackground: false)
|
||||
@@ -1009,6 +956,33 @@ internal extension AppDelegate {
|
||||
alert.beginSheetModal(for: window)
|
||||
}
|
||||
|
||||
private func presentTwitterDeprecationAlertIfRequired() {
|
||||
if AppDefaults.shared.twitterDeprecationAlertShown { return }
|
||||
|
||||
let expiryDate = Date(timeIntervalSince1970: 1691539200) // August 9th 2023, 00:00 UTC
|
||||
let currentDate = Date()
|
||||
if currentDate > expiryDate {
|
||||
return // If after August 9th, don't show
|
||||
}
|
||||
|
||||
if AccountManager.shared.anyLocalOriCloudAccountHasAtLeastOneTwitterFeed() {
|
||||
showTwitterDeprecationAlert()
|
||||
}
|
||||
AppDefaults.shared.twitterDeprecationAlertShown = true
|
||||
}
|
||||
|
||||
private func showTwitterDeprecationAlert() {
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = NSLocalizedString("Twitter Integration Removed", comment: "Twitter Integration Removed")
|
||||
alert.informativeText = NSLocalizedString("On February 1, 2023, Twitter announced the end of free access to the Twitter API, effective February 9.\n\nSince Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.\n\nWe’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.\n\nYou can still read whatever you have already downloaded. However, those feeds will no longer update.", comment: "Twitter deprecation informative text.")
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
|
||||
alert.buttons[0].keyEquivalent = "\r"
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17154" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17154"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AddTwitterFeedWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="accountLabel" destination="Acr-Ig-NVG" id="1gD-BE-CjH"/>
|
||||
<outlet property="accountPopupButton" destination="X1H-Vv-1CJ" id="I0k-bb-XcU"/>
|
||||
<outlet property="addButton" destination="dtI-Hu-rFb" id="D11-zR-dWH"/>
|
||||
<outlet property="folderPopupButton" destination="6vt-DL-mVR" id="98M-xt-ZYU"/>
|
||||
<outlet property="nameTextField" destination="TzV-3k-fXd" id="h4h-5v-4cY"/>
|
||||
<outlet property="screenSearchTextField" destination="cEh-Wt-f5D" id="bnp-Zp-1fe"/>
|
||||
<outlet property="typeDescriptionLabel" destination="f4Z-B8-HHm" id="jZ2-gz-Zr2"/>
|
||||
<outlet property="typePopupButton" destination="j18-w8-wsH" id="KFC-K4-0tG"/>
|
||||
<outlet property="window" destination="QvC-M9-y7g" id="7rH-S2-LF4"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Add Twitter Feed" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="306" height="216"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||
<view key="contentView" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="306" height="216"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hXq-IS-19x">
|
||||
<rect key="frame" x="119" y="13" width="88" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Dop-HC-6Q9">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="cancel:" target="-2" id="tcT-tt-t99"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dtI-Hu-rFb">
|
||||
<rect key="frame" x="205" y="13" width="88" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Add" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6NK-Ql-drk">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="addFeed:" target="-2" id="Ilv-Un-eDp"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddC-6D-Tvd">
|
||||
<rect key="frame" x="40" y="177" width="41" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Type:" id="qto-IO-a1j">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="j18-w8-wsH">
|
||||
<rect key="frame" x="85" y="172" width="204" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Home Timeline" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="uE6-1a-w5g" id="bad-PM-uqO">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="Ibj-Uy-KK7">
|
||||
<items>
|
||||
<menuItem title="Home Timeline" state="on" id="uE6-1a-w5g"/>
|
||||
<menuItem title="Mentions" tag="1" id="177-F8-Esj"/>
|
||||
<menuItem title="Screen Name" tag="2" id="DBZ-RV-FfV"/>
|
||||
<menuItem title="Search" tag="3" id="0gG-oY-8yR"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="selectedType:" target="-2" id="eAs-So-odx"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Acr-Ig-NVG">
|
||||
<rect key="frame" x="18" y="146" width="63" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Account:" id="LFf-JL-Ahl">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="X1H-Vv-1CJ">
|
||||
<rect key="frame" x="85" y="141" width="204" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="@username" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Tfk-aQ-RKg" id="HPE-P1-Hje">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="TmQ-5T-oaz">
|
||||
<items>
|
||||
<menuItem title="@username" state="on" id="Tfk-aQ-RKg"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="cEh-Wt-f5D">
|
||||
<rect key="frame" x="87" y="144" width="199" height="21"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="NLJ-ih-hZ8">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="hNy-Li-bjr"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="f4Z-B8-HHm">
|
||||
<rect key="frame" x="85" y="122" width="203" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" usesSingleLineMode="YES" id="5AA-um-oEb">
|
||||
<font key="font" metaFont="menu" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sM9-DX-M0c">
|
||||
<rect key="frame" x="35" y="94" width="46" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name:" id="8ca-Qp-BkT">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TzV-3k-fXd" userLabel="Name Text Field">
|
||||
<rect key="frame" x="87" y="91" width="199" height="21"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="Optional" drawsBackground="YES" usesSingleLineMode="YES" id="pLP-pL-5R5">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dNV-oD-vzR">
|
||||
<rect key="frame" x="31" y="61" width="50" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Folder:" id="Kwx-7B-CIu">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="6vt-DL-mVR" userLabel="Folder Popup">
|
||||
<rect key="frame" x="85" y="57" width="204" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tLJ-zY-CcZ" id="0cM-5q-Snl">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="OpL-Uf-woJ">
|
||||
<items>
|
||||
<menuItem title="Item 1" state="on" id="tLJ-zY-CcZ"/>
|
||||
<menuItem title="Item 2" id="APc-af-7Um"/>
|
||||
<menuItem title="Item 3" id="j09-9b-bGs"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="dNV-oD-vzR" firstAttribute="baseline" secondItem="6vt-DL-mVR" secondAttribute="baseline" id="14b-jN-4Y6"/>
|
||||
<constraint firstItem="X1H-Vv-1CJ" firstAttribute="firstBaseline" secondItem="Acr-Ig-NVG" secondAttribute="firstBaseline" id="3Cl-Bw-Pcy"/>
|
||||
<constraint firstItem="X1H-Vv-1CJ" firstAttribute="top" secondItem="j18-w8-wsH" secondAttribute="bottom" constant="10" id="48A-2f-2Wq"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dtI-Hu-rFb" secondAttribute="bottom" constant="20" symbolic="YES" id="6ac-2K-RnD"/>
|
||||
<constraint firstItem="cEh-Wt-f5D" firstAttribute="leading" secondItem="j18-w8-wsH" secondAttribute="leading" id="73d-zR-g8z"/>
|
||||
<constraint firstItem="TzV-3k-fXd" firstAttribute="leading" secondItem="cEh-Wt-f5D" secondAttribute="leading" id="Ap9-Ln-amq"/>
|
||||
<constraint firstAttribute="trailing" secondItem="X1H-Vv-1CJ" secondAttribute="trailing" constant="20" id="Boa-Qw-dIK"/>
|
||||
<constraint firstItem="TzV-3k-fXd" firstAttribute="leading" secondItem="sM9-DX-M0c" secondAttribute="trailing" constant="8" id="Ebw-Fa-w9o"/>
|
||||
<constraint firstItem="TzV-3k-fXd" firstAttribute="top" secondItem="f4Z-B8-HHm" secondAttribute="bottom" constant="10" id="Elk-Gm-e4i"/>
|
||||
<constraint firstItem="X1H-Vv-1CJ" firstAttribute="leading" secondItem="Acr-Ig-NVG" secondAttribute="trailing" constant="8" id="HwM-IS-kMa"/>
|
||||
<constraint firstItem="dtI-Hu-rFb" firstAttribute="width" secondItem="hXq-IS-19x" secondAttribute="width" id="J80-aG-OjE"/>
|
||||
<constraint firstItem="sM9-DX-M0c" firstAttribute="baseline" secondItem="TzV-3k-fXd" secondAttribute="baseline" id="K9a-t8-khQ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="f4Z-B8-HHm" secondAttribute="trailing" constant="20" id="POl-uX-qpn"/>
|
||||
<constraint firstItem="f4Z-B8-HHm" firstAttribute="leading" secondItem="j18-w8-wsH" secondAttribute="leading" id="RbK-fc-c6E"/>
|
||||
<constraint firstItem="hXq-IS-19x" firstAttribute="centerY" secondItem="dtI-Hu-rFb" secondAttribute="centerY" id="Sgq-Cy-rII"/>
|
||||
<constraint firstItem="6vt-DL-mVR" firstAttribute="top" secondItem="TzV-3k-fXd" secondAttribute="bottom" constant="10" id="Sjo-Bv-alZ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TzV-3k-fXd" secondAttribute="trailing" constant="20" symbolic="YES" id="V1s-JA-hA8"/>
|
||||
<constraint firstItem="6vt-DL-mVR" firstAttribute="leading" secondItem="dNV-oD-vzR" secondAttribute="trailing" constant="8" id="WNy-vn-p8M"/>
|
||||
<constraint firstItem="f4Z-B8-HHm" firstAttribute="top" secondItem="cEh-Wt-f5D" secondAttribute="bottom" constant="8" id="WiN-GE-aPh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="cEh-Wt-f5D" secondAttribute="trailing" constant="20" id="ZSt-ga-a8N"/>
|
||||
<constraint firstItem="dtI-Hu-rFb" firstAttribute="leading" secondItem="hXq-IS-19x" secondAttribute="trailing" constant="12" symbolic="YES" id="ahD-oU-iFu"/>
|
||||
<constraint firstItem="Acr-Ig-NVG" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="dhv-D0-aPe"/>
|
||||
<constraint firstAttribute="trailing" secondItem="j18-w8-wsH" secondAttribute="trailing" constant="20" id="eQ9-hw-PXg"/>
|
||||
<constraint firstItem="j18-w8-wsH" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" symbolic="YES" id="fK6-IW-NhJ"/>
|
||||
<constraint firstItem="j18-w8-wsH" firstAttribute="leading" secondItem="X1H-Vv-1CJ" secondAttribute="leading" id="gSv-gG-TLd"/>
|
||||
<constraint firstItem="6vt-DL-mVR" firstAttribute="leading" secondItem="TzV-3k-fXd" secondAttribute="leading" id="hMP-wG-fsP"/>
|
||||
<constraint firstItem="cEh-Wt-f5D" firstAttribute="top" secondItem="j18-w8-wsH" secondAttribute="bottom" constant="10" id="hxS-Z9-dWU"/>
|
||||
<constraint firstItem="j18-w8-wsH" firstAttribute="firstBaseline" secondItem="ddC-6D-Tvd" secondAttribute="firstBaseline" id="iz7-4p-NWj"/>
|
||||
<constraint firstAttribute="trailing" secondItem="dtI-Hu-rFb" secondAttribute="trailing" constant="20" symbolic="YES" id="kEo-af-SUe"/>
|
||||
<constraint firstItem="j18-w8-wsH" firstAttribute="leading" secondItem="ddC-6D-Tvd" secondAttribute="trailing" constant="8" id="n9D-4Y-HXk"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6vt-DL-mVR" secondAttribute="trailing" constant="20" id="suO-dd-E0b"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<point key="canvasLocation" x="102" y="-768"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -80,12 +80,6 @@
|
||||
<action selector="showAddRedditFeedWindow:" target="Ady-hI-5gd" id="Irh-Rw-mFK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="New Twitter Feed…" id="ki4-7l-tM6">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="showAddTwitterFeedWindow:" target="Ady-hI-5gd" id="dZR-aU-O52"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="New Folder…" keyEquivalent="N" id="wkh-LX-Xp1">
|
||||
<connections>
|
||||
<action selector="showAddFolderWindow:" target="Ady-hI-5gd" id="GIi-wc-uYk"/>
|
||||
@@ -646,48 +640,12 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="rK6-cL-4Vj"/>
|
||||
<menuItem title="Website" id="q2Z-9K-GBd">
|
||||
<menuItem title="NetNewsWire Website" id="q2Z-9K-GBd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openWebsite:" target="Ady-hI-5gd" id="rmL-lt-p8g"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Release Notes" id="b5s-xg-B1w" userLabel="Release Notes">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openReleaseNotes:" target="Ady-hI-5gd" id="7LS-uW-Yp0"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="How To Support NetNewsWire" id="kfC-NQ-g3E">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openHowToSupport:" target="Ady-hI-5gd" id="SIw-Ug-A4D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="GitHub Repository" id="QfD-Xw-sdF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openRepository:" target="Ady-hI-5gd" id="7xZ-V2-iPD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Bug Tracker" id="mE2-pM-rQF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openBugTracker:" target="Ady-hI-5gd" id="fZQ-ng-gIm"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Technotes" id="Ou5-Cc-iCb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openTechnotes:" target="Ady-hI-5gd" id="M7A-Qg-mH8"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Slack Group" id="4eb-qF-n9S">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="openSlackGroup:" target="Ady-hI-5gd" id="YX2-gA-pgV"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="mPU-HG-I4u">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="mPU-HG-I4u">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21225"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -36,7 +36,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<customView horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Ut3-yd-q6G">
|
||||
<rect key="frame" x="54" y="16" width="400" height="306"/>
|
||||
<rect key="frame" x="53" y="16" width="402" height="306"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pR2-Bf-7Fd">
|
||||
<rect key="frame" x="6" y="285" width="106" height="16"/>
|
||||
@@ -101,7 +101,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Tdg-6Y-gvW">
|
||||
<rect key="frame" x="0.0" y="197" width="400" height="5"/>
|
||||
<rect key="frame" x="0.0" y="197" width="402" height="5"/>
|
||||
</box>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Wsb-Lr-8Q7">
|
||||
<rect key="frame" x="6" y="166" width="106" height="16"/>
|
||||
@@ -155,7 +155,7 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="hQy-ng-ijd">
|
||||
<rect key="frame" x="0.0" y="38" width="400" height="5"/>
|
||||
<rect key="frame" x="0.0" y="38" width="402" height="5"/>
|
||||
</box>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ucw-vG-yLt">
|
||||
<rect key="frame" x="6" y="7" width="106" height="16"/>
|
||||
@@ -261,7 +261,7 @@
|
||||
<constraint firstItem="hQy-ng-ijd" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="KEI-R5-rzD"/>
|
||||
<constraint firstItem="S2Z-bG-jYk" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="KQI-3T-s6M"/>
|
||||
<constraint firstItem="pR2-Bf-7Fd" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" constant="8" id="LRG-HZ-yxh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SFF-mL-yc8" secondAttribute="trailing" id="N39-Q9-X5Q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SFF-mL-yc8" secondAttribute="trailing" constant="2" id="N39-Q9-X5Q"/>
|
||||
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="Ore-Y8-DM8"/>
|
||||
<constraint firstItem="ISO-Wu-R60" firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" id="P3r-hD-nE8"/>
|
||||
<constraint firstItem="S2Z-bG-jYk" firstAttribute="width" secondItem="pR2-Bf-7Fd" secondAttribute="width" id="QCg-QQ-rJf"/>
|
||||
@@ -276,7 +276,7 @@
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="trailing" secondItem="SFF-mL-yc8" secondAttribute="trailing" id="Zkn-zv-as5"/>
|
||||
<constraint firstItem="1w0-nA-DEO" firstAttribute="top" secondItem="ISO-Wu-R60" secondAttribute="bottom" constant="14" id="ZlG-V3-AAd"/>
|
||||
<constraint firstItem="pR2-Bf-7Fd" firstAttribute="firstBaseline" secondItem="Z6O-Zt-V1g" secondAttribute="firstBaseline" id="aO5-iE-L7A"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" id="aS9-KA-vSH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" constant="2" id="aS9-KA-vSH"/>
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="top" secondItem="j0t-Wa-UTL" secondAttribute="bottom" constant="14" id="aod-td-Gim"/>
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="firstBaseline" secondItem="ucw-vG-yLt" secondAttribute="firstBaseline" id="aqn-St-DJy"/>
|
||||
<constraint firstItem="Tdg-6Y-gvW" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="b3I-JF-If3"/>
|
||||
@@ -300,7 +300,7 @@
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="leading" secondItem="ucw-vG-yLt" secondAttribute="trailing" constant="8" symbolic="YES" id="yBm-Dc-lGA"/>
|
||||
<constraint firstAttribute="bottom" secondItem="SFF-mL-yc8" secondAttribute="bottom" constant="4" id="zIa-Ca-y3J"/>
|
||||
<constraint firstItem="ISO-Wu-R60" firstAttribute="top" secondItem="Z6O-Zt-V1g" secondAttribute="bottom" constant="14" id="zaM-J3-VcP"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Ci4-fW-KjU" secondAttribute="trailing" id="zbx-Ch-NEt"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Ci4-fW-KjU" secondAttribute="trailing" constant="2" id="zbx-Ch-NEt"/>
|
||||
<constraint firstItem="ucw-vG-yLt" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="zkC-Ma-Dz8"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
@@ -495,16 +495,16 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="7UM-iq-OLB" customClass="PreferencesTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="44" width="180" height="211"/>
|
||||
<rect key="frame" x="20" y="44" width="180" height="213"/>
|
||||
<subviews>
|
||||
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="26" horizontalPageScroll="10" verticalLineScroll="26" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PaF-du-r3c">
|
||||
<rect key="frame" x="1" y="1" width="178" height="209"/>
|
||||
<rect key="frame" x="1" y="0.0" width="178" height="212"/>
|
||||
<clipView key="contentView" id="cil-Gq-akO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="209"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="212"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" tableStyle="fullWidth" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="aTp-KR-y6b">
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="209"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="212"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -527,7 +527,7 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="27f-p8-Wnt">
|
||||
<rect key="frame" x="6" y="-2" width="16" height="22"/>
|
||||
<rect key="frame" x="6" y="-4" width="16" height="27"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="16" id="ewi-ds-jXB"/>
|
||||
<constraint firstAttribute="width" constant="16" id="k2v-Dn-07l"/>
|
||||
@@ -579,7 +579,7 @@
|
||||
<constraint firstItem="PaF-du-r3c" firstAttribute="leading" secondItem="7UM-iq-OLB" secondAttribute="leading" constant="1" id="Brq-cg-FVo"/>
|
||||
<constraint firstItem="PaF-du-r3c" firstAttribute="top" secondItem="7UM-iq-OLB" secondAttribute="top" constant="1" id="G3u-Hk-xlH"/>
|
||||
<constraint firstAttribute="width" constant="180" id="MWF-uR-jbC"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PaF-du-r3c" secondAttribute="bottom" constant="1" id="bjN-h8-jtK"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PaF-du-r3c" secondAttribute="bottom" id="bjN-h8-jtK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="PaF-du-r3c" secondAttribute="trailing" constant="1" id="dfm-a5-dYc"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
@@ -611,7 +611,7 @@
|
||||
<rect key="frame" x="83" y="20" width="117" height="24"/>
|
||||
</customView>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="Y7D-xQ-wep">
|
||||
<rect key="frame" x="208" y="20" width="222" height="235"/>
|
||||
<rect key="frame" x="208" y="20" width="222" height="237"/>
|
||||
</customView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -666,16 +666,16 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="pjs-G4-byk" customClass="PreferencesTableViewBackgroundView" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="44" width="180" height="211"/>
|
||||
<rect key="frame" x="20" y="44" width="180" height="213"/>
|
||||
<subviews>
|
||||
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="26" horizontalPageScroll="10" verticalLineScroll="26" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="29T-r2-ckC">
|
||||
<rect key="frame" x="1" y="1" width="178" height="209"/>
|
||||
<rect key="frame" x="1" y="0.0" width="178" height="212"/>
|
||||
<clipView key="contentView" id="dXw-GY-TP8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="209"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="212"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" tableStyle="fullWidth" columnReordering="NO" columnSelection="YES" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" viewBased="YES" id="dfn-Vn-oDp">
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="209"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="178" height="212"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -698,7 +698,7 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="kmG-vw-CbN">
|
||||
<rect key="frame" x="6" y="-2" width="16" height="22"/>
|
||||
<rect key="frame" x="6" y="-4" width="16" height="27"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="16" id="qC6-Mb-6EQ"/>
|
||||
<constraint firstAttribute="width" constant="16" id="yi0-bd-XJq"/>
|
||||
@@ -744,7 +744,7 @@
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="180" id="0gU-oR-pQf"/>
|
||||
<constraint firstAttribute="bottom" secondItem="29T-r2-ckC" secondAttribute="bottom" constant="1" id="BMY-9E-vH2"/>
|
||||
<constraint firstAttribute="bottom" secondItem="29T-r2-ckC" secondAttribute="bottom" id="BMY-9E-vH2"/>
|
||||
<constraint firstAttribute="trailing" secondItem="29T-r2-ckC" secondAttribute="trailing" constant="1" id="dAW-1i-3iD"/>
|
||||
<constraint firstItem="29T-r2-ckC" firstAttribute="top" secondItem="pjs-G4-byk" secondAttribute="top" constant="1" id="tAi-6L-Tjj"/>
|
||||
<constraint firstItem="29T-r2-ckC" firstAttribute="leading" secondItem="pjs-G4-byk" secondAttribute="leading" constant="1" id="wXE-ze-ubv"/>
|
||||
@@ -778,7 +778,7 @@
|
||||
<rect key="frame" x="83" y="20" width="117" height="24"/>
|
||||
</customView>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="N1N-pE-gBL">
|
||||
<rect key="frame" x="208" y="20" width="222" height="235"/>
|
||||
<rect key="frame" x="208" y="20" width="222" height="237"/>
|
||||
</customView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -813,8 +813,8 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="NSActionTemplate" width="15" height="15"/>
|
||||
<image name="NSAddTemplate" width="14" height="13"/>
|
||||
<image name="NSRemoveTemplate" width="14" height="4"/>
|
||||
<image name="NSActionTemplate" width="20" height="20"/>
|
||||
<image name="NSAddTemplate" width="18" height="17"/>
|
||||
<image name="NSRemoveTemplate" width="18" height="5"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -48,9 +48,6 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
case .redditFeed:
|
||||
addFeedWindowController = AddRedditFeedWindowController(folderTreeController: folderTreeController,
|
||||
delegate: self)
|
||||
case .twitterFeed:
|
||||
addFeedWindowController = AddTwitterFeedWindowController(folderTreeController: folderTreeController,
|
||||
delegate: self)
|
||||
}
|
||||
|
||||
addFeedWindowController!.runSheetOnWindow(hostWindow)
|
||||
|
||||
@@ -12,7 +12,6 @@ import Account
|
||||
enum AddFeedWindowControllerType {
|
||||
case webFeed
|
||||
case redditFeed
|
||||
case twitterFeed
|
||||
}
|
||||
|
||||
protocol AddFeedWindowControllerDelegate: AnyObject {
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
//
|
||||
// AddTwitterFeedWindowController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 4/21/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import RSCore
|
||||
import RSTree
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
class AddTwitterFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
|
||||
@IBOutlet weak var typePopupButton: NSPopUpButton!
|
||||
@IBOutlet weak var typeDescriptionLabel: NSTextField!
|
||||
|
||||
@IBOutlet weak var accountLabel: NSTextField!
|
||||
@IBOutlet weak var accountPopupButton: NSPopUpButton!
|
||||
@IBOutlet weak var screenSearchTextField: NSTextField!
|
||||
|
||||
@IBOutlet var nameTextField: NSTextField!
|
||||
@IBOutlet var addButton: NSButton!
|
||||
@IBOutlet var folderPopupButton: NSPopUpButton!
|
||||
|
||||
private weak var delegate: AddFeedWindowControllerDelegate?
|
||||
private var folderTreeController: TreeController!
|
||||
|
||||
private var userEnteredScreenSearch: String? {
|
||||
var s = screenSearchTextField.stringValue
|
||||
s = s.collapsingWhitespace
|
||||
if s.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
private var userEnteredTitle: String? {
|
||||
var s = nameTextField.stringValue
|
||||
s = s.collapsingWhitespace
|
||||
if s.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var hostWindow: NSWindow!
|
||||
|
||||
convenience init(folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
|
||||
self.init(windowNibName: NSNib.Name("AddTwitterFeedSheet"))
|
||||
self.folderTreeController = folderTreeController
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow) {
|
||||
hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in
|
||||
}
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
|
||||
let accountMenu = NSMenu()
|
||||
for feedProvider in ExtensionPointManager.shared.activeFeedProviders {
|
||||
if let twitterFeedProvider = feedProvider as? TwitterFeedProvider {
|
||||
let accountMenuItem = NSMenuItem()
|
||||
accountMenuItem.title = "@\(twitterFeedProvider.screenName)"
|
||||
accountMenu.addItem(accountMenuItem)
|
||||
}
|
||||
}
|
||||
accountPopupButton.menu = accountMenu
|
||||
|
||||
folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode, restrictToSpecialAccounts: true)
|
||||
|
||||
if let container = AddWebFeedDefaultContainer.defaultContainer {
|
||||
if let folder = container as? Folder, let account = folder.account {
|
||||
FolderTreeMenu.select(account: account, folder: folder, in: folderPopupButton)
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
FolderTreeMenu.select(account: account, folder: nil, in: folderPopupButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func selectedType(_ sender: Any) {
|
||||
screenSearchTextField.stringValue = ""
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: Any?) {
|
||||
cancelSheet()
|
||||
}
|
||||
|
||||
@IBAction func addFeed(_ sender: Any?) {
|
||||
guard let type = TwitterFeedType(rawValue: typePopupButton.selectedItem?.tag ?? 0),
|
||||
let atUsername = accountPopupButton.selectedItem?.title else { return }
|
||||
|
||||
let username = String(atUsername[atUsername.index(atUsername.startIndex, offsetBy: 1)..<atUsername.endIndex])
|
||||
|
||||
var screenSearch = userEnteredScreenSearch
|
||||
if let screenName = screenSearch, type == .screenName && screenName.starts(with: "@") {
|
||||
screenSearch = String(screenName[screenName.index(screenName.startIndex, offsetBy: 1)..<screenName.endIndex])
|
||||
}
|
||||
|
||||
guard let url = TwitterFeedProvider.buildURL(type, username: username, screenName: screenSearch, searchField: screenSearch) else { return }
|
||||
|
||||
let container = selectedContainer()!
|
||||
AddWebFeedDefaultContainer.saveDefaultContainer(container)
|
||||
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AddTwitterFeedWindowController: NSTextFieldDelegate {
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension AddTwitterFeedWindowController {
|
||||
|
||||
private func updateUI() {
|
||||
|
||||
switch typePopupButton.selectedItem?.tag ?? 0 {
|
||||
case 0:
|
||||
|
||||
accountLabel.isHidden = false
|
||||
accountPopupButton.isHidden = false
|
||||
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets from everyone you follow", comment: "Home Timeline")
|
||||
screenSearchTextField.isHidden = true
|
||||
addButton.isEnabled = true
|
||||
|
||||
case 1:
|
||||
|
||||
accountLabel.isHidden = false
|
||||
accountPopupButton.isHidden = false
|
||||
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets mentioning you", comment: "Mentions")
|
||||
screenSearchTextField.isHidden = true
|
||||
addButton.isEnabled = true
|
||||
|
||||
case 2:
|
||||
|
||||
accountLabel.isHidden = true
|
||||
accountPopupButton.isHidden = true
|
||||
|
||||
var screenSearch = userEnteredScreenSearch
|
||||
if screenSearch != nil {
|
||||
if let screenName = screenSearch, screenName.starts(with: "@") {
|
||||
screenSearch = String(screenName[screenName.index(screenName.startIndex, offsetBy: 1)..<screenName.endIndex])
|
||||
}
|
||||
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets from @\(screenSearch!)", comment: "Home Timeline")
|
||||
} else {
|
||||
typeDescriptionLabel.stringValue = ""
|
||||
}
|
||||
|
||||
screenSearchTextField.placeholderString = NSLocalizedString("@name", comment: "@name")
|
||||
screenSearchTextField.isHidden = false
|
||||
addButton.isEnabled = !screenSearchTextField.stringValue.isEmpty
|
||||
|
||||
default:
|
||||
|
||||
accountLabel.isHidden = true
|
||||
accountPopupButton.isHidden = true
|
||||
|
||||
if !screenSearchTextField.stringValue.isEmpty {
|
||||
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets that contain \(screenSearchTextField.stringValue)", comment: "Home Timeline")
|
||||
} else {
|
||||
typeDescriptionLabel.stringValue = ""
|
||||
}
|
||||
|
||||
screenSearchTextField.placeholderString = NSLocalizedString("Search Term or #hashtag", comment: "Search Term")
|
||||
screenSearchTextField.isHidden = false
|
||||
addButton.isEnabled = !screenSearchTextField.stringValue.isEmpty
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func cancelSheet() {
|
||||
delegate?.addFeedWindowControllerUserDidCancel(self)
|
||||
}
|
||||
|
||||
func selectedContainer() -> Container? {
|
||||
return folderPopupButton.selectedItem?.representedObject as? Container
|
||||
}
|
||||
}
|
||||
@@ -1452,11 +1452,6 @@ private extension MainWindowController {
|
||||
newRedditFeedItem.action = Selector(("showAddRedditFeedWindow:"))
|
||||
menu.addItem(newRedditFeedItem)
|
||||
|
||||
let newTwitterFeedItem = NSMenuItem()
|
||||
newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed…", comment: "New Twitter Feed")
|
||||
newTwitterFeedItem.action = Selector(("showAddTwitterFeedWindow:"))
|
||||
menu.addItem(newTwitterFeedItem)
|
||||
|
||||
let newFolderFeedItem = NSMenuItem()
|
||||
newFolderFeedItem.title = NSLocalizedString("New Folder…", comment: "New Folder")
|
||||
newFolderFeedItem.action = Selector(("showAddFolderWindow:"))
|
||||
|
||||
@@ -112,7 +112,7 @@ class SidebarCell : NSTableCellView {
|
||||
|
||||
override func accessibilityLabel() -> String? {
|
||||
if unreadCount > 0 {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessiblity")
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCount) \(unreadLabel)"
|
||||
} else {
|
||||
return name
|
||||
|
||||
@@ -84,6 +84,56 @@ extension SidebarViewController {
|
||||
runCommand(markReadCommand)
|
||||
}
|
||||
|
||||
|
||||
@objc func markObjectsReadOlderThanOneDayFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -1, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
@objc func markObjectsReadOlderThanTwoDaysFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -2, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
@objc func markObjectsReadOlderThanThreeDaysFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -3, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
@objc func markObjectsReadOlderThanOneWeekFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
@objc func markObjectsReadOlderThanTwoWeeksFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .weekOfYear, value: -2, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
@objc func markObjectsReadOlderThanOneMonthFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .month, value: -1, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
@objc func markObjectsReadOlderThanOneYearFromContextualMenu(_ sender: Any?) {
|
||||
return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .year, value: -1, to: Date()), after: nil, sender: sender)
|
||||
}
|
||||
|
||||
func markObjectsReadBetweenDatesFromContextualMenu(before: Date?, after: Date?, sender: Any?) {
|
||||
|
||||
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else {
|
||||
return
|
||||
}
|
||||
|
||||
var markableArticles = unreadArticlesBetween(for: objects, before: before, after: after)
|
||||
if let directlyMarkedAsUnreadArticles = delegate?.directlyMarkedAsUnreadArticles {
|
||||
markableArticles = markableArticles.subtracting(directlyMarkedAsUnreadArticles)
|
||||
}
|
||||
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
}
|
||||
|
||||
@objc func deleteFromContextualMenu(_ sender: Any?) {
|
||||
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else {
|
||||
return
|
||||
@@ -220,6 +270,12 @@ private extension SidebarViewController {
|
||||
|
||||
if webFeed.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([webFeed]))
|
||||
let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "")
|
||||
let catchUpSubMenu = catchUpSubMenu([webFeed])
|
||||
|
||||
menu.addItem(catchUpMenuItem)
|
||||
menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
@@ -276,6 +332,11 @@ private extension SidebarViewController {
|
||||
|
||||
if folder.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([folder]))
|
||||
let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "")
|
||||
let catchUpSubMenu = catchUpSubMenu([folder])
|
||||
|
||||
menu.addItem(catchUpMenuItem)
|
||||
menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
@@ -291,6 +352,18 @@ private extension SidebarViewController {
|
||||
|
||||
if smartFeed.unreadCount > 0 {
|
||||
menu.addItem(markAllReadMenuItem([smartFeed]))
|
||||
|
||||
// Doesn't make sense to mark articles newer than a day with catch up with first option being older than a day
|
||||
if let maybeSmartFeed = smartFeed as? SmartFeed {
|
||||
if maybeSmartFeed.delegate is TodayFeedDelegate {
|
||||
return menu
|
||||
}
|
||||
}
|
||||
|
||||
let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "")
|
||||
let catchUpSubMenu = catchUpSubMenu([smartFeed])
|
||||
menu.addItem(catchUpMenuItem)
|
||||
menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem)
|
||||
}
|
||||
return menu.numberOfItems > 0 ? menu : nil
|
||||
}
|
||||
@@ -301,6 +374,11 @@ private extension SidebarViewController {
|
||||
|
||||
if anyObjectInArrayHasNonZeroUnreadCount(objects) {
|
||||
menu.addItem(markAllReadMenuItem(objects))
|
||||
let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "")
|
||||
let catchUpSubMenu = catchUpSubMenu(objects)
|
||||
|
||||
menu.addItem(catchUpMenuItem)
|
||||
menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem)
|
||||
}
|
||||
|
||||
if allObjectsAreFeedsAndOrFolders(objects) {
|
||||
@@ -316,6 +394,20 @@ private extension SidebarViewController {
|
||||
return menuItem(NSLocalizedString("Mark All as Read", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)
|
||||
}
|
||||
|
||||
func catchUpSubMenu(_ objects: [Any]) -> NSMenu {
|
||||
let menu = NSMenu(title: "Catch up to articles older than...")
|
||||
|
||||
menu.addItem(menuItem(NSLocalizedString("1 day", comment: "Command"), #selector(markObjectsReadOlderThanOneDayFromContextualMenu(_:)), objects))
|
||||
menu.addItem(menuItem(NSLocalizedString("2 days", comment: "Command"), #selector(markObjectsReadOlderThanTwoDaysFromContextualMenu(_:)), objects))
|
||||
menu.addItem(menuItem(NSLocalizedString("3 days", comment: "Command"), #selector(markObjectsReadOlderThanThreeDaysFromContextualMenu(_:)), objects))
|
||||
menu.addItem(menuItem(NSLocalizedString("1 week", comment: "Command"), #selector(markObjectsReadOlderThanOneWeekFromContextualMenu(_:)), objects))
|
||||
menu.addItem(menuItem(NSLocalizedString("2 weeks", comment: "Command"), #selector(markObjectsReadOlderThanTwoWeeksFromContextualMenu(_:)), objects))
|
||||
menu.addItem(menuItem(NSLocalizedString("1 month", comment: "Command"), #selector(markObjectsReadOlderThanOneMonthFromContextualMenu(_:)), objects))
|
||||
menu.addItem(menuItem(NSLocalizedString("1 year", comment: "Command"), #selector(markObjectsReadOlderThanOneYearFromContextualMenu(_:)), objects))
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
func deleteMenuItem(_ objects: [Any]) -> NSMenuItem {
|
||||
|
||||
return menuItem(NSLocalizedString("Delete", comment: "Command"), #selector(deleteFromContextualMenu(_:)), objects)
|
||||
@@ -373,5 +465,18 @@ private extension SidebarViewController {
|
||||
}
|
||||
return articles
|
||||
}
|
||||
|
||||
func unreadArticlesBetween(for objects: [Any], before: Date?, after: Date?) -> Set<Article> {
|
||||
|
||||
var articles = Set<Article>()
|
||||
for object in objects {
|
||||
if let articleFetcher = object as? ArticleFetcher {
|
||||
if let unreadArticles = try? articleFetcher.fetchUnreadArticlesBetween(before: before, after: after) {
|
||||
articles.formUnion(unreadArticles)
|
||||
}
|
||||
}
|
||||
}
|
||||
return articles
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -376,7 +376,10 @@ protocol SidebarDelegate: AnyObject {
|
||||
}
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
|
||||
let node = item as! Node
|
||||
guard let node = item as? Node else {
|
||||
assertionFailure("Expected item to be a Node.")
|
||||
return false
|
||||
}
|
||||
return node.isGroupItem
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "twitter24x24.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "twitter48x48.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -12,6 +12,16 @@
|
||||
line-height: 1.4em;
|
||||
font-family: -apple-system;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
color: white;
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #1E1E1E !important;
|
||||
}
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
line-height: 2.0em;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -119,10 +119,6 @@
|
||||
512392C024E33A3C00F11704 /* RedditAdd.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516AE5FF246AF34100731738 /* RedditAdd.storyboard */; };
|
||||
512392C124E33A3C00F11704 /* RedditSelectTypeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE601246AF36100731738 /* RedditSelectTypeTableViewController.swift */; };
|
||||
512392C224E33A3C00F11704 /* RedditEnterDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE605246AF3A900731738 /* RedditEnterDetailTableViewController.swift */; };
|
||||
512392C324E3451400F11704 /* TwitterAdd.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 510289CF2451BA3A00426DDF /* TwitterAdd.storyboard */; };
|
||||
512392C424E3451400F11704 /* TwitterSelectTypeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510289D12451BC1F00426DDF /* TwitterSelectTypeTableViewController.swift */; };
|
||||
512392C524E3451400F11704 /* TwitterEnterDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */; };
|
||||
512392C624E3451400F11704 /* TwitterSelectAccountTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510289D52451DDD100426DDF /* TwitterSelectAccountTableViewController.swift */; };
|
||||
5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* SceneCoordinator.swift */; };
|
||||
5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; };
|
||||
5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; };
|
||||
@@ -199,10 +195,6 @@
|
||||
5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; };
|
||||
5148F44B2336DB4700F8CD8B /* MasterTimelineTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */; };
|
||||
5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */; };
|
||||
514A89A2244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */; };
|
||||
514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */; };
|
||||
514A89A5244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */; };
|
||||
514A89A6244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */; };
|
||||
514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; };
|
||||
514C16CE24D2E63F009A3AFA /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16CD24D2E63F009A3AFA /* Account */; };
|
||||
514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; };
|
||||
@@ -212,8 +204,6 @@
|
||||
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; };
|
||||
515A50E6243D07A90089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; };
|
||||
515A50E7243D07A90089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; };
|
||||
515A5107243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; };
|
||||
515A5108243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; };
|
||||
515A5148243E64BA0089E588 /* ExtensionPointEnableWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */; };
|
||||
515A5149243E64BA0089E588 /* ExtensionPointEnableWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */; };
|
||||
515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */ = {isa = PBXBuildFile; fileRef = 515A516D243E7F950089E588 /* ExtensionPointDetail.xib */; };
|
||||
@@ -224,7 +214,6 @@
|
||||
515A5178243E90200089E588 /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; };
|
||||
515A517B243E90260089E588 /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; };
|
||||
515A517C243E90260089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; };
|
||||
515A5180243E90260089E588 /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; };
|
||||
515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; };
|
||||
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
|
||||
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
|
||||
@@ -1183,9 +1172,6 @@
|
||||
17E0084525941887000C23F0 /* SizeCategories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeCategories.swift; sourceTree = "<group>"; };
|
||||
49F40DEF2335B71000552BF4 /* newsfoot.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = newsfoot.js; sourceTree = "<group>"; };
|
||||
510289CC24519A1D00426DDF /* SelectComboTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectComboTableViewCell.swift; sourceTree = "<group>"; };
|
||||
510289CF2451BA3A00426DDF /* TwitterAdd.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TwitterAdd.storyboard; sourceTree = "<group>"; };
|
||||
510289D12451BC1F00426DDF /* TwitterSelectTypeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterSelectTypeTableViewController.swift; sourceTree = "<group>"; };
|
||||
510289D52451DDD100426DDF /* TwitterSelectAccountTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterSelectAccountTableViewController.swift; sourceTree = "<group>"; };
|
||||
5103A9972421643300410853 /* blank.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = blank.html; sourceTree = "<group>"; };
|
||||
5103A9B324216A4200410853 /* blank.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = blank.html; sourceTree = "<group>"; };
|
||||
5103A9DA242258C600410853 /* AccountsAddCloudKit.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsAddCloudKit.xib; sourceTree = "<group>"; };
|
||||
@@ -1247,11 +1233,8 @@
|
||||
5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedbin.xib; sourceTree = "<group>"; };
|
||||
5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MasterTimelineTitleView.xib; sourceTree = "<group>"; };
|
||||
5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineTitleView.swift; sourceTree = "<group>"; };
|
||||
514A8980244FD63F0085E65D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/AddTwitterFeedSheet.xib; sourceTree = SOURCE_ROOT; };
|
||||
514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AddTwitterFeedWindowController.swift; path = AddFeed/AddTwitterFeedWindowController.swift; sourceTree = "<group>"; };
|
||||
514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = "<group>"; };
|
||||
515A50E5243D07A90089E588 /* ExtensionPointManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointManager.swift; sourceTree = "<group>"; };
|
||||
515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwitterFeedProvider-Extensions.swift"; sourceTree = "<group>"; };
|
||||
515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointEnableWindowController.swift; sourceTree = "<group>"; };
|
||||
515A516D243E7F950089E588 /* ExtensionPointDetail.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ExtensionPointDetail.xib; sourceTree = "<group>"; };
|
||||
515A5170243E802B0089E588 /* ExtensionPointDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointDetailViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -1312,7 +1295,6 @@
|
||||
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
||||
51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL-Extensions.swift"; sourceTree = "<group>"; };
|
||||
51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterEnterDetailTableViewController.swift; sourceTree = "<group>"; };
|
||||
51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuPreviewViewController.swift; sourceTree = "<group>"; };
|
||||
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard-Extensions.swift"; sourceTree = "<group>"; };
|
||||
51C45250226506F400C03939 /* String-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String-Extensions.swift"; sourceTree = "<group>"; };
|
||||
@@ -1838,17 +1820,6 @@
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
510289CE2451BA1E00426DDF /* Twitter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
510289CF2451BA3A00426DDF /* TwitterAdd.storyboard */,
|
||||
510289D12451BC1F00426DDF /* TwitterSelectTypeTableViewController.swift */,
|
||||
510289D52451DDD100426DDF /* TwitterSelectAccountTableViewController.swift */,
|
||||
51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */,
|
||||
);
|
||||
path = Twitter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
510C415D24E5CDE3008226FD /* ShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1869,7 +1840,6 @@
|
||||
515A50E5243D07A90089E588 /* ExtensionPointManager.swift */,
|
||||
84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */,
|
||||
84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */,
|
||||
515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */,
|
||||
5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */,
|
||||
);
|
||||
path = ExtensionPoints;
|
||||
@@ -2178,7 +2148,6 @@
|
||||
510289CC24519A1D00426DDF /* SelectComboTableViewCell.swift */,
|
||||
51E36E8B239D6765006F47A5 /* AddFeedSelectFolderTableViewCell.xib */,
|
||||
516AE5DD246AF2DD00731738 /* Reddit */,
|
||||
510289CE2451BA1E00426DDF /* Twitter */,
|
||||
);
|
||||
path = Add;
|
||||
sourceTree = "<group>";
|
||||
@@ -2399,8 +2368,6 @@
|
||||
51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */,
|
||||
51333D392468615D00EB5C91 /* AddRedditFeedSheet.xib */,
|
||||
51333D1524685D2E00EB5C91 /* AddRedditFeedWindowController.swift */,
|
||||
514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */,
|
||||
514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */,
|
||||
848363002262A3BC00DA1D35 /* AddWebFeedSheet.xib */,
|
||||
849A97521ED9EAC0007D329B /* AddWebFeedWindowController.swift */,
|
||||
51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */,
|
||||
@@ -3355,9 +3322,7 @@
|
||||
};
|
||||
840D617B2029031C009BC708 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
LastSwiftMigration = 1250;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 1;
|
||||
@@ -3510,7 +3475,6 @@
|
||||
65ED405C235DEF6C0081F399 /* ImportOPMLSheet.xib in Resources */,
|
||||
51DEE81926FBFF84006DAA56 /* Promenade.nnwtheme in Resources */,
|
||||
65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */,
|
||||
514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */,
|
||||
51D0214726ED617100FF2E0F /* core.css in Resources */,
|
||||
DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */,
|
||||
5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */,
|
||||
@@ -3564,7 +3528,11 @@
|
||||
511D43D2231FA62C00FB1562 /* GlobalKeyboardShortcuts.plist in Resources */,
|
||||
84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */,
|
||||
51BB7C312335ACDE008E8144 /* page.html in Resources */,
|
||||
<<<<<<< HEAD
|
||||
512392C324E3451400F11704 /* TwitterAdd.storyboard in Resources */,
|
||||
=======
|
||||
516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */,
|
||||
>>>>>>> main
|
||||
51077C5A27A86D16000C71DB /* Hyperlegible.nnwtheme in Resources */,
|
||||
DDF9E1D928EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */,
|
||||
51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */,
|
||||
@@ -3630,7 +3598,6 @@
|
||||
49F40DF82335B71000552BF4 /* newsfoot.js in Resources */,
|
||||
51333D3B2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */,
|
||||
BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */,
|
||||
514A89A2244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */,
|
||||
5103A9982421643300410853 /* blank.html in Resources */,
|
||||
B20180AB28E3B76F0059686A /* Localizable.stringsdict in Resources */,
|
||||
515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */,
|
||||
@@ -3673,7 +3640,7 @@
|
||||
};
|
||||
515D50802326D02600EE1167 /* Run Script: Verify No Build Settings */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 8;
|
||||
buildActionMask = 12;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
@@ -3685,12 +3652,13 @@
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 1;
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 buildscripts/VerifyNoBS.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
|
||||
shellScript = "xcrun -sdk macosx swift buildscripts/VerifyNoBS.swift --xcode ${PROJECT_DIR}/${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
|
||||
};
|
||||
5170CA5A279E468000702605 /* Delete Unnecessary Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
@@ -4021,7 +3989,6 @@
|
||||
65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */,
|
||||
518C3194237B00DA004D740F /* DetailIconSchemeHandler.swift in Sources */,
|
||||
65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */,
|
||||
514A89A6244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */,
|
||||
65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */,
|
||||
653813182680E152007A082C /* AccountType+Helpers.swift in Sources */,
|
||||
65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */,
|
||||
@@ -4097,7 +4064,6 @@
|
||||
65ED4028235DEF6C0081F399 /* ExtractedArticle.swift in Sources */,
|
||||
65ED4029235DEF6C0081F399 /* DeleteCommand.swift in Sources */,
|
||||
65ED402A235DEF6C0081F399 /* AddWebFeedWindowController.swift in Sources */,
|
||||
515A5108243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */,
|
||||
65ED402B235DEF6C0081F399 /* ImportOPMLWindowController.swift in Sources */,
|
||||
65ED402C235DEF6C0081F399 /* TimelineTableView.swift in Sources */,
|
||||
178A9F9E2549449F00AB7E9D /* AddAccountsView.swift in Sources */,
|
||||
@@ -4224,7 +4190,6 @@
|
||||
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
|
||||
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */,
|
||||
176813E92564BAE200D98635 /* WidgetDeepLinks.swift in Sources */,
|
||||
512392C424E3451400F11704 /* TwitterSelectTypeTableViewController.swift in Sources */,
|
||||
51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */,
|
||||
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
|
||||
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
|
||||
@@ -4267,19 +4232,16 @@
|
||||
DFB34997294C4DCB00BC81AD /* LocalizedNetNewsWireError.swift in Sources */,
|
||||
51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
|
||||
51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */,
|
||||
512392C524E3451400F11704 /* TwitterEnterDetailTableViewController.swift in Sources */,
|
||||
512AF9C2236ED52C0066F8BE /* ImageHeaderView.swift in Sources */,
|
||||
512392C124E33A3C00F11704 /* RedditSelectTypeTableViewController.swift in Sources */,
|
||||
515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */,
|
||||
DF766FED29377FD9006FBBE2 /* ExtensionsManagementView.swift in Sources */,
|
||||
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */,
|
||||
512392C624E3451400F11704 /* TwitterSelectAccountTableViewController.swift in Sources */,
|
||||
51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */,
|
||||
51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */,
|
||||
512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */,
|
||||
DFD406FC291FB63B00C02962 /* SettingsHelpSheets.swift in Sources */,
|
||||
51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */,
|
||||
515A5180243E90260089E588 /* TwitterFeedProvider-Extensions.swift in Sources */,
|
||||
51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */,
|
||||
51EF0F7E2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift in Sources */,
|
||||
512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */,
|
||||
@@ -4370,7 +4332,6 @@
|
||||
849C78922362AB04009A71E4 /* ExportOPMLWindowController.swift in Sources */,
|
||||
84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */,
|
||||
849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */,
|
||||
514A89A5244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */,
|
||||
849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */,
|
||||
5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */,
|
||||
8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */,
|
||||
@@ -4437,7 +4398,6 @@
|
||||
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
|
||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
|
||||
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
|
||||
515A5107243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */,
|
||||
845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */,
|
||||
DFBB4EAC2951BC0200639228 /* NNWThemeDocument.swift in Sources */,
|
||||
DFC14F1228EA5DC500F6EE86 /* AboutData.swift in Sources */,
|
||||
@@ -4659,14 +4619,6 @@
|
||||
name = MainInterface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
514A8980244FD63F0085E65D /* Base */,
|
||||
);
|
||||
name = AddTwitterFeedSheet.xib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0830"
|
||||
version = "1.7">
|
||||
version = "1.8">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -45,15 +45,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "849C645F1ED37A5D003D8FC0"
|
||||
BuildableName = "NetNewsWire.app"
|
||||
BlueprintName = "NetNewsWire"
|
||||
ReferencedContainer = "container:NetNewsWire.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
|
||||
@@ -12,8 +12,6 @@ public protocol SecretsProvider {
|
||||
var mercuryClientSecret: String { get }
|
||||
var feedlyClientId: String { get }
|
||||
var feedlyClientSecret: String { get }
|
||||
var twitterConsumerKey: String { get }
|
||||
var twitterConsumerSecret: String { get }
|
||||
var redditConsumerKey: String { get }
|
||||
var inoreaderAppId: String { get }
|
||||
var inoreaderAppKey: String { get }
|
||||
|
||||
@@ -286,30 +286,6 @@ blockquote {
|
||||
border-top: 1px solid var(--header-table-border-color);
|
||||
}
|
||||
|
||||
/* Twitter */
|
||||
|
||||
.twitterAvatar {
|
||||
vertical-align: middle;
|
||||
border-radius: 4px;
|
||||
height: 1.7em;
|
||||
width: 1.7em;
|
||||
}
|
||||
|
||||
.twitterUsername {
|
||||
line-height: 1.2;
|
||||
margin-left: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.twitterScreenName {
|
||||
font-size: 66%;
|
||||
}
|
||||
|
||||
.twitterTimestamp {
|
||||
font-size: 66%;
|
||||
}
|
||||
|
||||
/* Newsfoot theme for light mode (default) */
|
||||
.newsfoot-footnote-popover {
|
||||
background: #ccc;
|
||||
|
||||
@@ -20,7 +20,7 @@ struct ArticleTheme: Equatable {
|
||||
static let defaultTheme = ArticleTheme()
|
||||
static let nnwThemeSuffix = ".nnwtheme"
|
||||
|
||||
private static let defaultThemeName = NSLocalizedString("Default", comment: "Default")
|
||||
private static let defaultThemeName = "NetNewsWire"
|
||||
private static let unknownValue = NSLocalizedString("Unknown", comment: "Unknown Value")
|
||||
|
||||
let path: String?
|
||||
|
||||
@@ -77,6 +77,10 @@ public class ArticleThemeDownloader: Logging {
|
||||
private func findThemeFile(in searchPath: String) -> String? {
|
||||
if let directoryContents = FileManager.default.enumerator(atPath: searchPath) {
|
||||
while let file = directoryContents.nextObject() as? String {
|
||||
if file.hasPrefix("__MACOSX/") {
|
||||
logger.debug("Ignoring theme file in __MACOSX folder.")
|
||||
continue
|
||||
}
|
||||
if file.hasSuffix(".nnwtheme") {
|
||||
return file
|
||||
}
|
||||
|
||||
@@ -80,11 +80,13 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
}
|
||||
|
||||
func presentedSubitemDidChange(at url: URL) {
|
||||
themeNames = buildThemeNames()
|
||||
do {
|
||||
currentTheme = try articleThemeWithThemeName(currentThemeName)
|
||||
} catch {
|
||||
appDelegate.presentThemeImportError(error)
|
||||
if url.lastPathComponent.localizedCaseInsensitiveContains("nnwtheme") {
|
||||
themeNames = buildThemeNames()
|
||||
do {
|
||||
currentTheme = try articleThemeWithThemeName(currentThemeName)
|
||||
} catch {
|
||||
appDelegate.presentThemeImportError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ enum ExtensionPointIdentifer: Hashable {
|
||||
case marsEdit
|
||||
case microblog
|
||||
#endif
|
||||
case twitter(String)
|
||||
case reddit(String)
|
||||
|
||||
var extensionPointType: ExtensionPoint.Type {
|
||||
@@ -26,8 +25,6 @@ enum ExtensionPointIdentifer: Hashable {
|
||||
case .microblog:
|
||||
return SendToMicroBlogCommand.self
|
||||
#endif
|
||||
case .twitter:
|
||||
return TwitterFeedProvider.self
|
||||
case .reddit:
|
||||
return RedditFeedProvider.self
|
||||
}
|
||||
@@ -45,11 +42,6 @@ enum ExtensionPointIdentifer: Hashable {
|
||||
"type": "microblog"
|
||||
]
|
||||
#endif
|
||||
case .twitter(let screenName):
|
||||
return [
|
||||
"type": "twitter",
|
||||
"screenName": screenName
|
||||
]
|
||||
case .reddit(let username):
|
||||
return [
|
||||
"type": "reddit",
|
||||
@@ -68,9 +60,6 @@ enum ExtensionPointIdentifer: Hashable {
|
||||
case "microblog":
|
||||
self = ExtensionPointIdentifer.microblog
|
||||
#endif
|
||||
case "twitter":
|
||||
guard let screenName = userInfo["screenName"] as? String else { return nil }
|
||||
self = ExtensionPointIdentifer.twitter(screenName)
|
||||
case "reddit":
|
||||
guard let username = userInfo["username"] as? String else { return nil }
|
||||
self = ExtensionPointIdentifer.reddit(username)
|
||||
@@ -87,9 +76,6 @@ enum ExtensionPointIdentifer: Hashable {
|
||||
case .microblog:
|
||||
hasher.combine("microblog")
|
||||
#endif
|
||||
case .twitter(let screenName):
|
||||
hasher.combine("twitter")
|
||||
hasher.combine(screenName)
|
||||
case .reddit(let username):
|
||||
hasher.combine("reddit")
|
||||
hasher.combine(username)
|
||||
|
||||
@@ -69,16 +69,12 @@ final class ExtensionPointManager: FeedProviderManagerDelegate {
|
||||
return activeExtensionPoints.values.compactMap({ return $0 as? FeedProvider })
|
||||
}
|
||||
|
||||
var isTwitterEnabled: Bool {
|
||||
return activeExtensionPoints.values.contains(where: { $0 is TwitterFeedProvider })
|
||||
}
|
||||
|
||||
var isRedditEnabled: Bool {
|
||||
return activeExtensionPoints.values.contains(where: { $0 is RedditFeedProvider })
|
||||
}
|
||||
|
||||
init() {
|
||||
possibleExtensionPointTypes = [TwitterFeedProvider.self, RedditFeedProvider.self]
|
||||
possibleExtensionPointTypes = [RedditFeedProvider.self]
|
||||
loadExtensionPoints()
|
||||
}
|
||||
|
||||
@@ -121,12 +117,6 @@ private extension ExtensionPointManager {
|
||||
|
||||
func extensionPoint(for extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess?, completion: @escaping (Result<ExtensionPoint, Error>) -> Void) {
|
||||
switch extensionPointType {
|
||||
case is TwitterFeedProvider.Type:
|
||||
if let tokenSuccess = tokenSuccess, let twitter = TwitterFeedProvider(tokenSuccess: tokenSuccess) {
|
||||
completion(.success(twitter))
|
||||
} else {
|
||||
completion(.failure(ExtensionPointManagerError.unableToCreate))
|
||||
}
|
||||
case is RedditFeedProvider.Type:
|
||||
if let tokenSuccess = tokenSuccess {
|
||||
RedditFeedProvider.create(tokenSuccess: tokenSuccess) { result in
|
||||
@@ -147,8 +137,6 @@ private extension ExtensionPointManager {
|
||||
|
||||
func extensionPoint(for extensionPointID: ExtensionPointIdentifer) -> ExtensionPoint? {
|
||||
switch extensionPointID {
|
||||
case .twitter(let screenName):
|
||||
return TwitterFeedProvider(screenName: screenName)
|
||||
case .reddit(let username):
|
||||
return RedditFeedProvider(username: username)
|
||||
#if os(macOS)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<title>Default Feeds</title>
|
||||
</head>
|
||||
<body>
|
||||
<<<<<<< HEAD
|
||||
<outline text="Colossal" title="Colossal" type="rss" version="RSS" htmlUrl="https://www.thisiscolossal.com/" xmlUrl="https://www.thisiscolossal.com/feed/"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
@@ -21,5 +22,17 @@
|
||||
<outline text="Craig Hockenberry" title="Craig Hockenberry" type="rss" version="RSS" htmlUrl="https://furbo.org/" xmlUrl="https://furbo.org/feed/json"/>
|
||||
<outline text="Rose Orchard" title="Rose Orchard" type="rss" version="RSS" htmlUrl="https://rosemaryorchard.com/" xmlUrl="https://rosemaryorchard.com/feed.xml"/>
|
||||
<outline text="Michael Tsai" title="Michael Tsai" type="rss" version="RSS" htmlUrl="https://mjtsai.com/blog/" xmlUrl="https://mjtsai.com/blog/feed/"/>
|
||||
=======
|
||||
<outline text="BBC News - World" title="BBC News - World" type="rss" version="RSS" htmlUrl="https://www.bbc.com/news" xmlUrl="https://feeds.bbci.co.uk/news/world/rss.xml"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="Colossal" title="Colossal" type="rss" version="RSS" htmlUrl="https://www.thisiscolossal.com/" xmlUrl="https://www.thisiscolossal.com/feed/"/>
|
||||
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
|
||||
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
||||
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://nnw.ranchero.com/" xmlUrl="https://nnw.ranchero.com/feed.json"/>
|
||||
<outline text="One Foot Tsunami" title="One Foot Tsunami" type="rss" version="RSS" htmlUrl="https://onefoottsunami.com/" xmlUrl="https://onefoottsunami.com/feed/json/"/>
|
||||
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://feedpress.me/sixcolors?type=xml"/>
|
||||
>>>>>>> ios-release
|
||||
</body>
|
||||
</opml>
|
||||
|
||||
@@ -3,24 +3,26 @@
|
||||
body {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
word-wrap: break-word;
|
||||
max-width: 44em;
|
||||
background-color: #FBF0D9;
|
||||
color: #704214;
|
||||
background-color: rgb(248, 241, 227);
|
||||
color: rgb(79, 50, 28);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-shadow: 0 1px rgba(255, 255, 255, 2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.feedlink {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.headerTable {
|
||||
width: 100%;
|
||||
height: 68px;
|
||||
#nnwImageIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.systemMessage {
|
||||
@@ -37,34 +39,48 @@ a:hover {
|
||||
--header-color: rgba(0, 0, 0, 0.5);
|
||||
--body-code-color: #704214;
|
||||
--system-message-color: #704214;
|
||||
--feedlink-color: rgba(255, 0, 0, 0.6);
|
||||
--feedlink-color: #704214;
|
||||
--article-title-color: #704214;
|
||||
--article-date-color: rgba(0, 0, 0, 0.5);
|
||||
--table-cell-border-color: lightgray;
|
||||
--primary-accent-color: #43350E;
|
||||
--secondary-accent-color: #43350E;
|
||||
--primary-accent-color: #43350e;
|
||||
--secondary-accent-color: #43350e;
|
||||
--block-quote-border-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body a, body a:visited, body a * {
|
||||
body a,
|
||||
body a:visited,
|
||||
body a * {
|
||||
color: var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
|
||||
body .headerTable {
|
||||
body > header {
|
||||
border-bottom: 1px solid var(--header-table-border-color);
|
||||
color: var(--header-color);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
body .header {
|
||||
body > header a,
|
||||
body > header a:link,
|
||||
body > header a:visited {
|
||||
color: var(--header-color);
|
||||
}
|
||||
|
||||
body .header a:link, .header a:visited {
|
||||
body > header .headerTable {
|
||||
width: 100%;
|
||||
}
|
||||
body > header .headerTable td,
|
||||
body > header .headerTable th {
|
||||
color: var(--header-color);
|
||||
padding: 0.2em;
|
||||
border: none;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
body > header .headerTable td.avatar {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
body code, body pre {
|
||||
body code,
|
||||
body pre {
|
||||
color: var(--body-code-color);
|
||||
}
|
||||
|
||||
@@ -72,15 +88,11 @@ body > .systemMessage {
|
||||
color: var(--system-message-color);
|
||||
}
|
||||
|
||||
.headerContainer a:link, .headerContainer a:visited {
|
||||
text-decoration: none;
|
||||
.headerContainer a:link,
|
||||
.headerContainer a:visited {
|
||||
color: var(--feedlink-color);
|
||||
}
|
||||
|
||||
.headerContainer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -97,44 +109,35 @@ body > .systemMessage {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.articleTitle a:link, .articleTitle a:visited {
|
||||
.articleTitle a:link,
|
||||
.articleTitle a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--article-title-color);
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.articleTitle a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.articleDateline {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.articleDateline a:link, .articleDateline a:visited {
|
||||
.articleDateline a:link,
|
||||
.articleDateline a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--article-date-color);
|
||||
}
|
||||
|
||||
.articleDateline a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.articleDatelineTitle {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.articleDatelineTitle a:link, .articleDatelineTitle a:visited {
|
||||
.articleDatelineTitle a:link,
|
||||
.articleDatelineTitle a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--article-title-color);
|
||||
}
|
||||
|
||||
.articleDatelineTitle a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.externalLink {
|
||||
margin-bottom: 5px;
|
||||
font-style: italic;
|
||||
@@ -144,14 +147,11 @@ body > .systemMessage {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.externalLink a:link, .externalLink a:visited {
|
||||
.externalLink a:link,
|
||||
.externalLink a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.externalLink a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.articleBody {
|
||||
margin-top: 20px;
|
||||
line-height: 1.6em;
|
||||
@@ -177,17 +177,14 @@ pre {
|
||||
line-height: 1.4286em;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
code,
|
||||
pre {
|
||||
font-family: "SF Mono", Menlo, "Courier New", Courier, monospace;
|
||||
font-size: 1em;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: -0.027em;
|
||||
-webkit-hyphens: none;
|
||||
}
|
||||
|
||||
pre code {
|
||||
letter-spacing: -.027em;
|
||||
font-size: 0.9375em;
|
||||
}
|
||||
|
||||
.nnw-overflow {
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -209,7 +206,8 @@ pre code {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nnw-overflow td, .nnw-overflow th {
|
||||
.nnw-overflow td,
|
||||
.nnw-overflow th {
|
||||
-webkit-hyphens: none;
|
||||
word-break: normal;
|
||||
border: 1px solid var(--table-cell-border-color);
|
||||
@@ -222,7 +220,10 @@ pre code {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.nnw-overflow :matches(thead, tbody, tfoot):last-child > tr:last-child :matches(td, th) {
|
||||
.nnw-overflow
|
||||
:matches(thead, tbody, tfoot):last-child
|
||||
> tr:last-child
|
||||
:matches(td, th) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@@ -235,16 +236,16 @@ pre code {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
img, figure, video, div, object {
|
||||
img,
|
||||
figure,
|
||||
video,
|
||||
div,
|
||||
object {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
@@ -340,12 +341,12 @@ blockquote {
|
||||
}
|
||||
|
||||
.newsfoot-footnote-popover-arrow {
|
||||
background: #FBF0D9;
|
||||
background: #fbf0d9;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.newsfoot-footnote-popover-inner {
|
||||
background: #FBF0D9;
|
||||
background: #fbf0d9;
|
||||
}
|
||||
|
||||
body a.footnote,
|
||||
@@ -364,7 +365,6 @@ a.footnote:hover,
|
||||
|
||||
/* iOS Specific */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
|
||||
body {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 20px;
|
||||
@@ -374,7 +374,7 @@ a.footnote:hover,
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-webkit-text-size-adjust: none;
|
||||
font: Georgia;
|
||||
font-family: Charter, Georgia, sans-serif;
|
||||
font-size: [[font-size]]px;
|
||||
}
|
||||
|
||||
@@ -386,18 +386,16 @@ a.footnote:hover,
|
||||
.nnw-overflow table {
|
||||
border: 1px solid var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* macOS Specific */
|
||||
@supports not (-webkit-touch-callout: none) {
|
||||
|
||||
body {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 64px;
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
font-family: Georgia;
|
||||
font-family: Charter, Georgia, sans-serif;
|
||||
}
|
||||
|
||||
.smallText {
|
||||
@@ -428,5 +426,4 @@ a.footnote:hover,
|
||||
.nnw-overflow table {
|
||||
border: 1px solid var(--primary-accent-color);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<header class="headerContainer">
|
||||
<table cellpadding=0 cellspacing=0 border=0 class="headerTable">
|
||||
<table class="headerTable">
|
||||
<tr>
|
||||
<td class="header leftAlign"><a class="feedlink" href="[[feed_link]]">[[feed_link_title]]</a><br />[[byline]]</td>
|
||||
<td class="header rightAlign avatar"><img id="nnwImageIcon" src="[[avatar_src]]" height=48 width=48 /></td>
|
||||
<td class="leftAlign"><a class="feedlink" href="[[feed_link]]">[[feed_link_title]]</a><br />[[byline]]</td>
|
||||
<td class="rightAlign avatar"><img id="nnwImageIcon" src="[[avatar_src]]" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</header>
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
%{
|
||||
import os
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
=======
|
||||
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
>>>>>>> mac-release
|
||||
=======
|
||||
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
>>>>>>> ios-release
|
||||
|
||||
def chunks(seq, size):
|
||||
return (seq[i:(i + size)] for i in range(0, len(seq), size))
|
||||
|
||||
@@ -35,5 +35,10 @@ struct SearchFeedDelegate: SmartFeedDelegate {
|
||||
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
// TODO: after 5.0
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
fatalError("Function not implemented.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -35,4 +35,9 @@ struct SearchTimelineFeedDelegate: SmartFeedDelegate {
|
||||
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
// TODO: after 5.0
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
fatalError("Function not implemented.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ final class SmartFeed: PseudoFeed {
|
||||
}
|
||||
#endif
|
||||
|
||||
private let delegate: SmartFeedDelegate
|
||||
public let delegate: SmartFeedDelegate
|
||||
private var unreadCounts = [String: Int]()
|
||||
|
||||
init(delegate: SmartFeedDelegate) {
|
||||
@@ -95,6 +95,10 @@ extension SmartFeed: ArticleFetcher {
|
||||
return try delegate.fetchUnreadArticles()
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
return try delegate.fetchUnreadArticlesBetween(before: before, after: after)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
delegate.fetchUnreadArticlesAsync(completion)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ extension SmartFeedDelegate {
|
||||
return try fetchArticles().unreadArticles()
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
fetchArticlesAsync{ articleSetResult in
|
||||
switch articleSetResult {
|
||||
|
||||
@@ -29,4 +29,8 @@ struct StarredFeedDelegate: SmartFeedDelegate {
|
||||
func fetchUnreadCount(for account: Account, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||
account.fetchUnreadCountForStarredArticles(completion)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after).filter({ $0.status.starred })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,10 @@ extension UnreadFeed: ArticleFetcher {
|
||||
return try AccountManager.shared.fetchArticles(fetchType)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set<Article> {
|
||||
return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after)
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) {
|
||||
AccountManager.shared.fetchArticlesAsync(fetchType, completion)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public final class WidgetDataEncoder {
|
||||
|
||||
@available(iOS 14, *)
|
||||
private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) {
|
||||
var dispatchGroup = DispatchGroup()
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var groupError: Error? = nil
|
||||
|
||||
var unread = [LatestArticle]()
|
||||
@@ -143,9 +143,9 @@ public final class WidgetDataEncoder {
|
||||
let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount,
|
||||
currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount,
|
||||
currentStarredCount: (try? AccountManager.shared.fetchCountForStarredArticles()) ?? 0,
|
||||
unreadArticles: unread,
|
||||
starredArticles: starred,
|
||||
todayArticles:today,
|
||||
unreadArticles: unread.sorted(by: { $0.pubDate > $1.pubDate }),
|
||||
starredArticles: starred.sorted(by: { $0.pubDate > $1.pubDate }),
|
||||
todayArticles:today.sorted(by: { $0.pubDate > $1.pubDate }),
|
||||
lastUpdateTime: Date())
|
||||
completion(latestData)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ RSDatabase uses FMDB for SQLite persistence.
|
||||
Required by OAuthSwift as a testing dependency. Not shipped in NNW.
|
||||
|
||||
## [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift)
|
||||
Our Reddit and Twitter use the OAuth framework to authenticate with the services
|
||||
Our Reddit integration uses the OAuth framework to authenticate with the services
|
||||
and then service requests to them.
|
||||
|
||||
## [PLCrashReporter](https://github.com/microsoft/plcrashreporter)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Logs
|
||||
|
||||
`RSCore` contains a protocol called `Logging`. Classes and Structs that conform to `Logging` have a `logger` variable that the Class or Struct can use instead of importing `os.log` and creating a `var log = Logger(..)` variable.
|
||||
`RSCore` contains a protocol called `Logging`. Classes and structs that conform to `Logging` have a `logger` variable that the Class or Struct can use instead of importing `os.log` and creating a `var log = Logger(..)` variable.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Mac Release Notes
|
||||
|
||||
### 6.1.1b2 build 6108 5 Feb 2023
|
||||
|
||||
Remove Twitter integration. On first launch, for people with Twitter feeds, display an alert explaining what happened
|
||||
Fix a crashing bug that could happen in the sidebar
|
||||
|
||||
### 6.1.1b1 build 6107 3 Nov 2022
|
||||
|
||||
Fixed a bug that could prevent users from accessing BazQux if an article was missing a field
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# iOS Release Notes
|
||||
|
||||
### 6.1.1 TestFlight build 6114 - 5 Feb 2023
|
||||
|
||||
Remove Twitter integration. Include alert that Twitter integration was removed.
|
||||
|
||||
### 6.1.1 TestFlight build 6113 - 22 Jan 2023
|
||||
|
||||
Fix a crashing bug when fetching data for the widget
|
||||
|
||||
### 6.1.1 TestFlight build 6112 - 16 Jan 2023
|
||||
|
||||
Add some feeds back to defaults — now an even 10 feeds
|
||||
|
||||
### 6.1.1 TestFlight build 6111 - 8 Jan 2023 (didn’t actually go out via TestFlight)
|
||||
|
||||
Fixed a crashing bug in the Feeds screen
|
||||
Cut way down on number of default feeds, added BBC World News
|
||||
|
||||
### 6.1 Release build 6110 - 9 Nov 2022
|
||||
|
||||
Changes since 6.0.1…
|
||||
|
||||
@@ -37,7 +37,7 @@ struct StarredWidgetView : View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment:.leading, spacing: 0) {
|
||||
ForEach(0..<maxCount(), content: { i in
|
||||
ForEach(0..<maxCount(), id: \.self, content: { i in
|
||||
if i != 0 {
|
||||
Divider()
|
||||
ArticleItemView(article: entry.widgetData.starredArticles[i],
|
||||
|
||||
@@ -37,7 +37,7 @@ struct TodayWidgetView : View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment:.leading, spacing: 0) {
|
||||
ForEach(0..<maxCount(), content: { i in
|
||||
ForEach(0..<maxCount(), id: \.self, content: { i in
|
||||
if i != 0 {
|
||||
Divider()
|
||||
ArticleItemView(article: entry.widgetData.todayArticles[i],
|
||||
|
||||
@@ -37,7 +37,7 @@ struct UnreadWidgetView : View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment:.leading, spacing: 0) {
|
||||
ForEach(0..<maxCount(), content: { i in
|
||||
ForEach(0..<maxCount(), id: \.self, content: { i in
|
||||
if i != 0 {
|
||||
Divider()
|
||||
ArticleItemView(article: entry.widgetData.unreadArticles[i],
|
||||
|
||||
@@ -15,7 +15,6 @@ import RSParser
|
||||
enum AddFeedType {
|
||||
case web
|
||||
case reddit
|
||||
case twitter
|
||||
}
|
||||
|
||||
class AddFeedViewController: UITableViewController {
|
||||
@@ -44,9 +43,6 @@ class AddFeedViewController: UITableViewController {
|
||||
case .reddit:
|
||||
navigationItem.title = NSLocalizedString("Add Reddit Feed", comment: "Add Reddit Feed")
|
||||
navigationItem.leftBarButtonItem = nil
|
||||
case .twitter:
|
||||
navigationItem.title = NSLocalizedString("Add Twitter Feed", comment: "Add Twitter Feed")
|
||||
navigationItem.leftBarButtonItem = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="4Q4-Hi-Lic">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="np9-bP-Yhx">
|
||||
<objects>
|
||||
<navigationController id="4Q4-Hi-Lic" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="9e6-4b-IPV">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="q78-0w-suH" kind="relationship" relationship="rootViewController" id="xn5-zT-uXK"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="sjJ-Fz-BXf" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1254" y="173"/>
|
||||
</scene>
|
||||
<!--Select Type-->
|
||||
<scene sceneID="Fmm-TL-h7h">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="TwitterSelectTypeTableViewController" title="Select Type" id="q78-0w-suH" customClass="TwitterSelectTypeTableViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="SFq-R0-gSo">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<sections>
|
||||
<tableViewSection id="Dp6-La-NeL">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="WAs-zr-8RJ" detailTextLabel="f8o-SY-a2H" style="IBUITableViewCellStyleSubtitle" id="VbH-aQ-M4H" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="18" width="374" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VbH-aQ-M4H" id="Qud-m5-1ZQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Home Timeline" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="WAs-zr-8RJ">
|
||||
<rect key="frame" x="20" y="9" width="114" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Tweets from everyone you follow" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="f8o-SY-a2H">
|
||||
<rect key="frame" x="20" y="32.5" width="198.5" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="K0b-HX-8dR" detailTextLabel="uaZ-4Q-FBS" style="IBUITableViewCellStyleSubtitle" id="jft-wJ-OVX" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="77" width="374" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="jft-wJ-OVX" id="dXF-Bc-NkR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Mentions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="K0b-HX-8dR">
|
||||
<rect key="frame" x="20" y="9" width="70.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Tweets mentioning you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="uaZ-4Q-FBS">
|
||||
<rect key="frame" x="20" y="32.5" width="140" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Oqx-Ox-oyD" detailTextLabel="lqE-9F-jud" style="IBUITableViewCellStyleSubtitle" id="8qx-8E-cJo" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="136" width="374" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8qx-8E-cJo" id="bk5-cB-EOT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Screen Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="Oqx-Ox-oyD">
|
||||
<rect key="frame" x="20" y="9" width="102.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Tweets from another Twitter user" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="lqE-9F-jud">
|
||||
<rect key="frame" x="20" y="32.5" width="201" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Xj8-CU-C7D" detailTextLabel="SJg-RF-48S" style="IBUITableViewCellStyleSubtitle" id="vrw-y9-yJd" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="195" width="374" height="58"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vrw-y9-yJd" id="bOx-t8-zfi">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="58"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Search" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="Xj8-CU-C7D">
|
||||
<rect key="frame" x="20" y="9" width="53.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Tweets containing a #hastag or search term" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="SJg-RF-48S">
|
||||
<rect key="frame" x="20" y="32.5" width="248" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="q78-0w-suH" id="fIb-JL-htF"/>
|
||||
<outlet property="delegate" destination="q78-0w-suH" id="j69-O6-ths"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Select Twitter Type" id="kjr-7P-QSh">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="xkx-QM-tXd">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="q78-0w-suH" id="3LG-Q8-Aqh"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yI5-IG-7Sl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-538" y="174"/>
|
||||
</scene>
|
||||
<!--Select Account-->
|
||||
<scene sceneID="rKM-ZF-73N">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="TwitterSelectAccountTableViewController" title="Select Account" id="2vd-nT-5dg" customClass="TwitterSelectAccountTableViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="T93-wO-GIE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="j8c-JM-nzm" style="IBUITableViewCellStyleDefault" id="vEE-Gx-Zgc" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="55.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vEE-Gx-Zgc" id="pa0-mR-hgR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="j8c-JM-nzm">
|
||||
<rect key="frame" x="20" y="0.0" width="315" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="2vd-nT-5dg" id="GvE-oh-4gy"/>
|
||||
<outlet property="delegate" destination="2vd-nT-5dg" id="hdE-2N-0X0"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="LMf-ZZ-Z1s" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="155" y="173"/>
|
||||
</scene>
|
||||
<!--Enter Detail-->
|
||||
<scene sceneID="lmR-Pm-7vI">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="TwitterEnterDetailTableViewController" title="Enter Detail" id="Eh0-p4-hVX" customClass="TwitterEnterDetailTableViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="76O-el-2DO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<sections>
|
||||
<tableViewSection id="ZkR-cP-Kvy">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="bWA-Rs-IL9">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bWA-Rs-IL9" id="azg-eE-bd4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="IVd-Bz-j7J">
|
||||
<rect key="frame" x="20" y="11" width="334" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="IVd-Bz-j7J" firstAttribute="centerY" secondItem="azg-eE-bd4" secondAttribute="centerY" id="ItM-Jv-j2G"/>
|
||||
<constraint firstItem="IVd-Bz-j7J" firstAttribute="leading" secondItem="azg-eE-bd4" secondAttribute="leading" constant="20" symbolic="YES" id="ldz-j4-8kY"/>
|
||||
<constraint firstAttribute="trailing" secondItem="IVd-Bz-j7J" secondAttribute="trailing" constant="20" symbolic="YES" id="un0-pU-AHV"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Eh0-p4-hVX" id="eKE-xW-f3D"/>
|
||||
<outlet property="delegate" destination="Eh0-p4-hVX" id="e07-6Q-SQc"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="detailTextField" destination="IVd-Bz-j7J" id="p1f-3d-6MR"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cp9-xU-RGq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="836" y="173"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemGroupedBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// TwitterEnterDetailTableViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/23/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
class TwitterEnterDetailTableViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var detailTextField: UITextField!
|
||||
|
||||
var doneBarButtonItem = UIBarButtonItem()
|
||||
var twitterFeedType: TwitterFeedType?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
doneBarButtonItem.title = NSLocalizedString("Next", comment: "Next")
|
||||
doneBarButtonItem.style = .plain
|
||||
doneBarButtonItem.target = self
|
||||
doneBarButtonItem.action = #selector(done)
|
||||
navigationItem.rightBarButtonItem = doneBarButtonItem
|
||||
|
||||
if case .screenName = twitterFeedType {
|
||||
navigationItem.title = NSLocalizedString("Enter Name", comment: "Enter Name")
|
||||
detailTextField.placeholder = NSLocalizedString("Screen Name", comment: "Screen Name")
|
||||
} else {
|
||||
navigationItem.title = NSLocalizedString("Enter Search", comment: "Enter Search")
|
||||
detailTextField.placeholder = NSLocalizedString("Search Term or #hashtag", comment: "Search Term")
|
||||
}
|
||||
|
||||
detailTextField.delegate = self
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: detailTextField)
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@objc func done() {
|
||||
guard let twitterFeedType = twitterFeedType, var text = detailTextField.text?.collapsingWhitespace else { return }
|
||||
|
||||
let url: String?
|
||||
if twitterFeedType == .screenName {
|
||||
if text.starts(with: "@") {
|
||||
text = String(text[text.index(text.startIndex, offsetBy: 1)..<text.endIndex])
|
||||
}
|
||||
url = TwitterFeedProvider.buildURL(twitterFeedType, username: nil, screenName: text, searchField: nil)?.absoluteString
|
||||
} else {
|
||||
url = TwitterFeedProvider.buildURL(twitterFeedType, username: nil, screenName: nil, searchField: text)?.absoluteString
|
||||
}
|
||||
|
||||
let addViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedViewController") as! AddFeedViewController
|
||||
addViewController.addFeedType = .twitter
|
||||
addViewController.initialFeed = url
|
||||
navigationController?.pushViewController(addViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TwitterEnterDetailTableViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension TwitterEnterDetailTableViewController {
|
||||
|
||||
func updateUI() {
|
||||
doneBarButtonItem.isEnabled = !(detailTextField.text?.isEmpty ?? false)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// TwitterSelectAccountTableViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/23/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
class TwitterSelectAccountTableViewController: UITableViewController {
|
||||
|
||||
private var twitterFeedProviders = [TwitterFeedProvider]()
|
||||
|
||||
var twitterFeedType: TwitterFeedType?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
twitterFeedProviders = ExtensionPointManager.shared.activeExtensionPoints.values.compactMap { $0 as? TwitterFeedProvider }
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return twitterFeedProviders.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
cell.textLabel?.text = "@\(twitterFeedProviders[indexPath.row].screenName)"
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let twitterFeedType = twitterFeedType else { return }
|
||||
|
||||
let username = twitterFeedProviders[indexPath.row].screenName
|
||||
let url = TwitterFeedProvider.buildURL(twitterFeedType, username: username, screenName: nil, searchField: nil)?.absoluteString
|
||||
|
||||
let addViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedViewController") as! AddFeedViewController
|
||||
addViewController.addFeedType = .twitter
|
||||
addViewController.initialFeed = url
|
||||
navigationController?.pushViewController(addViewController, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// TwitterSelectTypeTableViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/23/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
class TwitterSelectTypeTableViewController: UITableViewController {
|
||||
|
||||
private var twitterFeedProviders = [TwitterFeedProvider]()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
twitterFeedProviders = ExtensionPointManager.shared.activeExtensionPoints.values.compactMap { $0 as? TwitterFeedProvider }
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
if indexPath.row < 2 {
|
||||
if twitterFeedProviders.count > 1 {
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
if twitterFeedProviders.count == 1 {
|
||||
let username = twitterFeedProviders.first!.screenName
|
||||
let url = TwitterFeedProvider.buildURL(.homeTimeline, username: username, screenName: nil, searchField: nil)?.absoluteString
|
||||
pushAddFeedController(url)
|
||||
} else {
|
||||
let selectAccount = UIStoryboard.twitterAdd.instantiateController(ofType: TwitterSelectAccountTableViewController.self)
|
||||
selectAccount.twitterFeedType = .homeTimeline
|
||||
navigationController?.pushViewController(selectAccount, animated: true)
|
||||
}
|
||||
case 1:
|
||||
if twitterFeedProviders.count == 1 {
|
||||
let username = twitterFeedProviders.first!.screenName
|
||||
let url = TwitterFeedProvider.buildURL(.mentions, username: username, screenName: nil, searchField: nil)?.absoluteString
|
||||
pushAddFeedController(url)
|
||||
} else {
|
||||
let selectAccount = UIStoryboard.twitterAdd.instantiateController(ofType: TwitterSelectAccountTableViewController.self)
|
||||
selectAccount.twitterFeedType = .mentions
|
||||
navigationController?.pushViewController(selectAccount, animated: true)
|
||||
}
|
||||
case 2:
|
||||
let enterDetail = UIStoryboard.twitterAdd.instantiateController(ofType: TwitterEnterDetailTableViewController.self)
|
||||
enterDetail.twitterFeedType = .screenName
|
||||
navigationController?.pushViewController(enterDetail, animated: true)
|
||||
case 3:
|
||||
let enterDetail = UIStoryboard.twitterAdd.instantiateController(ofType: TwitterEnterDetailTableViewController.self)
|
||||
enterDetail.twitterFeedType = .search
|
||||
navigationController?.pushViewController(enterDetail, animated: true)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension TwitterSelectTypeTableViewController {
|
||||
|
||||
func pushAddFeedController(_ url: String?) {
|
||||
let addViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddWebFeedViewController") as! AddFeedViewController
|
||||
addViewController.addFeedType = .twitter
|
||||
addViewController.initialFeed = url
|
||||
navigationController?.pushViewController(addViewController, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -113,10 +113,6 @@ struct AppAssets {
|
||||
return UIImage(named: "contextMenuReddit")!
|
||||
}()
|
||||
|
||||
static var contextMenuTwitter: UIImage = {
|
||||
return UIImage(named: "contextMenuTwitter")!
|
||||
}()
|
||||
|
||||
static var copyImage: UIImage = {
|
||||
return UIImage(systemName: "doc.on.doc")!
|
||||
}()
|
||||
@@ -133,10 +129,6 @@ struct AppAssets {
|
||||
return RSImage(named: "extensionPointReddit")!
|
||||
}()
|
||||
|
||||
static var extensionPointTwitter: UIImage = {
|
||||
return UIImage(named: "extensionPointTwitter")!
|
||||
}()
|
||||
|
||||
static var faviconTemplateImage: RSImage = {
|
||||
return RSImage(named: "faviconTemplateImage")!
|
||||
}()
|
||||
@@ -280,10 +272,6 @@ struct AppAssets {
|
||||
return UIImage(systemName: "trash")!
|
||||
}()
|
||||
|
||||
static var twitterOriginal: UIImage = {
|
||||
return UIImage(named: "twitterWhite")!.withRenderingMode(.alwaysOriginal).withTintColor(.secondaryLabel)
|
||||
}()
|
||||
|
||||
static var unreadFeedImage: IconImage {
|
||||
let image = UIImage(systemName: "largecircle.fill.circle")!
|
||||
return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor)
|
||||
|
||||
@@ -30,7 +30,7 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
|
||||
|
||||
final class AppDefaults: ObservableObject {
|
||||
|
||||
static let defaultThemeName = "Default"
|
||||
static let defaultThemeName = "NetNewsWire"
|
||||
|
||||
static let shared = AppDefaults()
|
||||
private init() {}
|
||||
@@ -60,6 +60,7 @@ final class AppDefaults: ObservableObject {
|
||||
static let addFolderAccountID = "addFolderAccountID"
|
||||
static let useSystemBrowser = "useSystemBrowser"
|
||||
static let currentThemeName = "currentThemeName"
|
||||
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
|
||||
}
|
||||
|
||||
let isDeveloperBuild: Bool = {
|
||||
@@ -256,6 +257,15 @@ final class AppDefaults: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
var twitterDeprecationAlertShown: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.twitterDeprecationAlertShown)
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.twitterDeprecationAlertShown, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
static func registerDefaults() {
|
||||
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
|
||||
Key.timelineGroupByFeed: false,
|
||||
|
||||
@@ -45,7 +45,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable, Loggi
|
||||
return button
|
||||
}()
|
||||
|
||||
var mainControllerIdentifer = MainControllerIdentifier.article
|
||||
var mainControllerIdentifier = MainControllerIdentifier.article
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
@@ -267,7 +267,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable, Loggi
|
||||
themeActions.append(action)
|
||||
}
|
||||
|
||||
let defaultThemeAction = UIAction(title: NSLocalizedString("Default", comment: "Default"),
|
||||
let defaultThemeAction = UIAction(title: "NetNewsWire",
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
|
||||
@@ -35,16 +35,24 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
||||
completion(.success(with: url))
|
||||
}
|
||||
|
||||
public func provideAccountNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
|
||||
public func resolveTitle(for intent: AddWebFeedIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
|
||||
guard let title = intent.title else {
|
||||
completion(INStringResolutionResult.notRequired())
|
||||
return
|
||||
}
|
||||
completion(.success(with: title))
|
||||
}
|
||||
|
||||
public func provideAccountNameOptionsCollection(for intent: AddWebFeedIntent, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) {
|
||||
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
|
||||
return
|
||||
}
|
||||
|
||||
let accountNames = extensionContainers.accounts.map { $0.name }
|
||||
completion(accountNames, nil)
|
||||
completion(INObjectCollection(items: accountNames as [NSString]), nil)
|
||||
}
|
||||
|
||||
|
||||
public func resolveAccountName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedAccountNameResolutionResult) -> Void) {
|
||||
guard let accountName = intent.accountName else {
|
||||
completion(AddWebFeedAccountNameResolutionResult.notRequired())
|
||||
@@ -78,6 +86,21 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
||||
completion(folderNames, nil)
|
||||
}
|
||||
|
||||
public func provideFolderNameOptionsCollection(for intent: AddWebFeedIntent, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) {
|
||||
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
|
||||
return
|
||||
}
|
||||
|
||||
guard let accountName = intent.accountName, let account = extensionContainers.findAccount(forName: accountName) else {
|
||||
completion(INObjectCollection(items: [NSString]()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
let folderNames = account.folders.map { $0.name }
|
||||
completion(INObjectCollection(items: folderNames as [NSString]), nil)
|
||||
}
|
||||
|
||||
public func resolveFolderName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedFolderNameResolutionResult) -> Void) {
|
||||
guard let accountName = intent.accountName, let folderName = intent.folderName else {
|
||||
completion(AddWebFeedFolderNameResolutionResult.notRequired())
|
||||
@@ -135,7 +158,7 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
||||
return
|
||||
}
|
||||
|
||||
let request = ExtensionFeedAddRequest(name: nil, feedURL: url, destinationContainerID: containerID)
|
||||
let request = ExtensionFeedAddRequest(name: intent.title, feedURL: url, destinationContainerID: containerID)
|
||||
ExtensionFeedAddRequestFile.save(request)
|
||||
completion(AddWebFeedIntentResponse(code: .success, userActivity: nil))
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<key>INEnums</key>
|
||||
<array/>
|
||||
<key>INIntentDefinitionModelVersion</key>
|
||||
<string>1.1</string>
|
||||
<string>1.2</string>
|
||||
<key>INIntentDefinitionNamespace</key>
|
||||
<string>U6u7RF</string>
|
||||
<key>INIntentDefinitionSystemVersion</key>
|
||||
<string>19D76</string>
|
||||
<string>22A400</string>
|
||||
<key>INIntentDefinitionToolsBuildVersion</key>
|
||||
<string>11B53</string>
|
||||
<string>14B47b</string>
|
||||
<key>INIntentDefinitionToolsVersion</key>
|
||||
<string>11.2.1</string>
|
||||
<string>14.1</string>
|
||||
<key>INIntents</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -32,21 +32,10 @@
|
||||
<key>INIntentKeyParameter</key>
|
||||
<string>url</string>
|
||||
<key>INIntentLastParameterTag</key>
|
||||
<integer>4</integer>
|
||||
<integer>5</integer>
|
||||
<key>INIntentManagedParameterCombinations</key>
|
||||
<dict>
|
||||
<key>url,accountName</key>
|
||||
<dict>
|
||||
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
|
||||
<true/>
|
||||
<key>INIntentParameterCombinationTitle</key>
|
||||
<string>Add ${url} to ${accountName}</string>
|
||||
<key>INIntentParameterCombinationTitleID</key>
|
||||
<string>kaKsEY</string>
|
||||
<key>INIntentParameterCombinationUpdatesLinked</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>url,accountName,folderName</key>
|
||||
<key>url,accountName,folderName,title</key>
|
||||
<dict>
|
||||
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
|
||||
<true/>
|
||||
@@ -57,12 +46,25 @@
|
||||
<key>INIntentParameterCombinationUpdatesLinked</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>url,accountName,title</key>
|
||||
<dict>
|
||||
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
|
||||
<true/>
|
||||
<key>INIntentParameterCombinationTitle</key>
|
||||
<string>Add ${url} to ${accountName}</string>
|
||||
<key>INIntentParameterCombinationTitleID</key>
|
||||
<string>kaKsEY</string>
|
||||
<key>INIntentParameterCombinationUpdatesLinked</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>INIntentName</key>
|
||||
<string>AddWebFeed</string>
|
||||
<key>INIntentParameters</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentParameterConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentParameterDisplayName</key>
|
||||
<string>URL</string>
|
||||
<key>INIntentParameterDisplayNameID</key>
|
||||
@@ -105,6 +107,52 @@
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentParameterDisplayName</key>
|
||||
<string>Title</string>
|
||||
<key>INIntentParameterDisplayNameID</key>
|
||||
<string>Ac5RHN</string>
|
||||
<key>INIntentParameterDisplayPriority</key>
|
||||
<integer>2</integer>
|
||||
<key>INIntentParameterMetadata</key>
|
||||
<dict>
|
||||
<key>INIntentParameterMetadataCapitalization</key>
|
||||
<string>Words</string>
|
||||
<key>INIntentParameterMetadataDefaultValueID</key>
|
||||
<string>SVcvQb</string>
|
||||
</dict>
|
||||
<key>INIntentParameterName</key>
|
||||
<string>title</string>
|
||||
<key>INIntentParameterPromptDialogs</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>Configuration</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
<key>INIntentParameterPromptDialogFormatString</key>
|
||||
<string>What is the ${title}of the feed?</string>
|
||||
<key>INIntentParameterPromptDialogFormatStringID</key>
|
||||
<string>IGNcSh</string>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>Primary</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INIntentParameterSupportsResolution</key>
|
||||
<true/>
|
||||
<key>INIntentParameterTag</key>
|
||||
<integer>5</integer>
|
||||
<key>INIntentParameterType</key>
|
||||
<string>String</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentParameterCustomDisambiguation</key>
|
||||
<true/>
|
||||
<key>INIntentParameterDisplayName</key>
|
||||
@@ -112,7 +160,7 @@
|
||||
<key>INIntentParameterDisplayNameID</key>
|
||||
<string>CSrgUY</string>
|
||||
<key>INIntentParameterDisplayPriority</key>
|
||||
<integer>2</integer>
|
||||
<integer>3</integer>
|
||||
<key>INIntentParameterMetadata</key>
|
||||
<dict>
|
||||
<key>INIntentParameterMetadataCapitalization</key>
|
||||
@@ -138,14 +186,6 @@
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>DisambiguationIntroduction</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogFormatString</key>
|
||||
<string>Which one?</string>
|
||||
<key>INIntentParameterPromptDialogFormatStringID</key>
|
||||
<string>fWs3li</string>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>DisambiguationSelection</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
@@ -190,6 +230,8 @@
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentParameterCustomDisambiguation</key>
|
||||
<true/>
|
||||
<key>INIntentParameterDisplayName</key>
|
||||
@@ -197,7 +239,7 @@
|
||||
<key>INIntentParameterDisplayNameID</key>
|
||||
<string>zXhMPF</string>
|
||||
<key>INIntentParameterDisplayPriority</key>
|
||||
<integer>3</integer>
|
||||
<integer>4</integer>
|
||||
<key>INIntentParameterMetadata</key>
|
||||
<dict>
|
||||
<key>INIntentParameterMetadataCapitalization</key>
|
||||
@@ -223,14 +265,6 @@
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>DisambiguationIntroduction</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogFormatString</key>
|
||||
<string>Which one?</string>
|
||||
<key>INIntentParameterPromptDialogFormatStringID</key>
|
||||
<string>gEzXaM</string>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>DisambiguationSelection</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>OrganizationIdentifier</key>
|
||||
<string>$(ORGANIZATION_IDENTIFIER)</string>
|
||||
<key>AppGroup</key>
|
||||
<string>group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS</string>
|
||||
<key>AppIdentifierPrefix</key>
|
||||
@@ -44,5 +42,7 @@
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).IntentHandler</string>
|
||||
</dict>
|
||||
<key>OrganizationIdentifier</key>
|
||||
<string>$(ORGANIZATION_IDENTIFIER)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -23,7 +23,7 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
|
||||
set {}
|
||||
get {
|
||||
if unreadCount > 0 {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessiblity")
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCount) \(unreadLabel)"
|
||||
} else {
|
||||
return name
|
||||
|
||||
@@ -26,7 +26,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
let refreshProgressModel = RefreshProgressModel()
|
||||
lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
|
||||
|
||||
var mainControllerIdentifer = MainControllerIdentifier.masterFeed
|
||||
var mainControllerIdentifier = MainControllerIdentifier.masterFeed
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
@@ -94,6 +94,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if (isBeingPresented || isMovingToParent) {
|
||||
// Only show the Twitter alert the first time
|
||||
// the view is presented.
|
||||
presentTwitterDeprecationAlertIfRequired()
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
|
||||
@@ -617,8 +627,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
Context Menu Order:
|
||||
1. Add Web Feed
|
||||
2. Add Reddit Feed
|
||||
3. Add Twitter Feed
|
||||
4. Add Folder
|
||||
3. Add Folder
|
||||
*/
|
||||
|
||||
var menuItems: [UIAction] = []
|
||||
@@ -637,13 +646,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
}
|
||||
menuItems.append(addRedditFeedAction)
|
||||
}
|
||||
if ExtensionPointManager.shared.isTwitterEnabled {
|
||||
let addTwitterFeedActionTitle = NSLocalizedString("Add Twitter Feed", comment: "Add Twitter Feed")
|
||||
let addTwitterFeedAction = UIAction(title: addTwitterFeedActionTitle, image: AppAssets.contextMenuTwitter.tinted(color: .label)) { _ in
|
||||
self.coordinator.showAddTwitterFeed()
|
||||
}
|
||||
menuItems.append(addTwitterFeedAction)
|
||||
}
|
||||
}
|
||||
|
||||
let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
|
||||
@@ -657,7 +659,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
|
||||
self.addNewItemButton.menu = contextMenu
|
||||
}
|
||||
|
||||
|
||||
func focus() {
|
||||
becomeFirstResponder()
|
||||
}
|
||||
@@ -670,6 +672,29 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
}
|
||||
}
|
||||
|
||||
private func presentTwitterDeprecationAlertIfRequired() {
|
||||
if AppDefaults.shared.twitterDeprecationAlertShown { return }
|
||||
|
||||
let expiryDate = Date(timeIntervalSince1970: 1691539200) // August 9th 2023, 00:00 UTC
|
||||
let currentDate = Date()
|
||||
if currentDate > expiryDate {
|
||||
return // If after August 9th, don't show
|
||||
}
|
||||
|
||||
if AccountManager.shared.anyLocalOriCloudAccountHasAtLeastOneTwitterFeed() {
|
||||
showTwitterDeprecationAlert()
|
||||
}
|
||||
AppDefaults.shared.twitterDeprecationAlertShown = true
|
||||
}
|
||||
|
||||
private func showTwitterDeprecationAlert() {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Twitter Integration Removed", comment: "Twitter Integration Removed"),
|
||||
message: NSLocalizedString("On February 1, 2023, Twitter announced the end of free access to the Twitter API, effective February 9.\n\nSince Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.\n\nWe’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.\n\nYou can still read whatever you have already downloaded. However, those feeds will no longer update.", comment: "Twitter deprecation message"),
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIContextMenuInteractionDelegate
|
||||
@@ -693,6 +718,9 @@ extension MasterFeedViewController: UIContextMenuInteractionDelegate {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
|
||||
}
|
||||
|
||||
if let catchUpAction = self.catchUpActionMenu(account: account, contentView: interaction.view) {
|
||||
menuElements.append(catchUpAction)
|
||||
}
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.deactivateAccountAction(account: account)]))
|
||||
|
||||
return UIMenu(title: "", children: menuElements)
|
||||
@@ -921,6 +949,11 @@ private extension MasterFeedViewController {
|
||||
|
||||
if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
|
||||
|
||||
}
|
||||
|
||||
if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) {
|
||||
menuElements.append(catchUpAction)
|
||||
}
|
||||
|
||||
if includeDeleteRename {
|
||||
@@ -948,6 +981,10 @@ private extension MasterFeedViewController {
|
||||
if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
|
||||
}
|
||||
|
||||
if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) {
|
||||
menuElements.append(catchUpAction)
|
||||
}
|
||||
|
||||
menuElements.append(UIMenu(title: "",
|
||||
options: .displayInline,
|
||||
@@ -961,13 +998,22 @@ private extension MasterFeedViewController {
|
||||
})
|
||||
}
|
||||
|
||||
func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration? {
|
||||
guard let markAllAction = self.markAllAsReadAction(indexPath: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in
|
||||
return UIMenu(title: "", children: [markAllAction])
|
||||
guard let self = self else { return nil }
|
||||
|
||||
var menuElements = [UIMenuElement]()
|
||||
|
||||
if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
|
||||
}
|
||||
|
||||
if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) {
|
||||
menuElements.append(catchUpAction)
|
||||
}
|
||||
|
||||
return UIMenu(title: "", children: menuElements)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1151,6 +1197,97 @@ private extension MasterFeedViewController {
|
||||
return action
|
||||
}
|
||||
|
||||
func catchUpActionMenu(indexPath: IndexPath) -> UIMenu? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView,
|
||||
feed.unreadCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Doesn't make sense to mark articles newer than a day with catch up with first option being older than a day
|
||||
if let maybeSmartFeed = feed as? SmartFeed {
|
||||
if maybeSmartFeed.delegate is TodayFeedDelegate {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Mark as Read Older Than", comment: "Command")
|
||||
let oneDayAction = UIAction(title: "1 Day") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Day as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .day, value: -1, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
let twoDayAction = UIAction(title: "2 Days") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Days as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .day, value: -2, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
let threeDayAction = UIAction(title: "3 Days") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 3 Days as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .day, value: -3, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
let oneWeekAction = UIAction(title: "1 Week") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Week as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
let twoWeekAction = UIAction(title: "2 Weeks") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Weeks as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -2, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
let oneMonthAction = UIAction(title: "1 Month") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Month as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .month, value: -1, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
let oneYearAction = UIAction(title: "1 Year") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Year as Read", sourceType: contentView) { [weak self] in
|
||||
let cutoff = Calendar.current.date(byAdding: .year, value: -1, to: Date())
|
||||
if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
var markActions = [UIAction]()
|
||||
markActions.append(oneDayAction)
|
||||
markActions.append(twoDayAction)
|
||||
markActions.append(threeDayAction)
|
||||
markActions.append(oneWeekAction)
|
||||
markActions.append(twoWeekAction)
|
||||
markActions.append(oneMonthAction)
|
||||
markActions.append(oneYearAction)
|
||||
let majorMenu = UIMenu(title: title, image: getMarkOlderImageDirection(), children: markActions)
|
||||
|
||||
return majorMenu
|
||||
}
|
||||
|
||||
func getMarkOlderImageDirection() -> UIImage {
|
||||
if AppDefaults.shared.timelineSortDirection == .orderedDescending {
|
||||
return AppAssets.markBelowAsReadImage
|
||||
} else {
|
||||
return AppAssets.markAboveAsReadImage
|
||||
}
|
||||
}
|
||||
func markAllAsReadAction(account: Account, contentView: UIView?) -> UIAction? {
|
||||
guard account.unreadCount > 0, let contentView = contentView else {
|
||||
return nil
|
||||
@@ -1171,6 +1308,102 @@ private extension MasterFeedViewController {
|
||||
return action
|
||||
}
|
||||
|
||||
func catchUpActionMenu(account: Account, contentView: UIView?) -> UIMenu? {
|
||||
guard account.unreadCount > 0, let contentView = contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Mark as Read Older Than", comment: "Command")
|
||||
let oneDayAction = UIAction(title: "1 Day") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Day as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .day, value: -1, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let twoDayAction = UIAction(title: "2 Days") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Days as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .day, value: -2, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let threeDayAction = UIAction(title: "3 Days") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 3 Days as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .day, value: -3, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let oneWeekAction = UIAction(title: "1 Week") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Week as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let twoWeekAction = UIAction(title: "2 Weeks") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Weeks as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -2, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let oneMonthAction = UIAction(title: "1 Month") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Month as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .month, value: -1, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let oneYearAction = UIAction(title: "1 Year") { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Year as Read", sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let cutoff = Calendar.current.date(byAdding: .year, value: -1, to: Date())
|
||||
if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var markActions = [UIAction]()
|
||||
markActions.append(oneDayAction)
|
||||
markActions.append(twoDayAction)
|
||||
markActions.append(threeDayAction)
|
||||
markActions.append(oneWeekAction)
|
||||
markActions.append(twoWeekAction)
|
||||
markActions.append(oneMonthAction)
|
||||
markActions.append(oneYearAction)
|
||||
let majorMenu = UIMenu(title: title, image: getMarkOlderImageDirection(), children: markActions)
|
||||
|
||||
return majorMenu
|
||||
}
|
||||
|
||||
|
||||
func rename(indexPath: IndexPath) {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
||||
|
||||
@@ -22,7 +22,7 @@ class MasterTimelineTitleView: UIView {
|
||||
set { }
|
||||
get {
|
||||
if let name = label.text {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessiblity")
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)"
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -31,7 +31,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
private lazy var dataSource = makeDataSource()
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
|
||||
var mainControllerIdentifer = MainControllerIdentifier.masterTimeline
|
||||
var mainControllerIdentifier = MainControllerIdentifier.masterTimeline
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
@@ -1,116 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "Icon_20x20@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-41.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "Icon_20x20@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "Icon_29x29@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-58.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "Icon_29x29@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-87.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "Icon_40x40@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-80.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "Icon_40x40@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-121.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"filename" : "Icon_60x60@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-120.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"filename" : "Icon_60x60@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-180.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-20.png",
|
||||
"scale" : "1x"
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "Icon_20x20@2x 1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-42.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-29.png",
|
||||
"scale" : "1x"
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "Icon_29x29@2x 1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-59.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "1x"
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "Icon_40x40@2x 1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-81.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-152.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"filename" : "Icon_76x76@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-167.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"filename" : "Icon_83.5x83.5@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon_1024x1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "icon-1024.png",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 969 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |