Merge branch 'ios-candidate'
This commit is contained in:
commit
f9af3c786b
@ -13,7 +13,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "../Articles", .upToNextMajor(from: "1.0.0")),
|
.package(url: "../Articles", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")),
|
.package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")),
|
||||||
|
@ -180,10 +180,14 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
if let ckError = ((error as? CloudKitError)?.error as? CKError), ckError.code == .unknownItem {
|
||||||
|
completion(.success(true))
|
||||||
|
} else {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
||||||
|
@ -16,7 +16,8 @@ import Articles
|
|||||||
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||||
|
|
||||||
private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String)
|
private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String)
|
||||||
private var unclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
|
private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
|
||||||
|
private var existingUnclaimedWebFeeds = [String: [WebFeed]]()
|
||||||
|
|
||||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
if let container = account.existingContainer(withExternalID: containerExternalID) {
|
if let container = account.existingContainer(withExternalID: containerExternalID) {
|
||||||
createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container)
|
createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container)
|
||||||
} else {
|
} else {
|
||||||
addUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID)
|
addNewUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,19 +107,27 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
folder?.externalID = record.externalID
|
folder?.externalID = record.externalID
|
||||||
}
|
}
|
||||||
|
|
||||||
if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] {
|
guard let container = folder, let containerExternalID = container.externalID else { return }
|
||||||
for unclaimedWebFeed in unclaimedWebFeeds {
|
|
||||||
createWebFeedIfNecessary(url: unclaimedWebFeed.url,
|
if let newUnclaimedWebFeeds = newUnclaimedWebFeeds[containerExternalID] {
|
||||||
name: unclaimedWebFeed.name,
|
for newUnclaimedWebFeed in newUnclaimedWebFeeds {
|
||||||
editedName: unclaimedWebFeed.editedName,
|
createWebFeedIfNecessary(url: newUnclaimedWebFeed.url,
|
||||||
homePageURL: unclaimedWebFeed.homePageURL,
|
name: newUnclaimedWebFeed.name,
|
||||||
webFeedExternalID: unclaimedWebFeed.webFeedExternalID,
|
editedName: newUnclaimedWebFeed.editedName,
|
||||||
container: folder)
|
homePageURL: newUnclaimedWebFeed.homePageURL,
|
||||||
|
webFeedExternalID: newUnclaimedWebFeed.webFeedExternalID,
|
||||||
|
container: container)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.unclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
self.newUnclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let existingUnclaimedWebFeeds = existingUnclaimedWebFeeds[containerExternalID] {
|
||||||
|
for existingUnclaimedWebFeed in existingUnclaimedWebFeeds {
|
||||||
|
container.addWebFeed(existingUnclaimedWebFeed)
|
||||||
|
}
|
||||||
|
self.existingUnclaimedWebFeeds.removeValue(forKey: containerExternalID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeContainer(_ externalID: String) {
|
func removeContainer(_ externalID: String) {
|
||||||
@ -152,6 +161,8 @@ private extension CloudKitAcountZoneDelegate {
|
|||||||
case .insert(_, let externalID, _):
|
case .insert(_, let externalID, _):
|
||||||
if let container = account.existingContainer(withExternalID: externalID) {
|
if let container = account.existingContainer(withExternalID: externalID) {
|
||||||
container.addWebFeed(webFeed)
|
container.addWebFeed(webFeed)
|
||||||
|
} else {
|
||||||
|
addExistingUnclaimedWebFeed(webFeed, containerExternalID: externalID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,14 +181,25 @@ private extension CloudKitAcountZoneDelegate {
|
|||||||
container.addWebFeed(webFeed)
|
container.addWebFeed(webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) {
|
func addNewUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) {
|
||||||
if var unclaimedWebFeeds = self.unclaimedWebFeeds[containerExternalID] {
|
if var unclaimedWebFeeds = self.newUnclaimedWebFeeds[containerExternalID] {
|
||||||
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
|
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
|
||||||
self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||||
} else {
|
} else {
|
||||||
var unclaimedWebFeeds = [UnclaimedWebFeed]()
|
var unclaimedWebFeeds = [UnclaimedWebFeed]()
|
||||||
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
|
unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID))
|
||||||
self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExistingUnclaimedWebFeed(_ webFeed: WebFeed, containerExternalID: String) {
|
||||||
|
if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] {
|
||||||
|
unclaimedWebFeeds.append(webFeed)
|
||||||
|
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||||
|
} else {
|
||||||
|
var unclaimedWebFeeds = [WebFeed]()
|
||||||
|
unclaimedWebFeeds.append(webFeed)
|
||||||
|
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,21 +78,21 @@ private extension TwitterStatus {
|
|||||||
|
|
||||||
var html = String()
|
var html = String()
|
||||||
var prevIndex = displayStartIndex
|
var prevIndex = displayStartIndex
|
||||||
var emojiOffset = 0
|
var unicodeScalarOffset = 0
|
||||||
|
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
|
|
||||||
// The twitter indices are messed up by emoji with more than one scalar, we are going to adjust for that here.
|
// The twitter indices are messed up by characters with more than one scalar, we are going to adjust for that here.
|
||||||
let emojiEndIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
|
let endIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex
|
||||||
if prevIndex < emojiEndIndex {
|
if prevIndex < endIndex {
|
||||||
let emojis = String(text[prevIndex..<emojiEndIndex]).emojis
|
let characters = String(text[prevIndex..<endIndex])
|
||||||
for emoji in emojis {
|
for character in characters {
|
||||||
emojiOffset += emoji.unicodeScalars.count - 1
|
unicodeScalarOffset += character.unicodeScalars.count - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let offsetStartIndex = entity.startIndex - emojiOffset
|
let offsetStartIndex = entity.startIndex - unicodeScalarOffset
|
||||||
let offsetEndIndex = entity.endIndex - emojiOffset
|
let offsetEndIndex = entity.endIndex - unicodeScalarOffset
|
||||||
|
|
||||||
let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex
|
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
|
let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import RSParser
|
import RSParser
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
final class FeedbinEntry: Codable {
|
final class FeedbinEntry: Decodable {
|
||||||
|
|
||||||
let articleID: Int
|
let articleID: Int
|
||||||
let feedID: Int
|
let feedID: Int
|
||||||
@ -50,14 +50,25 @@ final class FeedbinEntry: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeedbinEntryJSONFeed: Codable {
|
struct FeedbinEntryJSONFeed: Decodable {
|
||||||
let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor?
|
let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case jsonFeedAuthor = "author"
|
case jsonFeedAuthor = "author"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
do {
|
||||||
|
jsonFeedAuthor = try container.decode(FeedbinEntryJSONFeedAuthor.self, forKey: .jsonFeedAuthor)
|
||||||
|
} catch {
|
||||||
|
jsonFeedAuthor = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeedbinEntryJSONFeedAuthor: Codable {
|
}
|
||||||
|
|
||||||
|
struct FeedbinEntryJSONFeedAuthor: Decodable {
|
||||||
let url: String?
|
let url: String?
|
||||||
let avatarURL: String?
|
let avatarURL: String?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
|
@ -310,7 +310,7 @@ final class FeedlyAPICaller {
|
|||||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||||
|
|
||||||
send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
send(request: request, resultType: Optional<FeedlyCollection>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let (httpResponse, _)):
|
case .success(let (httpResponse, _)):
|
||||||
if httpResponse.statusCode == 200 {
|
if httpResponse.statusCode == 200 {
|
||||||
|
@ -133,7 +133,7 @@ final class NewsBlurAPICaller: NSObject {
|
|||||||
URLQueryItem(name: "page", value: String(page)),
|
URLQueryItem(name: "page", value: String(page)),
|
||||||
URLQueryItem(name: "order", value: "newest"),
|
URLQueryItem(name: "order", value: "newest"),
|
||||||
URLQueryItem(name: "read_filter", value: "all"),
|
URLQueryItem(name: "read_filter", value: "all"),
|
||||||
URLQueryItem(name: "include_hidden", value: "true"),
|
URLQueryItem(name: "include_hidden", value: "false"),
|
||||||
URLQueryItem(name: "include_story_content", value: "true"),
|
URLQueryItem(name: "include_story_content", value: "true"),
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ final class NewsBlurAPICaller: NSObject {
|
|||||||
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
|
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
|
||||||
let url = baseURL
|
let url = baseURL
|
||||||
.appendingPathComponent("reader/river_stories")
|
.appendingPathComponent("reader/river_stories")
|
||||||
.appendingQueryItem(.init(name: "include_hidden", value: "true"))?
|
.appendingQueryItem(.init(name: "include_hidden", value: "false"))?
|
||||||
.appendingQueryItems(hashes.map {
|
.appendingQueryItems(hashes.map {
|
||||||
URLQueryItem(name: "h", value: $0.hash)
|
URLQueryItem(name: "h", value: $0.hash)
|
||||||
})
|
})
|
||||||
|
@ -14,10 +14,24 @@ import SyncDatabase
|
|||||||
import os.log
|
import os.log
|
||||||
import Secrets
|
import Secrets
|
||||||
|
|
||||||
public enum ReaderAPIAccountDelegateError: String, Error {
|
public enum ReaderAPIAccountDelegateError: LocalizedError {
|
||||||
case unknown = "An unknown error occurred."
|
case unknown
|
||||||
case invalidParameter = "There was an invalid parameter passed."
|
case invalidParameter
|
||||||
case invalidResponse = "There was an invalid response from the server."
|
case invalidResponse
|
||||||
|
case urlNotFound
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .unknown:
|
||||||
|
return NSLocalizedString("An unexpected error occurred.", comment: "An unexpected error occurred.")
|
||||||
|
case .invalidParameter:
|
||||||
|
return NSLocalizedString("An invalid parameter was passed.", comment: "An invalid parameter was passed.")
|
||||||
|
case .invalidResponse:
|
||||||
|
return NSLocalizedString("There was an invalid response from the server.", comment: "There was an invalid response from the server.")
|
||||||
|
case .urlNotFound:
|
||||||
|
return NSLocalizedString("The API URL wasn't found.", comment: "The API URL wasn't found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ReaderAPIAccountDelegate: AccountDelegate {
|
final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||||
|
@ -132,9 +132,13 @@ final class ReaderAPICaller: NSObject {
|
|||||||
|
|
||||||
completion(.success(self.credentials))
|
completion(.success(self.credentials))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
if let transportError = error as? TransportError, case .httpError(let code) = transportError, code == 404 {
|
||||||
|
completion(.failure(ReaderAPIAccountDelegateError.urlNotFound))
|
||||||
|
} else {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.0")),
|
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
|
||||||
.package(url: "../Articles", .upToNextMajor(from: "1.0.0")),
|
.package(url: "../Articles", .upToNextMajor(from: "1.0.0")),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
@ -149,18 +149,7 @@ private extension WebFeedInspectorViewController {
|
|||||||
guard let feed = feed, let iconView = iconView else {
|
guard let feed = feed, let iconView = iconView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
iconView.iconImage = IconImageCache.shared.imageForFeed(feed)
|
||||||
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
|
|
||||||
iconView.iconImage = feedIcon
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
|
|
||||||
iconView.iconImage = favicon
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iconView.iconImage = feed.smallIcon
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateName() {
|
func updateName() {
|
||||||
|
@ -768,7 +768,7 @@ private extension SidebarViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func imageFor(_ node: Node) -> IconImage? {
|
func imageFor(_ node: Node) -> IconImage? {
|
||||||
if let feed = node.representedObject as? WebFeed, let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
|
if let feed = node.representedObject as? WebFeed, let feedIcon = IconImageCache.shared.imageForFeed(feed) {
|
||||||
return feedIcon
|
return feedIcon
|
||||||
}
|
}
|
||||||
if let smallIconProvider = node.representedObject as? SmallIconProvider {
|
if let smallIconProvider = node.representedObject as? SmallIconProvider {
|
||||||
|
@ -38,7 +38,7 @@ extension Article: PasteboardWriterOwner {
|
|||||||
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
||||||
var types = [ArticlePasteboardWriter.articleUTIType]
|
var types = [ArticlePasteboardWriter.articleUTIType]
|
||||||
|
|
||||||
if let link = article.preferredLink, let _ = URL(string: link) {
|
if let _ = article.preferredURL {
|
||||||
types += [.URL]
|
types += [.URL]
|
||||||
}
|
}
|
||||||
types += [.string, .html, ArticlePasteboardWriter.articleUTIInternalType]
|
types += [.string, .html, ArticlePasteboardWriter.articleUTIInternalType]
|
||||||
|
@ -886,28 +886,7 @@ extension TimelineViewController: NSTableViewDelegate {
|
|||||||
if !showIcons {
|
if !showIcons {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return IconImageCache.shared.imageForArticle(article)
|
||||||
if let authors = article.authors {
|
|
||||||
for author in authors {
|
|
||||||
if let image = avatarForAuthor(author) {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let feed = article.webFeed else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) {
|
|
||||||
return feedIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
|
|
||||||
return favicon
|
|
||||||
}
|
|
||||||
|
|
||||||
return FaviconGenerator.favicon(feed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func avatarForAuthor(_ author: Author) -> IconImage? {
|
private func avatarForAuthor(_ author: Author) -> IconImage? {
|
||||||
|
@ -107,7 +107,7 @@ struct ArticleToolbarModifier: ViewModifier {
|
|||||||
.disabled(sceneModel.shareButtonState == nil)
|
.disabled(sceneModel.shareButtonState == nil)
|
||||||
.help("Share")
|
.help("Share")
|
||||||
.sheet(isPresented: $showActivityView) {
|
.sheet(isPresented: $showActivityView) {
|
||||||
if let article = sceneModel.selectedArticles.first, let link = article.preferredLink, let url = URL(string: link) {
|
if let article = sceneModel.selectedArticles.first, let url = article.preferredURL {
|
||||||
ActivityViewController(title: article.title, url: url)
|
ActivityViewController(title: article.title, url: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,20 +41,6 @@ final class FeedIconImageLoader: ObservableObject {
|
|||||||
private extension FeedIconImageLoader {
|
private extension FeedIconImageLoader {
|
||||||
|
|
||||||
func fetchImage() {
|
func fetchImage() {
|
||||||
if let webFeed = feed as? WebFeed {
|
image = IconImageCache.shared.imageForFeed(feed)
|
||||||
if let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
|
|
||||||
image = feedIconImage
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
|
||||||
image = faviconImage
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let smallIconProvider = feed as? SmallIconProvider {
|
|
||||||
image = smallIconProvider.smallIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -249,12 +249,11 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openIndicatedArticleInBrowser(_ article: Article) {
|
func openIndicatedArticleInBrowser(_ article: Article) {
|
||||||
guard let link = article.preferredLink else { return }
|
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
guard let link = article.preferredLink else { return }
|
||||||
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||||
#else
|
#else
|
||||||
guard let url = URL(string: link) else { return }
|
guard let url = article.preferredURL else { return }
|
||||||
UIApplication.shared.open(url, options: [:])
|
UIApplication.shared.open(url, options: [:])
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{\rtf1\ansi\ansicpg1252\cocoartf2511
|
{\rtf1\ansi\ansicpg1252\cocoartf2513
|
||||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
|
||||||
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
|
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
|
||||||
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
|
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
|
||||||
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||||
\deftab720
|
\deftab720
|
||||||
\pard\pardeftab720\li354\fi-355\sa60\partightenfactor0
|
\pard\pardeftab720\sa60\partightenfactor0
|
||||||
|
|
||||||
\f0\fs28 \cf2 NetNewsWire 5.0 is dedicated to all the people who showed up to help with code, design, HTML, documentation, icons, testing, and just to help talk things over and think things through. This app\'92s for you!}
|
\f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.}
|
@ -24,7 +24,7 @@ struct SettingsAboutView: View {
|
|||||||
Section(header: Text("THANKS")) {
|
Section(header: Text("THANKS")) {
|
||||||
AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20)
|
AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20)
|
||||||
}
|
}
|
||||||
Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2020 BrentSimmons").font(.footnote)) {
|
Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2021 Brent Simmons").font(.footnote)) {
|
||||||
AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20)
|
AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20)
|
||||||
}
|
}
|
||||||
}.listStyle(InsetGroupedListStyle())
|
}.listStyle(InsetGroupedListStyle())
|
||||||
|
@ -1015,6 +1015,9 @@
|
|||||||
844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; };
|
844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; };
|
||||||
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; };
|
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; };
|
||||||
845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; };
|
845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; };
|
||||||
|
8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; };
|
||||||
|
8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; };
|
||||||
|
8454C3FD263F3AD600E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; };
|
||||||
845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; };
|
845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; };
|
||||||
845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */; };
|
845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */; };
|
||||||
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */; };
|
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */; };
|
||||||
@ -1901,6 +1904,7 @@
|
|||||||
844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = SidebarKeyboardShortcuts.plist; sourceTree = "<group>"; };
|
844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = SidebarKeyboardShortcuts.plist; sourceTree = "<group>"; };
|
||||||
845213221FCA5B10003B6E93 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
|
845213221FCA5B10003B6E93 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
|
||||||
845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TimelineKeyboardShortcuts.plist; sourceTree = "<group>"; };
|
845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TimelineKeyboardShortcuts.plist; sourceTree = "<group>"; };
|
||||||
|
8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageCache.swift; sourceTree = "<group>"; };
|
||||||
845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = "<group>"; };
|
845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = "<group>"; };
|
||||||
845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellLayout.swift; sourceTree = "<group>"; };
|
845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellLayout.swift; sourceTree = "<group>"; };
|
||||||
845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellAppearance.swift; sourceTree = "<group>"; };
|
845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellAppearance.swift; sourceTree = "<group>"; };
|
||||||
@ -3436,6 +3440,7 @@
|
|||||||
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
|
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
|
||||||
51C4CFEF24D37D1F00AF9874 /* Secrets.swift */,
|
51C4CFEF24D37D1F00AF9874 /* Secrets.swift */,
|
||||||
511B9805237DCAC90028BCAA /* UserInfoKey.swift */,
|
511B9805237DCAC90028BCAA /* UserInfoKey.swift */,
|
||||||
|
8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */,
|
||||||
51C452AD2265102800C03939 /* Timeline */,
|
51C452AD2265102800C03939 /* Timeline */,
|
||||||
84702AB31FA27AE8006B8943 /* Commands */,
|
84702AB31FA27AE8006B8943 /* Commands */,
|
||||||
51934CCC231078DC006127BE /* Activity */,
|
51934CCC231078DC006127BE /* Activity */,
|
||||||
@ -5150,6 +5155,7 @@
|
|||||||
65ED4007235DEF6C0081F399 /* AddFeedController.swift in Sources */,
|
65ED4007235DEF6C0081F399 /* AddFeedController.swift in Sources */,
|
||||||
65ED4008235DEF6C0081F399 /* AccountRefreshTimer.swift in Sources */,
|
65ED4008235DEF6C0081F399 /* AccountRefreshTimer.swift in Sources */,
|
||||||
65ED4009235DEF6C0081F399 /* SidebarStatusBarView.swift in Sources */,
|
65ED4009235DEF6C0081F399 /* SidebarStatusBarView.swift in Sources */,
|
||||||
|
8454C3FD263F3AD600E3F9C7 /* IconImageCache.swift in Sources */,
|
||||||
65ED400A235DEF6C0081F399 /* SearchTimelineFeedDelegate.swift in Sources */,
|
65ED400A235DEF6C0081F399 /* SearchTimelineFeedDelegate.swift in Sources */,
|
||||||
65ED400B235DEF6C0081F399 /* TodayFeedDelegate.swift in Sources */,
|
65ED400B235DEF6C0081F399 /* TodayFeedDelegate.swift in Sources */,
|
||||||
65ED400C235DEF6C0081F399 /* FolderInspectorViewController.swift in Sources */,
|
65ED400C235DEF6C0081F399 /* FolderInspectorViewController.swift in Sources */,
|
||||||
@ -5375,6 +5381,7 @@
|
|||||||
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
||||||
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
|
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
|
||||||
D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */,
|
D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */,
|
||||||
|
8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */,
|
||||||
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
|
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
|
||||||
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
||||||
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
|
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
|
||||||
@ -5545,6 +5552,7 @@
|
|||||||
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
||||||
841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */,
|
841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */,
|
||||||
D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */,
|
D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */,
|
||||||
|
8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */,
|
||||||
518651B223555EB20078E021 /* NNW3Document.swift in Sources */,
|
518651B223555EB20078E021 /* NNW3Document.swift in Sources */,
|
||||||
D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */,
|
D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */,
|
||||||
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */,
|
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */,
|
||||||
|
@ -248,9 +248,8 @@ private extension ActivityManager {
|
|||||||
attributeSet.title = feed.nameForDisplay
|
attributeSet.title = feed.nameForDisplay
|
||||||
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
attributeSet.keywords = makeKeywords(feed.nameForDisplay)
|
||||||
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed)
|
attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed)
|
||||||
if let iconImage = appDelegate.webFeedIconDownloader.icon(for: feed) {
|
|
||||||
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
|
if let iconImage = IconImageCache.shared.imageForFeed(feed) {
|
||||||
} else if let iconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
|
|
||||||
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
|
attributeSet.thumbnailData = iconImage.image.dataRepresentation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@
|
|||||||
document.addEventListener("click", (ev) =>
|
document.addEventListener("click", (ev) =>
|
||||||
{
|
{
|
||||||
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
|
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
|
||||||
if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return")) return;
|
if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return, .footnotes a[href*='#fn'], .footnotes a[href^='#']")) return;
|
||||||
const id = idFromHash(ev.target);
|
const id = idFromHash(ev.target);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const fnref = document.getElementById(id);
|
const fnref = document.getElementById(id);
|
||||||
|
@ -382,7 +382,8 @@ img[src*="share-buttons"] {
|
|||||||
|
|
||||||
.newsfoot-footnote-popover .reversefootnote,
|
.newsfoot-footnote-popover .reversefootnote,
|
||||||
.newsfoot-footnote-popover .footnoteBackLink,
|
.newsfoot-footnote-popover .footnoteBackLink,
|
||||||
.newsfoot-footnote-popover .footnote-return {
|
.newsfoot-footnote-popover .footnote-return,
|
||||||
|
.newsfoot-footnote-popover a[href*='#fn'] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ final class MarkStatusCommand: UndoableCommand {
|
|||||||
// Filter out articles that already have the desired status or can't be marked.
|
// Filter out articles that already have the desired status or can't be marked.
|
||||||
let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag)
|
let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag)
|
||||||
if articlesToMark.isEmpty {
|
if articlesToMark.isEmpty {
|
||||||
|
completion?()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
self.articles = Set(articlesToMark)
|
self.articles = Set(articlesToMark)
|
||||||
|
@ -56,6 +56,18 @@ extension Article {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var preferredURL: URL? {
|
||||||
|
guard let link = preferredLink else { return nil }
|
||||||
|
// If required, we replace any space characters to handle malformed links that are otherwise percent
|
||||||
|
// encoded but contain spaces. For performance reasons, only try this if initial URL init fails.
|
||||||
|
if let url = URL(string: link) {
|
||||||
|
return url
|
||||||
|
} else if let url = URL(string: link.replacingOccurrences(of: " ", with: "%20")) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var body: String? {
|
var body: String? {
|
||||||
return contentHTML ?? contentText ?? summary
|
return contentHTML ?? contentText ?? summary
|
||||||
}
|
}
|
||||||
@ -84,32 +96,7 @@ extension Article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func iconImage() -> IconImage? {
|
func iconImage() -> IconImage? {
|
||||||
if let authors = authors, authors.count == 1, let author = authors.first {
|
return IconImageCache.shared.imageForArticle(self)
|
||||||
if let image = appDelegate.authorAvatarDownloader.image(for: author) {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let authors = webFeed?.authors, authors.count == 1, let author = authors.first {
|
|
||||||
if let image = appDelegate.authorAvatarDownloader.image(for: author) {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let webFeed = webFeed else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed)
|
|
||||||
if feedIconImage != nil {
|
|
||||||
return feedIconImage
|
|
||||||
}
|
|
||||||
|
|
||||||
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
|
||||||
return faviconImage
|
|
||||||
}
|
|
||||||
|
|
||||||
return FaviconGenerator.favicon(webFeed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func iconImageUrl(webFeed: WebFeed) -> URL? {
|
func iconImageUrl(webFeed: WebFeed) -> URL? {
|
||||||
|
129
Shared/IconImageCache.swift
Normal file
129
Shared/IconImageCache.swift
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
//
|
||||||
|
// IconImageCache.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Brent Simmons on 5/2/21.
|
||||||
|
// Copyright © 2021 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Account
|
||||||
|
import Articles
|
||||||
|
|
||||||
|
class IconImageCache {
|
||||||
|
|
||||||
|
static var shared = IconImageCache()
|
||||||
|
|
||||||
|
private var smartFeedIconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
private var webFeedIconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
private var faviconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
private var smallIconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
private var authorIconImageCache = [Author: IconImage]()
|
||||||
|
|
||||||
|
func imageFor(_ feedID: FeedIdentifier) -> IconImage? {
|
||||||
|
if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
|
||||||
|
return imageForFeed(smartFeed)
|
||||||
|
}
|
||||||
|
if let feed = AccountManager.shared.existingFeed(with: feedID) {
|
||||||
|
return imageForFeed(feed)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageForFeed(_ feed: Feed) -> IconImage? {
|
||||||
|
guard let feedID = feed.feedID else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let smartFeed = feed as? PseudoFeed {
|
||||||
|
return imageForSmartFeed(smartFeed, feedID)
|
||||||
|
}
|
||||||
|
if let webFeed = feed as? WebFeed, let iconImage = imageForWebFeed(webFeed, feedID) {
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
if let smallIconProvider = feed as? SmallIconProvider {
|
||||||
|
return imageForSmallIconProvider(smallIconProvider, feedID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageForArticle(_ article: Article) -> IconImage? {
|
||||||
|
if let iconImage = imageForAuthors(article.authors) {
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
guard let feed = article.webFeed else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return imageForFeed(feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyCache() {
|
||||||
|
smartFeedIconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
webFeedIconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
faviconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
smallIconImageCache = [FeedIdentifier: IconImage]()
|
||||||
|
authorIconImageCache = [Author: IconImage]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IconImageCache {
|
||||||
|
|
||||||
|
func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: FeedIdentifier) -> IconImage? {
|
||||||
|
if let iconImage = smartFeedIconImageCache[feedID] {
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
if let iconImage = smartFeed.smallIcon {
|
||||||
|
smartFeedIconImageCache[feedID] = iconImage
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageForWebFeed(_ webFeed: WebFeed, _ feedID: FeedIdentifier) -> IconImage? {
|
||||||
|
if let iconImage = webFeedIconImageCache[feedID] {
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
if let iconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
|
||||||
|
webFeedIconImageCache[feedID] = iconImage
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
if let faviconImage = faviconImageCache[feedID] {
|
||||||
|
return faviconImage
|
||||||
|
}
|
||||||
|
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
||||||
|
faviconImageCache[feedID] = faviconImage
|
||||||
|
return faviconImage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: FeedIdentifier) -> IconImage? {
|
||||||
|
if let iconImage = smallIconImageCache[feedID] {
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
if let iconImage = provider.smallIcon {
|
||||||
|
smallIconImageCache[feedID] = iconImage
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageForAuthors(_ authors: Set<Author>?) -> IconImage? {
|
||||||
|
guard let authors = authors, authors.count == 1, let author = authors.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return imageForAuthor(author)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageForAuthor(_ author: Author) -> IconImage? {
|
||||||
|
if let iconImage = authorIconImageCache[author] {
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
if let iconImage = appDelegate.authorAvatarDownloader.image(for: author) {
|
||||||
|
authorIconImageCache[author] = iconImage
|
||||||
|
return iconImage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -56,9 +56,7 @@ private extension WebFeedTreeControllerDelegate {
|
|||||||
|
|
||||||
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
|
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
|
||||||
return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in
|
return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in
|
||||||
if let feedID = feed.feedID, !filterExceptions.contains(feedID) && isReadFiltered && feed.unreadCount == 0 {
|
// All Smart Feeds should remain visible despite the Hide Read Feeds setting
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return parentNode.existingOrNewChildNode(with: feed as AnyObject)
|
return parentNode.existingOrNewChildNode(with: feed as AnyObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
Technotes/ReleaseNotes-iOS.markdown
Normal file
25
Technotes/ReleaseNotes-iOS.markdown
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# iOS Release Notes
|
||||||
|
|
||||||
|
### 6.0 TestFlight build 603 - 16 May 2021
|
||||||
|
|
||||||
|
Feedly: handle Feedly API change with return value on deleting a folder
|
||||||
|
NewsBlur: sync no longer includes items marked as hidden on NewsBlur
|
||||||
|
FreshRSS: form for adding account now suggests endpoing URL
|
||||||
|
FreshRSS: improved the error message for when the API URL can’t be found
|
||||||
|
iCloud: retain existing feeds moved to a folder that doesn’t exist yet (sync ordering issue)
|
||||||
|
Renamed a Delete Account button to Remove Account
|
||||||
|
iCloud: skip displaying an error message on deleting a feed that doesn’t exist in iCloud
|
||||||
|
Preferences: Tweaked text explaining Feed Providers
|
||||||
|
Feeds list: context menu for smart feeds is back (regression fix)
|
||||||
|
Feeds list: all smart feeds remain visible despite Hide Read Feeds setting
|
||||||
|
Article view: fixed zoom issue on iPad on rotation
|
||||||
|
Article view: fixed bug where mark-read button on toolbar would flash on navigating to an unread article
|
||||||
|
Article view: made footnote detection more robust
|
||||||
|
Fixed regression on iPad where timeline and article wouldn’t update after the selected feed was deleted
|
||||||
|
Sharing: handle feeds where the URL has unencoded space characters (why a feed would do that is beyond our ken)
|
||||||
|
|
||||||
|
### 6.0 TestFlight build 602 - 21 April 2021
|
||||||
|
|
||||||
|
Inoreader: don’t call it so often, so we don’t go over the API limits
|
||||||
|
Feedly: handle a specific case where Feedly started not returning a value we expected but didn’t actually need (we were reporting it as an error to the user, but it wasn’t)
|
||||||
|
|
@ -50,6 +50,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
|||||||
switch unwrappedAccountType {
|
switch unwrappedAccountType {
|
||||||
case .freshRSS:
|
case .freshRSS:
|
||||||
title = NSLocalizedString("FreshRSS", comment: "FreshRSS")
|
title = NSLocalizedString("FreshRSS", comment: "FreshRSS")
|
||||||
|
apiURLTextField.placeholder = NSLocalizedString("API URL: fresh.rss.net/api/greader.php", comment: "FreshRSS API Helper")
|
||||||
case .inoreader:
|
case .inoreader:
|
||||||
title = NSLocalizedString("InoReader", comment: "InoReader")
|
title = NSLocalizedString("InoReader", comment: "InoReader")
|
||||||
case .bazQux:
|
case .bazQux:
|
||||||
|
@ -133,6 +133,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
shuttingDown = true
|
shuttingDown = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
|
IconImageCache.shared.emptyCache()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
|
||||||
@objc func unreadCountDidChange(_ note: Notification) {
|
@objc func unreadCountDidChange(_ note: Notification) {
|
||||||
|
@ -79,6 +79,13 @@ class WebViewController: UIViewController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
// We need to reload the webview on the iPhone when rotation happens to clear out any old bad viewport sizes
|
||||||
|
if traitCollection.userInterfaceIdiom == .phone {
|
||||||
|
loadWebView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
|
||||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||||
@ -235,20 +242,14 @@ class WebViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
|
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
|
||||||
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
|
guard let url = article?.preferredURL else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
|
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
|
||||||
activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem
|
activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem
|
||||||
present(activityViewController, animated: true)
|
present(activityViewController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openInAppBrowser() {
|
func openInAppBrowser() {
|
||||||
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
|
guard let url = article?.preferredURL else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let vc = SFSafariViewController(url: url)
|
let vc = SFSafariViewController(url: url)
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
@ -12,28 +12,19 @@ final class IconView: UIView {
|
|||||||
|
|
||||||
var iconImage: IconImage? = nil {
|
var iconImage: IconImage? = nil {
|
||||||
didSet {
|
didSet {
|
||||||
if iconImage !== oldValue {
|
guard iconImage !== oldValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
imageView.image = iconImage?.image
|
imageView.image = iconImage?.image
|
||||||
|
if traitCollection.userInterfaceStyle == .dark {
|
||||||
if self.traitCollection.userInterfaceStyle == .dark {
|
let isDark = iconImage?.isDark ?? false
|
||||||
if self.iconImage?.isDark ?? false {
|
isDiscernable = !isDark
|
||||||
self.isDiscernable = false
|
|
||||||
self.setNeedsLayout()
|
|
||||||
} else {
|
|
||||||
self.isDiscernable = true
|
|
||||||
self.setNeedsLayout()
|
|
||||||
}
|
}
|
||||||
} else {
|
else {
|
||||||
if self.iconImage?.isBright ?? false {
|
let isBright = iconImage?.isBright ?? false
|
||||||
self.isDiscernable = false
|
isDiscernable = !isBright
|
||||||
self.setNeedsLayout()
|
|
||||||
} else {
|
|
||||||
self.isDiscernable = true
|
|
||||||
self.setNeedsLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.setNeedsLayout()
|
|
||||||
}
|
}
|
||||||
|
setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,15 +70,10 @@ final class IconView: UIView {
|
|||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
imageView.setFrameIfNotEqual(rectForImageView())
|
imageView.setFrameIfNotEqual(rectForImageView())
|
||||||
if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) {
|
updateBackgroundColor()
|
||||||
backgroundColor = AppAssets.iconBackgroundColor
|
|
||||||
} else {
|
|
||||||
backgroundColor = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension IconView {
|
private extension IconView {
|
||||||
|
|
||||||
func commonInit() {
|
func commonInit() {
|
||||||
@ -125,4 +111,11 @@ private extension IconView {
|
|||||||
return CGRect(x: 0.0, y: originY, width: viewSize.width, height: height)
|
return CGRect(x: 0.0, y: originY, width: viewSize.width, height: height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateBackgroundColor() {
|
||||||
|
if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) {
|
||||||
|
backgroundColor = AppAssets.iconBackgroundColor
|
||||||
|
} else {
|
||||||
|
backgroundColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,13 +92,13 @@ class AccountInspectorViewController: UITableViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = NSLocalizedString("Delete Account", comment: "Delete Account")
|
let title = NSLocalizedString("Remove Account", comment: "Remove Account")
|
||||||
let message: String = {
|
let message: String = {
|
||||||
switch account.type {
|
switch account.type {
|
||||||
case .feedly:
|
case .feedly:
|
||||||
return NSLocalizedString("Are you sure you want to delete this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.", comment: "Log Out and Delete Account")
|
return NSLocalizedString("Are you sure you want to remove this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.", comment: "Log Out and Remove Account")
|
||||||
default:
|
default:
|
||||||
return NSLocalizedString("Are you sure you want to delete this account? This cannot be undone.", comment: "Delete Account")
|
return NSLocalizedString("Are you sure you want to remove this account? This cannot be undone.", comment: "Remove Account")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
@ -106,7 +106,7 @@ class AccountInspectorViewController: UITableViewController {
|
|||||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
||||||
alertController.addAction(cancelAction)
|
alertController.addAction(cancelAction)
|
||||||
|
|
||||||
let markTitle = NSLocalizedString("Delete", comment: "Delete")
|
let markTitle = NSLocalizedString("Remove", comment: "Remove")
|
||||||
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
|
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
|
||||||
guard let self = self, let account = self.account else { return }
|
guard let self = self, let account = self.account else { return }
|
||||||
AccountManager.shared.deleteAccount(account)
|
AccountManager.shared.deleteAccount(account)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@ -27,10 +27,10 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="19" translatesAutoresizingMaskIntoConstraints="NO" id="mQa-0W-eVS">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="19" translatesAutoresizingMaskIntoConstraints="NO" id="mQa-0W-eVS">
|
||||||
<rect key="frame" x="20" y="11" width="334" height="22"/>
|
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name (Optional)" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="LUW-uv-piz">
|
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name (Optional)" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="LUW-uv-piz">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="334" height="22"/>
|
<rect key="frame" x="0.0" y="0.0" width="334" height="18.5"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
||||||
</textField>
|
</textField>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-Gm-p4F">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-Gm-p4F">
|
||||||
<rect key="frame" x="20" y="11.5" width="48" height="21"/>
|
<rect key="frame" x="20" y="13.5" width="40" height="17"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@ -119,7 +119,7 @@
|
|||||||
<constraint firstAttribute="height" constant="44" id="WtN-fp-Ldt"/>
|
<constraint firstAttribute="height" constant="44" id="WtN-fp-Ldt"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<state key="normal" title="Delete Account">
|
<state key="normal" title="Remove Account">
|
||||||
<color key="titleColor" systemColor="systemRedColor"/>
|
<color key="titleColor" systemColor="systemRedColor"/>
|
||||||
</state>
|
</state>
|
||||||
<state key="highlighted">
|
<state key="highlighted">
|
||||||
@ -179,7 +179,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ZdA-rl-9eP">
|
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ZdA-rl-9eP">
|
||||||
<rect key="frame" x="20" y="11" width="334" height="22"/>
|
<rect key="frame" x="20" y="13" width="334" height="18.5"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
|
||||||
</textField>
|
</textField>
|
||||||
@ -199,7 +199,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notify About New Articles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YV2-gG-lMP">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notify About New Articles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YV2-gG-lMP">
|
||||||
<rect key="frame" x="24" y="11.5" width="197" height="21"/>
|
<rect key="frame" x="24" y="13.5" width="167.5" height="17"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@ -229,7 +229,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Always Use Reader View" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bf4-3X-Rfr">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Always Use Reader View" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bf4-3X-Rfr">
|
||||||
<rect key="frame" x="24" y="11.5" width="187" height="21"/>
|
<rect key="frame" x="24" y="13.5" width="158.5" height="17"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@ -256,7 +256,7 @@
|
|||||||
<tableViewSection headerTitle="Home Page" id="dTd-6q-SZd">
|
<tableViewSection headerTitle="Home Page" id="dTd-6q-SZd">
|
||||||
<cells>
|
<cells>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="0zc-o6-Sjh" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="0zc-o6-Sjh" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||||
<rect key="frame" x="20" y="204.5" width="374" height="43.5"/>
|
<rect key="frame" x="20" y="198.5" width="374" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0zc-o6-Sjh" id="vJs-XK-ebf">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0zc-o6-Sjh" id="vJs-XK-ebf">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||||
@ -300,7 +300,7 @@
|
|||||||
<tableViewSection headerTitle="Feed URL" id="MtQ-oG-lrU">
|
<tableViewSection headerTitle="Feed URL" id="MtQ-oG-lrU">
|
||||||
<cells>
|
<cells>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fKD-Vi-B8O">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fKD-Vi-B8O">
|
||||||
<rect key="frame" x="20" y="304" width="374" height="43.5"/>
|
<rect key="frame" x="20" y="292" width="374" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fKD-Vi-B8O" id="2G0-9f-qwN">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fKD-Vi-B8O" id="2G0-9f-qwN">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||||
|
@ -23,14 +23,8 @@ class WebFeedInspectorViewController: UITableViewController {
|
|||||||
@IBOutlet weak var feedURLLabel: InteractiveLabel!
|
@IBOutlet weak var feedURLLabel: InteractiveLabel!
|
||||||
|
|
||||||
private var headerView: InspectorIconHeaderView?
|
private var headerView: InspectorIconHeaderView?
|
||||||
private var iconImage: IconImage {
|
private var iconImage: IconImage? {
|
||||||
if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: webFeed) {
|
return IconImageCache.shared.imageForFeed(webFeed)
|
||||||
return feedIcon
|
|
||||||
}
|
|
||||||
if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
|
||||||
return favicon
|
|
||||||
}
|
|
||||||
return FaviconGenerator.favicon(webFeed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private let homePageIndexPath = IndexPath(row: 0, section: 1)
|
private let homePageIndexPath = IndexPath(row: 0, section: 1)
|
||||||
|
@ -85,6 +85,12 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
IconImageCache.shared.emptyCache()
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
reloadAllVisibleCells()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
|
||||||
@objc func unreadCountDidChange(_ note: Notification) {
|
@objc func unreadCountDidChange(_ note: Notification) {
|
||||||
@ -842,36 +848,10 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
|
func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
|
||||||
cell.iconImage = imageFor(identifier)
|
guard let feedID = identifier.feedID else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
cell.iconImage = IconImageCache.shared.imageFor(feedID)
|
||||||
func imageFor(_ identifier: MasterFeedTableViewIdentifier) -> IconImage? {
|
|
||||||
guard let feedID = identifier.feedID else { return nil }
|
|
||||||
|
|
||||||
if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
|
|
||||||
return smartFeed.smallIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let feed = AccountManager.shared.existingFeed(with: feedID) else { return nil }
|
|
||||||
|
|
||||||
if let webFeed = feed as? WebFeed {
|
|
||||||
|
|
||||||
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed)
|
|
||||||
if feedIconImage != nil {
|
|
||||||
return feedIconImage
|
|
||||||
}
|
|
||||||
|
|
||||||
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
|
|
||||||
return faviconImage
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if let smallIconProvider = feed as? SmallIconProvider {
|
|
||||||
return smallIconProvider.smallIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func nameFor(_ node: Node) -> String {
|
func nameFor(_ node: Node) -> String {
|
||||||
@ -1212,8 +1192,19 @@ private extension MasterFeedViewController {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var smartFeed: Feed?
|
||||||
|
if identifier.isPsuedoFeed {
|
||||||
|
if SmartFeedsController.shared.todayFeed.feedID == identifier.feedID {
|
||||||
|
smartFeed = SmartFeedsController.shared.todayFeed
|
||||||
|
} else if SmartFeedsController.shared.unreadFeed.feedID == identifier.feedID {
|
||||||
|
smartFeed = SmartFeedsController.shared.unreadFeed
|
||||||
|
} else if SmartFeedsController.shared.starredFeed.feedID == identifier.feedID {
|
||||||
|
smartFeed = SmartFeedsController.shared.starredFeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
guard let feedID = identifier.feedID,
|
guard let feedID = identifier.feedID,
|
||||||
let feed = AccountManager.shared.existingFeed(with: feedID),
|
let feed = smartFeed ?? AccountManager.shared.existingFeed(with: feedID),
|
||||||
feed.unreadCount > 0,
|
feed.unreadCount > 0,
|
||||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||||
return nil
|
return nil
|
||||||
@ -1349,13 +1340,12 @@ private extension MasterFeedViewController {
|
|||||||
ActivityManager.cleanUp(feed)
|
ActivityManager.cleanUp(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pushUndoableCommand(deleteCommand)
|
|
||||||
deleteCommand.perform()
|
|
||||||
|
|
||||||
if indexPath == coordinator.currentFeedIndexPath {
|
if indexPath == coordinator.currentFeedIndexPath {
|
||||||
coordinator.selectFeed(indexPath: nil)
|
coordinator.selectFeed(indexPath: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushUndoableCommand(deleteCommand)
|
||||||
|
deleteCommand.perform()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -243,8 +243,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
|
|
||||||
// Set up the read action
|
// Set up the read action
|
||||||
let readTitle = article.status.read ?
|
let readTitle = article.status.read ?
|
||||||
NSLocalizedString("Unread", comment: "Unread") :
|
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
|
||||||
NSLocalizedString("Read", comment: "Read")
|
NSLocalizedString("Mark as Read", comment: "Mark as Read")
|
||||||
|
|
||||||
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in
|
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in
|
||||||
self?.coordinator.toggleRead(article)
|
self?.coordinator.toggleRead(article)
|
||||||
@ -889,9 +889,7 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openInBrowserAction(_ article: Article) -> UIAction? {
|
func openInBrowserAction(_ article: Article) -> UIAction? {
|
||||||
guard let preferredLink = article.preferredLink, let _ = URL(string: preferredLink) else {
|
guard let _ = article.preferredURL else { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
||||||
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in
|
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in
|
||||||
self?.coordinator.showBrowserForArticle(article)
|
self?.coordinator.showBrowserForArticle(article)
|
||||||
@ -900,9 +898,7 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||||
guard let preferredLink = article.preferredLink, let _ = URL(string: preferredLink) else {
|
guard let _ = article.preferredURL else { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
||||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||||
self?.coordinator.showBrowserForArticle(article)
|
self?.coordinator.showBrowserForArticle(article)
|
||||||
@ -923,10 +919,7 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
||||||
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
|
guard let url = article.preferredURL else { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = NSLocalizedString("Share", comment: "Share")
|
let title = NSLocalizedString("Share", comment: "Share")
|
||||||
let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in
|
let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in
|
||||||
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
|
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
|
||||||
@ -935,10 +928,7 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||||
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
|
guard let url = article.preferredURL else { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = NSLocalizedString("Share", comment: "Share")
|
let title = NSLocalizedString("Share", comment: "Share")
|
||||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||||
completion(true)
|
completion(true)
|
||||||
|
@ -4,17 +4,14 @@
|
|||||||
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
|
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
|
||||||
\margl1440\margr1440\vieww14220\viewh13280\viewkind0
|
\margl1440\margr1440\vieww14220\viewh13280\viewkind0
|
||||||
\deftab720
|
\deftab720
|
||||||
\pard\pardeftab720\li360\fi-360\sa60\partightenfactor0
|
\pard\pardeftab720\sa60\partightenfactor0
|
||||||
|
|
||||||
\f0\fs22 \cf2 iOS app design: {\field{\*\fldinst{HYPERLINK "https://inessential.com/"}}{\fldrslt Brent Simmons}} and {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\
|
\f0\fs22 \cf2 Lead developer: {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\
|
||||||
Lead iOS developer: {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\
|
|
||||||
App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\
|
App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\
|
||||||
\pard\pardeftab720\li366\fi-367\sa60\partightenfactor0
|
Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\
|
||||||
\cf2 Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\
|
NewsBlur syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/quanganhdo"}}{\fldrslt Anh Do}}\
|
||||||
Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\
|
Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\
|
||||||
\pard\pardeftab720\li362\fi-363\sa60\partightenfactor0
|
Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\
|
||||||
\cf2 Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\
|
Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\
|
||||||
\pard\pardeftab720\li355\fi-356\sa60\partightenfactor0
|
And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\
|
||||||
\cf2 Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\
|
}
|
||||||
\pard\pardeftab720\li358\fi-359\sa60\partightenfactor0
|
|
||||||
\cf2 And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!}
|
|
@ -1,9 +1,9 @@
|
|||||||
{\rtf1\ansi\ansicpg1252\cocoartf2511
|
{\rtf1\ansi\ansicpg1252\cocoartf2513
|
||||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
|
||||||
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
|
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
|
||||||
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
|
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
|
||||||
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||||
\deftab720
|
\deftab720
|
||||||
\pard\pardeftab720\li354\fi-355\sa60\partightenfactor0
|
\pard\pardeftab720\sa60\partightenfactor0
|
||||||
|
|
||||||
\f0\fs28 \cf2 NetNewsWire 5.0 is dedicated to all the people who showed up to help with code, design, HTML, documentation, icons, testing, and just to help talk things over and think things through. This app\'92s for you!}
|
\f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.}
|
@ -148,20 +148,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
private(set) var currentFeedIndexPath: IndexPath?
|
private(set) var currentFeedIndexPath: IndexPath?
|
||||||
|
|
||||||
var timelineIconImage: IconImage? {
|
var timelineIconImage: IconImage? {
|
||||||
if let feed = timelineFeed as? WebFeed {
|
guard let timelineFeed = timelineFeed else {
|
||||||
|
return nil
|
||||||
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: feed)
|
|
||||||
if feedIconImage != nil {
|
|
||||||
return feedIconImage
|
|
||||||
}
|
}
|
||||||
|
return IconImageCache.shared.imageForFeed(timelineFeed)
|
||||||
if let faviconIconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) {
|
|
||||||
return faviconIconImage
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return (timelineFeed as? SmallIconProvider)?.smallIcon
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var exceptionArticleFetcher: ArticleFetcher?
|
private var exceptionArticleFetcher: ArticleFetcher?
|
||||||
@ -853,11 +843,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
currentArticleViewController = articleViewController!
|
currentArticleViewController = articleViewController!
|
||||||
}
|
}
|
||||||
|
|
||||||
masterTimelineViewController?.updateArticleSelection(animations: animations)
|
// Mark article as read before navigating to it, so the read status does not flash unread/read on display
|
||||||
currentArticleViewController.article = article
|
|
||||||
|
|
||||||
markArticles(Set([article!]), statusKey: .read, flag: true)
|
markArticles(Set([article!]), statusKey: .read, flag: true)
|
||||||
|
|
||||||
|
masterTimelineViewController?.updateArticleSelection(animations: animations)
|
||||||
|
currentArticleViewController.article = article
|
||||||
}
|
}
|
||||||
|
|
||||||
func beginSearching() {
|
func beginSearching() {
|
||||||
@ -1238,16 +1228,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showBrowserForArticle(_ article: Article) {
|
func showBrowserForArticle(_ article: Article) {
|
||||||
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
|
guard let url = article.preferredURL else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
UIApplication.shared.open(url, options: [:])
|
UIApplication.shared.open(url, options: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func showBrowserForCurrentArticle() {
|
func showBrowserForCurrentArticle() {
|
||||||
guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
guard let url = currentArticle?.preferredURL else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
UIApplication.shared.open(url, options: [:])
|
UIApplication.shared.open(url, options: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ class AboutViewController: UITableViewController {
|
|||||||
let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0))
|
let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0))
|
||||||
buildLabel.font = UIFont.systemFont(ofSize: 11.0)
|
buildLabel.font = UIFont.systemFont(ofSize: 11.0)
|
||||||
buildLabel.textColor = UIColor.gray
|
buildLabel.textColor = UIColor.gray
|
||||||
buildLabel.text = NSLocalizedString("Copyright © 2002-2020 Brent Simmons", comment: "Copyright")
|
buildLabel.text = NSLocalizedString("Copyright © 2002-2021 Brent Simmons", comment: "Copyright")
|
||||||
buildLabel.numberOfLines = 0
|
buildLabel.numberOfLines = 0
|
||||||
buildLabel.sizeToFit()
|
buildLabel.sizeToFit()
|
||||||
buildLabel.translatesAutoresizingMaskIntoConstraints = false
|
buildLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -44,7 +44,7 @@ class AddExtensionPointViewController: UITableViewController, AddExtensionPointD
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||||
return NSLocalizedString("Feed Providers allow you to subscribe to web site URL's as if they were RSS feeds.", comment: "Feed Provider Footer")
|
return NSLocalizedString("Feed Providers allow you to subscribe to some pages as if they were RSS feeds.", comment: "Feed Provider Footer")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
||||||
MARKETING_VERSION = 6.0
|
MARKETING_VERSION = 6.0
|
||||||
CURRENT_PROJECT_VERSION = 601
|
CURRENT_PROJECT_VERSION = 603
|
||||||
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||||
|
Loading…
x
Reference in New Issue
Block a user