Merge branch 'master' of https://github.com/brentsimmons/NetNewsWire
This commit is contained in:
commit
3f8e7282e3
|
@ -115,6 +115,7 @@
|
||||||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; };
|
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; };
|
||||||
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */; };
|
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */; };
|
||||||
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */; };
|
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */; };
|
||||||
|
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -287,6 +288,7 @@
|
||||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = "<group>"; };
|
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = "<group>"; };
|
||||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = "<group>"; };
|
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = "<group>"; };
|
||||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = "<group>"; };
|
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = "<group>"; };
|
||||||
|
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegateError.swift; sourceTree = "<group>"; };
|
||||||
D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = "<group>"; };
|
D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = "<group>"; };
|
||||||
D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = "<group>"; };
|
D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = "<group>"; };
|
||||||
D511EEB7202422BB00712EC3 /* Account_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_release.xcconfig; sourceTree = "<group>"; };
|
D511EEB7202422BB00712EC3 /* Account_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
@ -540,6 +542,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */,
|
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */,
|
||||||
|
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */,
|
||||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */,
|
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */,
|
||||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
|
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
|
||||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */,
|
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */,
|
||||||
|
@ -783,6 +786,7 @@
|
||||||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
||||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
|
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
|
||||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
|
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
|
||||||
|
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */,
|
||||||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
|
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
|
||||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
||||||
9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */,
|
9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */,
|
||||||
|
|
|
@ -90,6 +90,11 @@ final class FeedlyAPICaller {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStream(for collection: FeedlyCollection, newerThan: Date? = nil, unreadOnly: Bool? = nil, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
func getStream(for collection: FeedlyCollection, newerThan: Date? = nil, unreadOnly: Bool? = nil, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||||
|
let id = FeedlyCategoryResourceId(id: collection.id)
|
||||||
|
getStream(for: id, newerThan: newerThan, unreadOnly: unreadOnly, completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStream(for resource: FeedlyResourceId, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||||
guard let accessToken = credentials?.secret else {
|
guard let accessToken = credentials?.secret else {
|
||||||
return DispatchQueue.main.async {
|
return DispatchQueue.main.async {
|
||||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||||
|
@ -115,7 +120,7 @@ final class FeedlyAPICaller {
|
||||||
|
|
||||||
queryItems.append(contentsOf: [
|
queryItems.append(contentsOf: [
|
||||||
URLQueryItem(name: "count", value: "1000"),
|
URLQueryItem(name: "count", value: "1000"),
|
||||||
URLQueryItem(name: "streamId", value: collection.id),
|
URLQueryItem(name: "streamId", value: resource.id),
|
||||||
])
|
])
|
||||||
|
|
||||||
components.queryItems = queryItems
|
components.queryItems = queryItems
|
||||||
|
@ -129,12 +134,6 @@ 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)
|
||||||
|
|
||||||
// URLSession.shared.dataTask(with: request) { (data, response, error) in
|
|
||||||
// let obj = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments)
|
|
||||||
// let data = try! JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted)
|
|
||||||
// print(String(data: data, encoding: .utf8)!)
|
|
||||||
// }.resume()
|
|
||||||
|
|
||||||
transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let (_, collections)):
|
case .success(let (_, collections)):
|
||||||
|
@ -387,6 +386,98 @@ final class FeedlyAPICaller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addFeed(with feedId: FeedlyFeedResourceId, title: String? = nil, toCollectionWith collectionId: String, completionHandler: @escaping (Result<[FeedlyFeed], Error>) -> ()) {
|
||||||
|
guard let accessToken = credentials?.secret else {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let encodedId = encodeForURLPath(collectionId) else {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var components = baseUrlComponents
|
||||||
|
components.percentEncodedPath = "/v3/collections/\(encodedId)/feeds"
|
||||||
|
|
||||||
|
guard let url = components.url else {
|
||||||
|
fatalError("\(components) does not produce a valid URL.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "PUT"
|
||||||
|
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||||
|
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||||
|
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||||
|
|
||||||
|
do {
|
||||||
|
struct AddFeedBody: Encodable {
|
||||||
|
var id: String
|
||||||
|
var title: String?
|
||||||
|
}
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(AddFeedBody(id: feedId.id, title: title))
|
||||||
|
request.httpBody = data
|
||||||
|
} catch {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(_, let collectionFeeds):
|
||||||
|
if let feeds = collectionFeeds {
|
||||||
|
completionHandler(.success(feeds))
|
||||||
|
} else {
|
||||||
|
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||||
|
guard let accessToken = credentials?.secret else {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let encodedCollectionId = encodeForURLPath(collectionId), let encodedFeedId = encodeForURLPath(feedId) else {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var components = baseUrlComponents
|
||||||
|
components.percentEncodedPath = "/v3/collections/\(encodedCollectionId)/feeds/\(encodedFeedId)"
|
||||||
|
|
||||||
|
guard let url = components.url else {
|
||||||
|
fatalError("\(components) does not produce a valid URL.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "DELETE"
|
||||||
|
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||||
|
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||||
|
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||||
|
|
||||||
|
transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let httpResponse, _):
|
||||||
|
if httpResponse.statusCode == 200 {
|
||||||
|
completionHandler(.success(()))
|
||||||
|
} else {
|
||||||
|
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting {
|
extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting {
|
||||||
|
|
|
@ -148,14 +148,21 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||||
|
|
||||||
|
let progress = refreshProgress
|
||||||
|
progress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
caller.createCollection(named: name) { result in
|
caller.createCollection(named: name) { result in
|
||||||
|
progress.completeTask()
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let collection):
|
case .success(let collection):
|
||||||
if let folder = account.ensureFolder(with: collection.label) {
|
if let folder = account.ensureFolder(with: collection.label) {
|
||||||
folder.externalID = collection.id
|
folder.externalID = collection.id
|
||||||
completion(.success(folder))
|
completion(.success(folder))
|
||||||
} else {
|
} else {
|
||||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
// Is the name empty? Or one of the global resource names?
|
||||||
|
completion(.failure(FeedlyAccountDelegateError.unableToAddFolder(name)))
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
|
@ -165,26 +172,40 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let id = folder.externalID else {
|
guard let id = folder.externalID else {
|
||||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
return DispatchQueue.main.async {
|
||||||
return
|
completion(.failure(FeedlyAccountDelegateError.unableToRenameFolder(folder.nameForDisplay, name)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nameBefore = folder.name
|
||||||
|
|
||||||
caller.renameCollection(with: id, to: name) { result in
|
caller.renameCollection(with: id, to: name) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let collection):
|
case .success(let collection):
|
||||||
folder.name = collection.label
|
folder.name = collection.label
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
folder.name = nameBefore
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
folder.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let id = folder.externalID else {
|
guard let id = folder.externalID else {
|
||||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
return DispatchQueue.main.async {
|
||||||
return
|
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFolder(folder.nameForDisplay)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let progress = refreshProgress
|
||||||
|
progress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
caller.deleteCollection(with: id) { result in
|
caller.deleteCollection(with: id) { result in
|
||||||
|
progress.completeTask()
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
account.removeFolder(folder)
|
account.removeFolder(folder)
|
||||||
|
@ -195,32 +216,267 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isValidContainer(for account: Account, container: Container) throws -> (Folder, String) {
|
||||||
|
guard let folder = container as? Folder else {
|
||||||
|
throw FeedlyAccountDelegateError.addFeedChooseFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let collectionId = folder.externalID else {
|
||||||
|
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let userId = credentials?.username else {
|
||||||
|
throw FeedlyAccountDelegateError.notLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
let uncategorized = FeedlyCategoryResourceId.uncategorized(for: userId)
|
||||||
|
|
||||||
|
guard collectionId != uncategorized.id else {
|
||||||
|
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (folder, collectionId)
|
||||||
|
}
|
||||||
|
|
||||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
fatalError()
|
let (folder, collectionId): (Folder, String)
|
||||||
|
do {
|
||||||
|
(folder, collectionId) = try isValidContainer(for: account, container: container)
|
||||||
|
} catch {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resourceId = FeedlyFeedResourceId(url: url)
|
||||||
|
|
||||||
|
let progress = refreshProgress
|
||||||
|
progress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
|
caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { [weak self] result in
|
||||||
|
defer { progress.completeTask() }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let feedlyFeeds):
|
||||||
|
let feedsBefore = folder.flattenedFeeds()
|
||||||
|
for feedlyFeed in feedlyFeeds where !account.hasFeed(with: feedlyFeed.feedId) {
|
||||||
|
let resourceId = FeedlyFeedResourceId(id: feedlyFeed.id)
|
||||||
|
let feed = account.createFeed(with: feedlyFeed.title,
|
||||||
|
url: resourceId.url,
|
||||||
|
feedID: feedlyFeed.id,
|
||||||
|
homePageURL: feedlyFeed.website)
|
||||||
|
folder.addFeed(feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let feedsAfter = folder.flattenedFeeds()
|
||||||
|
let added = feedsAfter.subtracting(feedsBefore)
|
||||||
|
|
||||||
|
guard let first = added.first else {
|
||||||
|
return completion(.failure(AccountError.createErrorNotFound))
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
progress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
|
if let self = self {
|
||||||
|
for feed in added {
|
||||||
|
group.enter()
|
||||||
|
let resourceId = FeedlyFeedResourceId(id: feed.feedID)
|
||||||
|
self.caller.getStream(for: resourceId, newerThan: nil, unreadOnly: nil) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let stream):
|
||||||
|
let items = Set(stream.items.map { FeedlyEntryParser(entry: $0).parsedItemRepresentation })
|
||||||
|
|
||||||
|
account.update(feed, parsedItems: items, defaultRead: false) {
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure:
|
||||||
|
// Feed will remain empty until new articles appear.
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
progress.completeTask()
|
||||||
|
completion(.success(first))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID }
|
||||||
|
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
|
||||||
|
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let feedId = FeedlyFeedResourceId(id: feed.feedID)
|
||||||
|
let editedNameBefore = feed.editedName
|
||||||
|
|
||||||
|
// Adding an existing feed updates it.
|
||||||
|
// Updating feed name in one folder/collection updates it for all folders/collections.
|
||||||
|
caller.addFeed(with: feedId, title: name, toCollectionWith: collectionId) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
feed.editedName = editedNameBefore
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimistically set the name
|
||||||
|
feed.editedName = name
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
let (folder, collectionId): (Folder, String)
|
||||||
|
do {
|
||||||
|
(folder, collectionId) = try isValidContainer(for: account, container: container)
|
||||||
|
} catch {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let feedId = FeedlyFeedResourceId(id: with.feedID)
|
||||||
|
|
||||||
|
caller.addFeed(with: feedId, toCollectionWith: collectionId) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let feedlyFeeds):
|
||||||
|
for feedlyFeed in feedlyFeeds where !folder.hasFeed(with: feedlyFeed.feedId) {
|
||||||
|
let feed: Feed = {
|
||||||
|
if with.url == FeedlyFeedResourceId(id: feedlyFeed.id).url {
|
||||||
|
with.metadata.feedID = feedlyFeed.id
|
||||||
|
with.name = feedlyFeed.title
|
||||||
|
with.homePageURL = feedlyFeed.website
|
||||||
|
return with
|
||||||
|
} else {
|
||||||
|
let resourceId = FeedlyFeedResourceId(id: feedlyFeed.id)
|
||||||
|
return account.createFeed(with: feedlyFeed.title,
|
||||||
|
url: resourceId.url,
|
||||||
|
feedID: feedlyFeed.id,
|
||||||
|
homePageURL: feedlyFeed.website)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
folder.addFeed(feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(.success(()))
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
guard let folder = container as? Folder, let collectionId = folder.externalID else {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caller.removeFeed(feed.feedID, fromCollectionWith: collectionId) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure(let error):
|
||||||
|
folder.addFeed(feed)
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.removeFeed(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
guard let from = from as? Folder, let to = to as? Folder else {
|
||||||
|
return DispatchQueue.main.async {
|
||||||
|
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFeed(for: account, with: feed, to: to) { [weak self] addResult in
|
||||||
|
switch addResult {
|
||||||
|
// now that we have added the feed, remove it from the other collection
|
||||||
|
case .success:
|
||||||
|
self?.removeFeed(for: account, with: feed, from: from) { removeResult in
|
||||||
|
switch removeResult {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure:
|
||||||
|
from.addFeed(feed)
|
||||||
|
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
from.addFeed(feed)
|
||||||
|
to.removeFeed(feed)
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimistically move the feed, undoing as appropriate to the failure
|
||||||
|
from.removeFeed(feed)
|
||||||
|
to.addFeed(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||||
|
account.addFeed(existingFeed, to: container) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
for feed in folder.topLevelFeeds {
|
||||||
|
|
||||||
|
folder.topLevelFeeds.remove(feed)
|
||||||
|
|
||||||
|
group.enter()
|
||||||
|
restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||||
|
group.leave()
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
account.addFolder(folder)
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
//
|
||||||
|
// FeedlyAccountDelegateError.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Kiel Gillard on 9/10/19.
|
||||||
|
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum FeedlyAccountDelegateError: LocalizedError {
|
||||||
|
case notLoggedIn
|
||||||
|
case unableToAddFolder(String)
|
||||||
|
case unableToRenameFolder(String, String)
|
||||||
|
case unableToRemoveFolder(String)
|
||||||
|
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
|
||||||
|
case addFeedChooseFolder
|
||||||
|
case addFeedInvalidFolder(Folder)
|
||||||
|
case unableToRemoveFeed(Feed)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .notLoggedIn:
|
||||||
|
return NSLocalizedString("Please add the Feedly account again.", comment: "Feedly - Credentials not found.")
|
||||||
|
|
||||||
|
case .unableToAddFolder(let name):
|
||||||
|
let template = NSLocalizedString("Could not create a folder named \"%@\".", comment: "Feedly - Could not create a folder/collection.")
|
||||||
|
return String(format: template, name)
|
||||||
|
|
||||||
|
case .unableToRenameFolder(let from, let to):
|
||||||
|
let template = NSLocalizedString("Could not rename \"%@\" to \"%@\".", comment: "Feedly - Could not rename a folder/collection.")
|
||||||
|
return String(format: template, from, to)
|
||||||
|
|
||||||
|
case .unableToRemoveFolder(let name):
|
||||||
|
let template = NSLocalizedString("Could not remove the folder named \"%@\".", comment: "Feedly - Could not remove a folder/collection.")
|
||||||
|
return String(format: template, name)
|
||||||
|
|
||||||
|
case .unableToMoveFeedBetweenFolders(let feed, _, let to):
|
||||||
|
let template = NSLocalizedString("Could not move \"%@\" to \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.")
|
||||||
|
return String(format: template, feed.nameForDisplay, to.nameForDisplay)
|
||||||
|
|
||||||
|
case .addFeedChooseFolder:
|
||||||
|
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly - Feed can only be added to folders.")
|
||||||
|
|
||||||
|
case .addFeedInvalidFolder(let invalidFolder):
|
||||||
|
let template = NSLocalizedString("Feeds cannot be added to the \"%@\" folder.", comment: "Feedly - Feed can only be added to folders.")
|
||||||
|
return String(format: template, invalidFolder.nameForDisplay)
|
||||||
|
|
||||||
|
case .unableToRemoveFeed(let feed):
|
||||||
|
let template = NSLocalizedString("Could not remove \"%@\".", comment: "Feedly - Could not remove a feed.")
|
||||||
|
return String(format: template, feed.nameForDisplay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var recoverySuggestion: String? {
|
||||||
|
switch self {
|
||||||
|
case .notLoggedIn:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .unableToAddFolder:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .unableToRenameFolder:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .unableToRemoveFolder:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .unableToMoveFeedBetweenFolders(let feed, let from, let to):
|
||||||
|
let template = NSLocalizedString("\"%@\" may be in both \"%@\" and \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.")
|
||||||
|
return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)
|
||||||
|
|
||||||
|
case .addFeedChooseFolder:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .addFeedInvalidFolder:
|
||||||
|
return NSLocalizedString("Please choose a different folder to contain the feed.", comment: "Feedly - Feed can only be added to folders recovery suggestion.")
|
||||||
|
|
||||||
|
case .unableToRemoveFeed:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,17 +13,15 @@ protocol FeedlyResourceId {
|
||||||
|
|
||||||
/// The resource Id from Feedly.
|
/// The resource Id from Feedly.
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
|
|
||||||
/// The location of the kind of resource a concrete type represents.
|
|
||||||
/// If the conrete type cannot strip the resource type from the Id, it should just return the Id
|
|
||||||
/// since the Id is a legitimate URL.
|
|
||||||
var url: String { get }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
|
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
|
||||||
struct FeedlyFeedResourceId: FeedlyResourceId {
|
struct FeedlyFeedResourceId: FeedlyResourceId {
|
||||||
var id: String
|
var id: String
|
||||||
|
|
||||||
|
/// The location of the kind of resource a concrete type represents.
|
||||||
|
/// If the conrete type cannot strip the resource type from the Id, it should just return the Id
|
||||||
|
/// since the Id is a legitimate URL.
|
||||||
var url: String {
|
var url: String {
|
||||||
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
|
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
|
||||||
var mutant = id
|
var mutant = id
|
||||||
|
@ -35,3 +33,19 @@ struct FeedlyFeedResourceId: FeedlyResourceId {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FeedlyFeedResourceId {
|
||||||
|
init(url: String) {
|
||||||
|
self.id = "feed/\(url)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeedlyCategoryResourceId: FeedlyResourceId {
|
||||||
|
var id: String
|
||||||
|
|
||||||
|
static func uncategorized(for userId: String) -> FeedlyCategoryResourceId {
|
||||||
|
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||||
|
let id = "user/\(userId)/category/global.uncategorized"
|
||||||
|
return FeedlyCategoryResourceId(id: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -140,6 +140,18 @@ figcaption {
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
vertical-align: bottom;
|
||||||
|
position: relative;
|
||||||
|
top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.iframeWrap {
|
.iframeWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -184,6 +184,9 @@
|
||||||
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
||||||
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; };
|
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; };
|
||||||
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
|
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
|
||||||
|
51E149B3234D82E40004F7A5 /* PasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E149B2234D82E40004F7A5 /* PasswordField.swift */; };
|
||||||
|
51E149C0234D839E0004F7A5 /* ShowHidePasswordView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */; };
|
||||||
|
51E149C2234D852F0004F7A5 /* ShowHidePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */; };
|
||||||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
|
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
|
||||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; };
|
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; };
|
||||||
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
|
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
|
||||||
|
@ -802,6 +805,9 @@
|
||||||
51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
|
51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
|
||||||
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
||||||
51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
|
51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
|
||||||
|
51E149B2234D82E40004F7A5 /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = "<group>"; };
|
||||||
|
51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowHidePasswordView.xib; sourceTree = "<group>"; };
|
||||||
|
51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowHidePasswordView.swift; sourceTree = "<group>"; };
|
||||||
51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||||
51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||||
51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = "<group>"; };
|
51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1244,6 +1250,9 @@
|
||||||
DF999FF622B5AEFA0064B687 /* SafariView.swift */,
|
DF999FF622B5AEFA0064B687 /* SafariView.swift */,
|
||||||
51322858232FDDB80033D4ED /* VibrantButtonStyle.swift */,
|
51322858232FDDB80033D4ED /* VibrantButtonStyle.swift */,
|
||||||
51322854232EED360033D4ED /* VibrantSelectAction.swift */,
|
51322854232EED360033D4ED /* VibrantSelectAction.swift */,
|
||||||
|
51E149B2234D82E40004F7A5 /* PasswordField.swift */,
|
||||||
|
51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */,
|
||||||
|
51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */,
|
||||||
);
|
);
|
||||||
path = "SwiftUI Extensions";
|
path = "SwiftUI Extensions";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2206,15 +2215,11 @@
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
513C5CE5232571C2003D4054 = {
|
513C5CE5232571C2003D4054 = {
|
||||||
CreatedOnToolsVersion = 11.0;
|
CreatedOnToolsVersion = 11.0;
|
||||||
DevelopmentTeam = M8L2WTLA8W;
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
};
|
};
|
||||||
6581C73220CED60000F4AD34 = {
|
6581C73220CED60000F4AD34 = {
|
||||||
};
|
};
|
||||||
840D617B2029031C009BC708 = {
|
840D617B2029031C009BC708 = {
|
||||||
CreatedOnToolsVersion = 9.3;
|
CreatedOnToolsVersion = 9.3;
|
||||||
DevelopmentTeam = M8L2WTLA8W;
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.BackgroundModes = {
|
com.apple.BackgroundModes = {
|
||||||
enabled = 1;
|
enabled = 1;
|
||||||
|
@ -2223,8 +2228,6 @@
|
||||||
};
|
};
|
||||||
849C645F1ED37A5D003D8FC0 = {
|
849C645F1ED37A5D003D8FC0 = {
|
||||||
CreatedOnToolsVersion = 8.2.1;
|
CreatedOnToolsVersion = 8.2.1;
|
||||||
DevelopmentTeam = M8L2WTLA8W;
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.HardenedRuntime = {
|
com.apple.HardenedRuntime = {
|
||||||
enabled = 1;
|
enabled = 1;
|
||||||
|
@ -2233,8 +2236,6 @@
|
||||||
};
|
};
|
||||||
849C64701ED37A5D003D8FC0 = {
|
849C64701ED37A5D003D8FC0 = {
|
||||||
CreatedOnToolsVersion = 8.2.1;
|
CreatedOnToolsVersion = 8.2.1;
|
||||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
|
||||||
ProvisioningStyle = Automatic;
|
|
||||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2481,6 +2482,7 @@
|
||||||
51F85BED227251DF00C787DC /* Acknowledgments.rtf in Resources */,
|
51F85BED227251DF00C787DC /* Acknowledgments.rtf in Resources */,
|
||||||
511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */,
|
511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */,
|
||||||
51C452AB22650DC600C03939 /* template.html in Resources */,
|
51C452AB22650DC600C03939 /* template.html in Resources */,
|
||||||
|
51E149C0234D839E0004F7A5 /* ShowHidePasswordView.xib in Resources */,
|
||||||
51F85BF12272524100C787DC /* Credits.rtf in Resources */,
|
51F85BF12272524100C787DC /* Credits.rtf in Resources */,
|
||||||
84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */,
|
84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */,
|
||||||
511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */,
|
511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */,
|
||||||
|
@ -2709,6 +2711,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */,
|
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */,
|
||||||
|
51E149B3234D82E40004F7A5 /* PasswordField.swift in Sources */,
|
||||||
512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */,
|
512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */,
|
||||||
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */,
|
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */,
|
||||||
51EF0F79227716380050506E /* ColorHash.swift in Sources */,
|
51EF0F79227716380050506E /* ColorHash.swift in Sources */,
|
||||||
|
@ -2777,6 +2780,7 @@
|
||||||
51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */,
|
51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */,
|
||||||
51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */,
|
51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */,
|
||||||
51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */,
|
51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */,
|
||||||
|
51E149C2234D852F0004F7A5 /* ShowHidePasswordView.swift in Sources */,
|
||||||
51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */,
|
51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */,
|
||||||
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */,
|
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */,
|
||||||
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
|
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
set -v
|
set -v
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Unencrypt our provisioning profile, certificate, and private key
|
||||||
|
openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/profile/NetNewsWire.provisionprofile.enc -d -a -out buildscripts/profile/NetNewsWire.provisionprofile
|
||||||
openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/certs/dev.cer.enc -d -a -out buildscripts/certs/dev.cer
|
openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/certs/dev.cer.enc -d -a -out buildscripts/certs/dev.cer
|
||||||
openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/certs/dev.p12.enc -d -a -out buildscripts/certs/dev.p12
|
openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/certs/dev.p12.enc -d -a -out buildscripts/certs/dev.p12
|
||||||
|
|
||||||
|
# Put the certificates and private key in the Keychain, set ACL permissions, and make default
|
||||||
security create-keychain -p github-actions github-build.keychain
|
security create-keychain -p github-actions github-build.keychain
|
||||||
security import buildscripts/certs/apple.cer -k ~/Library/Keychains/github-build.keychain -A
|
security import buildscripts/certs/apple.cer -k ~/Library/Keychains/github-build.keychain -A
|
||||||
security import buildscripts/certs/dev.cer -k ~/Library/Keychains/github-build.keychain -A
|
security import buildscripts/certs/dev.cer -k ~/Library/Keychains/github-build.keychain -A
|
||||||
|
@ -12,9 +15,18 @@ security import buildscripts/certs/dev.p12 -k ~/Library/Keychains/github-build.k
|
||||||
security set-key-partition-list -S apple-tool:,apple: -s -k github-actions github-build.keychain
|
security set-key-partition-list -S apple-tool:,apple: -s -k github-actions github-build.keychain
|
||||||
security default-keychain -s github-build.keychain
|
security default-keychain -s github-build.keychain
|
||||||
|
|
||||||
rm -f ./buildscripts/certs/dev.cer
|
# Copy the provisioning profile
|
||||||
rm -f ./buildscripts/certs/dev.p12
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
cp buildscripts/profile/NetNewsWire.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||||
|
|
||||||
xcodebuild -scheme 'NetNewsWire' -configuration Release -allowProvisioningUpdates -showBuildTimingSummary
|
# Delete the decrypted files
|
||||||
|
rm -f buildscripts/profile/NetNewsWire.provisionprofile
|
||||||
|
rm -f buildscripts/certs/dev.cer
|
||||||
|
rm -f buildscripts/certs/dev.p12
|
||||||
|
|
||||||
security delete-keychain github-build.keychain
|
# Do the build
|
||||||
|
xcodebuild -scheme 'NetNewsWire' -configuration Release -showBuildTimingSummary
|
||||||
|
|
||||||
|
# Delete the keychain and the provisioningi profile
|
||||||
|
security delete-keychain github-build.keychain
|
||||||
|
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/NetNewsWire.provisionprofile
|
|
@ -141,6 +141,18 @@ figcaption {
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
vertical-align: bottom;
|
||||||
|
position: relative;
|
||||||
|
top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.iframeWrap {
|
.iframeWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -31,7 +31,7 @@ struct SettingsFeedbinAccountView : View {
|
||||||
TextField("Email", text: $viewModel.email)
|
TextField("Email", text: $viewModel.email)
|
||||||
.keyboardType(.emailAddress)
|
.keyboardType(.emailAddress)
|
||||||
.textContentType(.emailAddress)
|
.textContentType(.emailAddress)
|
||||||
SecureField("Password", text: $viewModel.password)
|
PasswordField(password: $viewModel.password)
|
||||||
}
|
}
|
||||||
Section(footer:
|
Section(footer:
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// PasswordField.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 10/8/19.
|
||||||
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PasswordField: UIViewRepresentable {
|
||||||
|
|
||||||
|
let password: Binding<String>
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> ShowHidePasswordView {
|
||||||
|
let showHideView = Bundle.main.loadNibNamed("ShowHidePasswordView", owner: Self.self, options: nil)?[0] as! ShowHidePasswordView
|
||||||
|
showHideView.passwordTextField.bindingString = password
|
||||||
|
return showHideView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ showHideView: ShowHidePasswordView, context: Context) {
|
||||||
|
showHideView.passwordTextField.bindingString = password
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// ShowHidePasswordView.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 10/8/19.
|
||||||
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class ShowHidePasswordView: UIView {
|
||||||
|
|
||||||
|
@IBOutlet weak var passwordTextField: BindingTextField!
|
||||||
|
@IBOutlet weak var showHideButton: UIButton!
|
||||||
|
|
||||||
|
@IBAction func toggleShowHideButton(_ sender: Any) {
|
||||||
|
if passwordTextField.isSecureTextEntry {
|
||||||
|
passwordTextField.isSecureTextEntry = false
|
||||||
|
showHideButton.setTitle(NSLocalizedString("Hide", comment: "Hide"), for: .normal)
|
||||||
|
} else {
|
||||||
|
passwordTextField.isSecureTextEntry = true
|
||||||
|
showHideButton.setTitle(NSLocalizedString("Show", comment: "Show"), for: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class BindingTextField: UITextField, UITextFieldDelegate {
|
||||||
|
|
||||||
|
var bindingString: Binding<String>? = nil
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
if let currentValue = textField.text as NSString? {
|
||||||
|
let proposedValue = currentValue.replacingCharacters(in: range, with: string)
|
||||||
|
bindingString?.wrappedValue = proposedValue
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15508"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ShowHidePasswordView" customModule="NetNewsWire" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="284" height="54"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TT2-5T-DdA">
|
||||||
|
<rect key="frame" x="246" y="12" width="38" height="30"/>
|
||||||
|
<state key="normal" title="Show"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleShowHideButton:" destination="iN0-l3-epB" eventType="touchUpInside" id="DTB-2f-JoB"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Iwe-LN-7ZI" customClass="BindingTextField" customModule="NetNewsWire" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="16.5" width="238" height="21"/>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
|
<textInputTraits key="textInputTraits" secureTextEntry="YES"/>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="TT2-5T-DdA" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="Buq-YS-fG8"/>
|
||||||
|
<constraint firstItem="TT2-5T-DdA" firstAttribute="leading" secondItem="Iwe-LN-7ZI" secondAttribute="trailing" constant="8" symbolic="YES" id="FIM-1x-LoT"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="TT2-5T-DdA" secondAttribute="trailing" id="b4w-k0-zUR"/>
|
||||||
|
<constraint firstItem="Iwe-LN-7ZI" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="eGg-IF-dp6"/>
|
||||||
|
<constraint firstItem="Iwe-LN-7ZI" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="mUI-zV-GHb"/>
|
||||||
|
</constraints>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="passwordTextField" destination="Iwe-LN-7ZI" id="Tvk-Q4-kHr"/>
|
||||||
|
<outlet property="showHideButton" destination="TT2-5T-DdA" id="1GH-1O-ma0"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="43.478260869565219" y="-127.23214285714285"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
</document>
|
|
@ -1,6 +1,7 @@
|
||||||
CODE_SIGN_IDENTITY = Mac Developer
|
CODE_SIGN_IDENTITY = Developer ID Application
|
||||||
DEVELOPMENT_TEAM = M8L2WTLA8W
|
DEVELOPMENT_TEAM = M8L2WTLA8W
|
||||||
CODE_SIGN_STYLE = Automatic
|
CODE_SIGN_STYLE = Manual
|
||||||
|
ORGANIZATION_IDENTIFIER = com.ranchero
|
||||||
PROVISIONING_PROFILE_SPECIFIER =
|
PROVISIONING_PROFILE_SPECIFIER =
|
||||||
|
|
||||||
// developers can locally override the Xcode settings for code signing
|
// developers can locally override the Xcode settings for code signing
|
||||||
|
@ -37,6 +38,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||||
CODE_SIGN_ENTITLEMENTS = Mac/Resources/NetNewsWire.entitlements
|
CODE_SIGN_ENTITLEMENTS = Mac/Resources/NetNewsWire.entitlements
|
||||||
INFOPLIST_FILE = Mac/Resources/Info.plist
|
INFOPLIST_FILE = Mac/Resources/Info.plist
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.NetNewsWire-Evergreen
|
PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen
|
||||||
PRODUCT_NAME = NetNewsWire
|
PRODUCT_NAME = NetNewsWire
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = Mac/NetNewsWire-Bridging-Header.h
|
SWIFT_OBJC_BRIDGING_HEADER = Mac/NetNewsWire-Bridging-Header.h
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
CODE_SIGN_IDENTITY = Mac Developer
|
CODE_SIGN_IDENTITY = Developer ID Application
|
||||||
DEVELOPMENT_TEAM = M8L2WTLA8W
|
DEVELOPMENT_TEAM = M8L2WTLA8W
|
||||||
CODE_SIGN_STYLE = Automatic
|
CODE_SIGN_STYLE = Manual
|
||||||
|
ORGANIZATION_IDENTIFIER = com.ranchero
|
||||||
PROVISIONING_PROFILE_SPECIFIER =
|
PROVISIONING_PROFILE_SPECIFIER =
|
||||||
|
|
||||||
// developers can locally override the Xcode settings for code signing
|
// developers can locally override the Xcode settings for code signing
|
||||||
|
@ -16,6 +17,7 @@ PROVISIONING_PROFILE_SPECIFIER =
|
||||||
// CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
|
// CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
|
||||||
// CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = iPhone Developer
|
// CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = iPhone Developer
|
||||||
// DEVELOPMENT_TEAM = <Your Team ID>
|
// DEVELOPMENT_TEAM = <Your Team ID>
|
||||||
|
// ORGANIZATION_IDENTIFIER = <Your Domain Name Reversed>
|
||||||
// CODE_SIGN_STYLE = Automatic
|
// CODE_SIGN_STYLE = Automatic
|
||||||
// PROVISIONING_PROFILE_SPECIFIER =
|
// PROVISIONING_PROFILE_SPECIFIER =
|
||||||
//
|
//
|
||||||
|
@ -34,7 +36,7 @@ PROVISIONING_PROFILE_SPECIFIER =
|
||||||
CODE_SIGN_ENTITLEMENTS = Mac/SafariExtension/Subscribe_to_Feed.entitlements
|
CODE_SIGN_ENTITLEMENTS = Mac/SafariExtension/Subscribe_to_Feed.entitlements
|
||||||
INFOPLIST_FILE = Mac/SafariExtension/Info.plist
|
INFOPLIST_FILE = Mac/SafariExtension/Info.plist
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.NetNewsWire-Evergreen.Subscribe-to-Feed
|
PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen.Subscribe-to-Feed
|
||||||
PRODUCT_NAME = $(TARGET_NAME)
|
PRODUCT_NAME = $(TARGET_NAME)
|
||||||
|
|
||||||
SDKROOT = macosx
|
SDKROOT = macosx
|
||||||
|
|
Loading…
Reference in New Issue