Merge branch 'master' into feature/feed-wrangler

# Conflicts:
#	Frameworks/Account/Account.xcodeproj/project.pbxproj
#	submodules/RSWeb
This commit is contained in:
Jonathan Bennett
2019-10-17 13:34:54 -04:00
17 changed files with 624 additions and 499 deletions

View File

@@ -20,7 +20,7 @@
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */; };
513323082281070D00C30F19 /* AccountFeedSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedSyncTest.swift */; };
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; };
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; };
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; };
5133230E2281089500C30F19 /* icons.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230D2281089500C30F19 /* icons.json */; };
@@ -31,7 +31,7 @@
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */; };
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */; };
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB42324FF8C0057B0E7 /* Credentials.swift */; };
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */; };
5165D7122282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift */; };
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71322821C2400D9D53D /* taggings_delete.json */; };
5165D71722821C2400D9D53D /* taggings_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71422821C2400D9D53D /* taggings_add.json */; };
5165D71822821C2400D9D53D /* taggings_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71522821C2400D9D53D /* taggings_initial.json */; };
@@ -45,7 +45,7 @@
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
51D5875C227F630B00900287 /* tags_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58759227F630B00900287 /* tags_initial.json */; };
51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */; };
51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5875D227F643C00900287 /* AccountFeedbinFolderSyncTest.swift */; };
51E148EC234B8FFC0004F7A5 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E148EB234B8FFC0004F7A5 /* SyncDatabase.framework */; };
51E148ED234B8FFC0004F7A5 /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51E148EB234B8FFC0004F7A5 /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB40229AF61B00645299 /* AccountError.swift */; };
@@ -206,7 +206,7 @@
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; };
510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMetadataFile.swift; sourceTree = "<group>"; };
513323072281070C00C30F19 /* AccountFeedSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedSyncTest.swift; sourceTree = "<group>"; };
513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = "<group>"; };
513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; };
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
5133230D2281089500C30F19 /* icons.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = icons.json; sourceTree = "<group>"; };
@@ -217,7 +217,7 @@
515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+RSWeb.swift"; sourceTree = "<group>"; };
515E4EB42324FF8C0057B0E7 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderContentsSyncTest.swift; sourceTree = "<group>"; };
5165D7112282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinFolderContentsSyncTest.swift; sourceTree = "<group>"; };
5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = "<group>"; };
5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = "<group>"; };
5165D71522821C2400D9D53D /* taggings_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_initial.json; sourceTree = "<group>"; };
@@ -232,7 +232,7 @@
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
51D58759227F630B00900287 /* tags_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_initial.json; sourceTree = "<group>"; };
51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderSyncTest.swift; sourceTree = "<group>"; };
51D5875D227F643C00900287 /* AccountFeedbinFolderSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinFolderSyncTest.swift; sourceTree = "<group>"; };
51E148EB234B8FFC0004F7A5 /* SyncDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SyncDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
51E3EB40229AF61B00645299 /* AccountError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountError.swift; sourceTree = "<group>"; };
51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = "<group>"; };
@@ -368,6 +368,16 @@
path = FeedWrangler;
sourceTree = "<group>";
};
5111D71C2357534700737D45 /* Feedbin */ = {
isa = PBXGroup;
children = (
513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */,
5165D7112282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift */,
51D5875D227F643C00900287 /* AccountFeedbinFolderSyncTest.swift */,
);
path = Feedbin;
sourceTree = "<group>";
};
515E4EB12324FF7D0057B0E7 /* Credentials */ = {
isa = PBXGroup;
children = (
@@ -530,11 +540,9 @@
isa = PBXGroup;
children = (
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */,
51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */,
513323072281070C00C30F19 /* AccountFeedSyncTest.swift */,
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */,
5107A09C227DE77700C7C3C5 /* TestTransport.swift */,
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */,
5111D71C2357534700737D45 /* Feedbin */,
9E7F15082341E97100F860D1 /* Feedly */,
51D58756227F62E300900287 /* JSON */,
848935061F62485000CEBD24 /* Info.plist */,
@@ -989,10 +997,10 @@
buildActionMask = 2147483647;
files = (
9E7F15072341E96700F860D1 /* AccountFeedlySyncTest.swift in Sources */,
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */,
51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */,
5165D7122282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift in Sources */,
51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */,
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
513323082281070D00C30F19 /* AccountFeedSyncTest.swift in Sources */,
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */,
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */,
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */,

View File

@@ -1,5 +1,5 @@
//
// AccountFolderContentsSyncTest.swift
// AccountFeedbinFolderContentsSyncTest.swift
// AccountTests
//
// Created by Maurice Parker on 5/7/19.
@@ -9,7 +9,7 @@
import XCTest
@testable import Account
class AccountFolderContentsSyncTest: XCTestCase {
class AccountFeedbinFolderContentsSyncTest: XCTestCase {
override func setUp() {
}

View File

@@ -1,5 +1,5 @@
//
// AccountFolderSyncTest.swift
// AccountFeedbinFolderSyncTest.swift
// AccountTests
//
// Created by Maurice Parker on 5/5/19.
@@ -9,7 +9,7 @@
import XCTest
@testable import Account
class AccountFolderSyncTest: XCTestCase {
class AccountFeedbinFolderSyncTest: XCTestCase {
override func setUp() {
}

View File

@@ -1,5 +1,5 @@
//
// AccountFullSyncTest.swift
// AccountFeedbinSyncTest.swift
// AccountTests
//
// Created by Maurice Parker on 5/6/19.
@@ -9,7 +9,7 @@
import XCTest
@testable import Account
class AccountFeedSyncTest: XCTestCase {
class AccountFeedbinSyncTest: XCTestCase {
override func setUp() {
}
@@ -27,8 +27,13 @@ class AccountFeedSyncTest: XCTestCase {
// Test initial folders
let initialExpection = self.expectation(description: "Initial feeds")
account.refreshAll() { _ in
initialExpection.fulfill()
account.refreshAll() { result in
switch result {
case .success:
initialExpection.fulfill()
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
waitForExpectations(timeout: 5, handler: nil)
@@ -41,11 +46,16 @@ class AccountFeedSyncTest: XCTestCase {
XCTAssertEqual("https://favicons.feedbinusercontent.com/6ac/6acc098f35ed2bcc0915ca89d50a97e5793eda45.png", daringFireball!.faviconURL)
// Test Adding a Feed
testTransport.testFiles["https://api.feedbin.com/v2/subscriptions.json"] = "subscriptions_add.json"
testTransport.testFiles["subscriptions.json"] = "subscriptions_add.json"
let addExpection = self.expectation(description: "Add feeds")
account.refreshAll() { _ in
addExpection.fulfill()
account.refreshAll() { result in
switch result {
case .success:
addExpection.fulfill()
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
waitForExpectations(timeout: 5, handler: nil)

View File

@@ -339,9 +339,13 @@ final class FeedbinAPICaller: NSObject {
let concatIDs = articleIDs.reduce("") { param, articleID in return param + ",\(articleID)" }
let paramIDs = String(concatIDs.dropFirst())
var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)!
callComponents.queryItems = [URLQueryItem(name: "ids", value: paramIDs), URLQueryItem(name: "mode", value: "extended")]
let request = URLRequest(url: callComponents.url!, credentials: credentials)
let url = feedbinBaseURL
.appendingPathComponent("entries.json")
.appendingQueryItems([
URLQueryItem(name: "ids", value: paramIDs),
URLQueryItem(name: "mode", value: "extended")
])
let request = URLRequest(url: url!, credentials: credentials)
transport.send(request: request, resultType: [FeedbinEntry].self) { result in
@@ -361,9 +365,14 @@ final class FeedbinAPICaller: NSObject {
let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
let sinceString = FeedbinDate.formatter.string(from: since)
var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("feeds/\(feedID)/entries.json"), resolvingAgainstBaseURL: false)!
callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")]
let request = URLRequest(url: callComponents.url!, credentials: credentials)
let url = feedbinBaseURL
.appendingPathComponent("feeds/\(feedID)/entries.json")
.appendingQueryItems([
URLQueryItem(name: "since", value: sinceString),
URLQueryItem(name: "per_page", value: "100"),
URLQueryItem(name: "mode", value: "extended")
])
let request = URLRequest(url: url!, credentials: credentials)
transport.send(request: request, resultType: [FeedbinEntry].self) { result in
@@ -392,9 +401,14 @@ final class FeedbinAPICaller: NSObject {
}()
let sinceString = FeedbinDate.formatter.string(from: since)
var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)!
callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")]
let request = URLRequest(url: callComponents.url!, credentials: credentials)
let url = feedbinBaseURL
.appendingPathComponent("entries.json")
.appendingQueryItems([
URLQueryItem(name: "since", value: sinceString),
URLQueryItem(name: "per_page", value: "100"),
URLQueryItem(name: "mode", value: "extended")
])
let request = URLRequest(url: url!, credentials: credentials)
transport.send(request: request, resultType: [FeedbinEntry].self) { result in

View File

@@ -166,17 +166,11 @@ final class ReaderAPICaller: NSObject {
return
}
// Add query string for getting JSON (probably should break this out as I will be doing it a lot)
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.tagList.rawValue), resolvingAgainstBaseURL: false) else {
completion(.failure(TransportError.noURL))
return
}
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.tagList.rawValue)
.appendingQueryItem(URLQueryItem(name: "output", value: "json"))
components.queryItems = [
URLQueryItem(name: "output", value: "json")
]
guard let callURL = components.url else {
guard let callURL = url else {
completion(.failure(TransportError.noURL))
return
}
@@ -278,17 +272,11 @@ final class ReaderAPICaller: NSObject {
return
}
// Add query string for getting JSON (probably should break this out as I will be doing it a lot)
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionList.rawValue), resolvingAgainstBaseURL: false) else {
completion(.failure(TransportError.noURL))
return
}
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.subscriptionList.rawValue)
.appendingQueryItem(URLQueryItem(name: "output", value: "json"))
components.queryItems = [
URLQueryItem(name: "output", value: "json")
]
guard let callURL = components.url else {
guard let callURL = url else {
completion(.failure(TransportError.noURL))
return
}
@@ -333,16 +321,11 @@ final class ReaderAPICaller: NSObject {
self.requestAuthorizationToken(endpoint: baseURL) { (result) in
switch result {
case .success(let token):
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue), resolvingAgainstBaseURL: false) else {
completion(.failure(TransportError.noURL))
return
}
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue)
.appendingQueryItem(URLQueryItem(name: "quickadd", value: url.absoluteString))
components.queryItems = [
URLQueryItem(name: "quickadd", value: url.absoluteString)
]
guard let callURL = components.url else {
guard let callURL = url else {
completion(.failure(TransportError.noURL))
return
}
@@ -616,19 +599,15 @@ final class ReaderAPICaller: NSObject {
return
}
// Add query string for getting JSON (probably should break this out as I will be doing it a lot)
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else {
completion(.failure(TransportError.noURL))
return
}
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue)
.appendingQueryItems([
URLQueryItem(name: "s", value: feedID),
URLQueryItem(name: "ot", value: String(since.timeIntervalSince1970)),
URLQueryItem(name: "output", value: "json")
])
components.queryItems = [
URLQueryItem(name: "s", value: feedID),
URLQueryItem(name: "ot", value: String(since.timeIntervalSince1970)),
URLQueryItem(name: "output", value: "json")
]
guard let callURL = components.url else {
guard let callURL = url else {
completion(.failure(TransportError.noURL))
return
}
@@ -684,22 +663,17 @@ final class ReaderAPICaller: NSObject {
}()
let sinceString = since.timeIntervalSince1970
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue)
.appendingQueryItems([
URLQueryItem(name: "o", value: String(sinceString)),
URLQueryItem(name: "n", value: "10000"),
URLQueryItem(name: "output", value: "json"),
URLQueryItem(name: "xt", value: ReaderState.read.rawValue),
URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue)
])
// Add query string for getting JSON (probably should break this out as I will be doing it a lot)
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else {
completion(.failure(TransportError.noURL))
return
}
components.queryItems = [
URLQueryItem(name: "o", value: String(sinceString)),
URLQueryItem(name: "n", value: "10000"),
URLQueryItem(name: "output", value: "json"),
URLQueryItem(name: "xt", value: ReaderState.read.rawValue),
URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue)
]
guard let callURL = components.url else {
guard let callURL = url else {
completion(.failure(TransportError.noURL))
return
}
@@ -768,13 +742,11 @@ final class ReaderAPICaller: NSObject {
func retrieveEntries(page: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) {
guard let url = URL(string: page), var callComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
guard let url = URL(string: page)?.appendingQueryItem(URLQueryItem(name: "mode", value: "extended")) else {
completion(.success((nil, nil)))
return
}
callComponents.queryItems?.append(URLQueryItem(name: "mode", value: "extended"))
let request = URLRequest(url: callComponents.url!, credentials: credentials)
let request = URLRequest(url: url, credentials: credentials)
transport.send(request: request, resultType: [ReaderAPIEntry].self) { result in
@@ -800,20 +772,16 @@ final class ReaderAPICaller: NSObject {
return
}
// Add query string for getting JSON (probably should break this out as I will be doing it a lot)
guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else {
completion(.failure(TransportError.noURL))
return
}
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue)
.appendingQueryItems([
URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue),
URLQueryItem(name: "n", value: "10000"),
URLQueryItem(name: "xt", value: ReaderState.read.rawValue),
URLQueryItem(name: "output", value: "json")
])
components.queryItems = [
URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue),
URLQueryItem(name: "n", value: "10000"),
URLQueryItem(name: "xt", value: ReaderState.read.rawValue),
URLQueryItem(name: "output", value: "json")
]
guard let callURL = components.url else {
guard let callURL = url else {
completion(.failure(TransportError.noURL))
return
}

View File

@@ -68,6 +68,10 @@ struct AppAssets {
return RSImage(named: "faviconTemplateImage")!
}()
static var fullScreenBackgroundColor: UIColor = {
return UIColor(named: "fullScreenBackgroundColor")!
}()
static var infoImage: UIImage = {
UIImage(systemName: "info.circle")!
}()

View File

@@ -24,6 +24,7 @@ class ArticleViewController: UIViewController {
private struct MessageName {
static let imageWasClicked = "imageWasClicked"
static let imageWasShown = "imageWasShown"
}
@IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem!
@@ -43,7 +44,8 @@ class ArticleViewController: UIViewController {
}()
private var webView: WKWebView!
private var transition = ImageTransition()
private lazy var transition = ImageTransition(controller: self)
private var clickedImageCompletion: (() -> Void)?
weak var coordinator: SceneCoordinator!
@@ -67,9 +69,6 @@ class ArticleViewController: UIViewController {
}
}
var clickedImage: UIImage?
var clickedImageFrame: CGRect?
var articleExtractorButtonState: ArticleExtractorButtonState {
get {
return articleExtractorButton.buttonState
@@ -112,7 +111,9 @@ class ArticleViewController: UIViewController {
webView.uiDelegate = self
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked)
webView.configuration.userContentController.add(self, name: MessageName.imageWasClicked)
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown)
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
// Even though page.html should be loaded into this webview, we have to do it again
// to work around this bug: http://www.openradar.me/22855188
@@ -299,6 +300,15 @@ class ArticleViewController: UIViewController {
webView.scrollView.setContentOffset(scrollToPoint, animated: true)
}
func hideClickedImage() {
webView?.evaluateJavaScript("hideClickedImage();")
}
func showClickedImage(completion: @escaping () -> Void) {
clickedImageCompletion = completion
webView?.evaluateJavaScript("showClickedImage();")
}
}
// MARK: WKNavigationDelegate
@@ -353,39 +363,38 @@ extension ArticleViewController: WKUIDelegate {
extension ArticleViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == MessageName.imageWasClicked,
let body = message.body as? String,
let data = body.data(using: .utf8),
let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data),
let range = clickMessage.imageURL.range(of: ";base64,") {
let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound))
if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) {
let rect = CGRect(x: CGFloat(clickMessage.x), y: CGFloat(clickMessage.y), width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height))
clickedImageFrame = webView.convert(rect, to: nil)
clickedImage = image
let imageVC = UIStoryboard.main.instantiateController(ofType: ImageViewController.self)
imageVC.image = image
imageVC.modalPresentationStyle = .fullScreen
imageVC.transitioningDelegate = self
present(imageVC, animated: true)
}
switch message.name {
case MessageName.imageWasShown:
clickedImageCompletion?()
case MessageName.imageWasClicked:
imageWasClicked(body: message.body as? String)
default:
return
}
}
}
class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler {
// We need to wrap a message handler to prevent a circlular reference
private weak var handler: WKScriptMessageHandler?
init(_ handler: WKScriptMessageHandler) {
self.handler = handler
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
handler?.userContentController(userContentController, didReceive: message)
}
}
// MARK: UIViewControllerTransitioningDelegate
extension ArticleViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let frame = clickedImageFrame, let image = clickedImage else { return nil }
transition.originFrame = frame
transition.originImage = image
transition.presenting = true
return transition
}
@@ -421,4 +430,22 @@ private extension ArticleViewController {
}
}
func imageWasClicked(body: String?) {
guard let body = body,
let data = body.data(using: .utf8),
let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data),
let range = clickMessage.imageURL.range(of: ";base64,")
else { return }
let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound))
if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) {
let rect = CGRect(x: CGFloat(clickMessage.x), y: CGFloat(clickMessage.y), width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height))
transition.originFrame = webView.convert(rect, to: nil)
transition.maskFrame = webView.convert(webView.frame, to: nil)
transition.originImage = image
coordinator.showFullScreenImage(image: image, transitioningDelegate: self)
}
}
}

View File

@@ -9,365 +9,352 @@
import UIKit
@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate {
func imageScrollViewDidChangeOrientation(imageScrollView: ImageScrollView)
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView)
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView)
}
open class ImageScrollView: UIScrollView {
@objc public enum ScaleMode: Int {
case aspectFill
case aspectFit
case widthFill
case heightFill
}
@objc public enum Offset: Int {
case begining
case center
}
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
@objc open var imageContentMode: ScaleMode = .widthFill
@objc open var initialOffset: Offset = .begining
@objc public private(set) var zoomView: UIImageView? = nil
@objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate?
var imageSize: CGSize = CGSize.zero
private var pointToCenterAfterResize: CGPoint = CGPoint.zero
private var scaleToRestoreAfterResize: CGFloat = 1.0
var maxScaleFromMinScale: CGFloat = 3.0
@objc public enum ScaleMode: Int {
case aspectFill
case aspectFit
case widthFill
case heightFill
}
@objc public enum Offset: Int {
case begining
case center
}
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
@objc open var imageContentMode: ScaleMode = .widthFill
@objc open var initialOffset: Offset = .begining
@objc public private(set) var zoomView: UIImageView? = nil
@objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate?
var imageSize: CGSize = CGSize.zero
private var pointToCenterAfterResize: CGPoint = CGPoint.zero
private var scaleToRestoreAfterResize: CGFloat = 1.0
var maxScaleFromMinScale: CGFloat = 3.0
var zoomedFrame: CGRect {
return zoomView?.frame ?? CGRect.zero
}
override open var frame: CGRect {
willSet {
if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
prepareToResize()
}
}
didSet {
if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
recoverFromResizing()
}
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func initialize() {
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
bouncesZoom = true
decelerationRate = UIScrollView.DecelerationRate.fast
delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(ImageScrollView.changeOrientationNotification), name: UIDevice.orientationDidChangeNotification, object: nil)
}
@objc public func adjustFrameToCenter() {
guard let unwrappedZoomView = zoomView else {
return
}
var frameToCenter = unwrappedZoomView.frame
// center horizontally
if frameToCenter.size.width < bounds.width {
frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2
} else {
frameToCenter.origin.x = 0
}
// center vertically
if frameToCenter.size.height < bounds.height {
frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2
} else {
frameToCenter.origin.y = 0
}
unwrappedZoomView.frame = frameToCenter
}
private func prepareToResize() {
let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY)
pointToCenterAfterResize = convert(boundsCenter, to: zoomView)
scaleToRestoreAfterResize = zoomScale
// If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum
// allowable scale when the scale is restored.
if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) {
scaleToRestoreAfterResize = 0
}
}
private func recoverFromResizing() {
setMaxMinZoomScalesForCurrentBounds()
// restore zoom scale, first making sure it is within the allowable range.
let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize)
zoomScale = min(maximumZoomScale, maxZoomScale)
// restore center point, first making sure it is within the allowable range.
// convert our desired center point back to our own coordinate space
let boundsCenter = convert(pointToCenterAfterResize, to: zoomView)
// calculate the content offset that would yield that center point
var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0)
// restore offset, adjusted to be within the allowable range
let maxOffset = maximumContentOffset()
let minOffset = minimumContentOffset()
var realMaxOffset = min(maxOffset.x, offset.x)
offset.x = max(minOffset.x, realMaxOffset)
realMaxOffset = min(maxOffset.y, offset.y)
offset.y = max(minOffset.y, realMaxOffset)
contentOffset = offset
}
private func maximumContentOffset() -> CGPoint {
return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height)
}
private func minimumContentOffset() -> CGPoint {
return CGPoint.zero
}
// MARK: - Set up
open func setup() {
var topSupperView = superview
while topSupperView?.superview != nil {
topSupperView = topSupperView?.superview
}
// Make sure views have already layout with precise frame
topSupperView?.layoutIfNeeded()
}
// MARK: - Display image
@objc open func display(image: UIImage) {
if let zoomView = zoomView {
zoomView.removeFromSuperview()
}
zoomView = UIImageView(image: image)
zoomView!.isUserInteractionEnabled = true
addSubview(zoomView!)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:)))
tapGesture.numberOfTapsRequired = 2
zoomView!.addGestureRecognizer(tapGesture)
override open var frame: CGRect {
willSet {
if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
prepareToResize()
}
}
didSet {
if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
recoverFromResizing()
}
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
bouncesZoom = true
decelerationRate = UIScrollView.DecelerationRate.fast
delegate = self
}
@objc public func adjustFrameToCenter() {
guard let unwrappedZoomView = zoomView else {
return
}
var frameToCenter = unwrappedZoomView.frame
// center horizontally
if frameToCenter.size.width < bounds.width {
frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2
} else {
frameToCenter.origin.x = 0
}
// center vertically
if frameToCenter.size.height < bounds.height {
frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2
} else {
frameToCenter.origin.y = 0
}
unwrappedZoomView.frame = frameToCenter
}
private func prepareToResize() {
let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY)
pointToCenterAfterResize = convert(boundsCenter, to: zoomView)
scaleToRestoreAfterResize = zoomScale
// If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum
// allowable scale when the scale is restored.
if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) {
scaleToRestoreAfterResize = 0
}
}
private func recoverFromResizing() {
setMaxMinZoomScalesForCurrentBounds()
// restore zoom scale, first making sure it is within the allowable range.
let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize)
zoomScale = min(maximumZoomScale, maxZoomScale)
// restore center point, first making sure it is within the allowable range.
// convert our desired center point back to our own coordinate space
let boundsCenter = convert(pointToCenterAfterResize, to: zoomView)
// calculate the content offset that would yield that center point
var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0)
// restore offset, adjusted to be within the allowable range
let maxOffset = maximumContentOffset()
let minOffset = minimumContentOffset()
var realMaxOffset = min(maxOffset.x, offset.x)
offset.x = max(minOffset.x, realMaxOffset)
realMaxOffset = min(maxOffset.y, offset.y)
offset.y = max(minOffset.y, realMaxOffset)
contentOffset = offset
}
private func maximumContentOffset() -> CGPoint {
return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height)
}
private func minimumContentOffset() -> CGPoint {
return CGPoint.zero
}
// MARK: - Set up
open func setup() {
var topSupperView = superview
while topSupperView?.superview != nil {
topSupperView = topSupperView?.superview
}
// Make sure views have already layout with precise frame
topSupperView?.layoutIfNeeded()
}
// MARK: - Display image
@objc open func display(image: UIImage) {
if let zoomView = zoomView {
zoomView.removeFromSuperview()
}
zoomView = UIImageView(image: image)
zoomView!.isUserInteractionEnabled = true
addSubview(zoomView!)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:)))
tapGesture.numberOfTapsRequired = 2
zoomView!.addGestureRecognizer(tapGesture)
let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:)))
downSwipeGesture.direction = .down
zoomView!.addGestureRecognizer(downSwipeGesture)
let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:)))
upSwipeGesture.direction = .up
zoomView!.addGestureRecognizer(upSwipeGesture)
configureImageForSize(image.size)
}
private func configureImageForSize(_ size: CGSize) {
imageSize = size
contentSize = imageSize
setMaxMinZoomScalesForCurrentBounds()
zoomScale = minimumZoomScale
switch initialOffset {
case .begining:
contentOffset = CGPoint.zero
case .center:
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2
switch imageContentMode {
case .aspectFit:
contentOffset = CGPoint.zero
case .aspectFill:
contentOffset = CGPoint(x: xOffset, y: yOffset)
case .heightFill:
contentOffset = CGPoint(x: xOffset, y: 0)
case .widthFill:
contentOffset = CGPoint(x: 0, y: yOffset)
}
}
}
private func setMaxMinZoomScalesForCurrentBounds() {
// calculate min/max zoomscale
let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise
let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise
var minScale: CGFloat = 1
switch imageContentMode {
case .aspectFill:
minScale = max(xScale, yScale)
case .aspectFit:
minScale = min(xScale, yScale)
case .widthFill:
minScale = xScale
case .heightFill:
minScale = yScale
}
let maxScale = maxScaleFromMinScale*minScale
// don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
if minScale > maxScale {
minScale = maxScale
}
maximumZoomScale = maxScale
minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController
}
// MARK: - Gesture
@objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
// zoom out if it bigger than middle scale point. Else, zoom in
if zoomScale >= maximumZoomScale / 2.0 {
setZoomScale(minimumZoomScale, animated: true)
} else {
let center = gestureRecognizer.location(in: gestureRecognizer.view)
let zoomRect = zoomRectForScale(ImageScrollView.kZoomInFactorFromMinWhenDoubleTap * minimumZoomScale, center: center)
zoom(to: zoomRect, animated: true)
}
}
@objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
configureImageForSize(image.size)
}
private func configureImageForSize(_ size: CGSize) {
imageSize = size
contentSize = imageSize
setMaxMinZoomScalesForCurrentBounds()
zoomScale = minimumZoomScale
switch initialOffset {
case .begining:
contentOffset = CGPoint.zero
case .center:
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2
switch imageContentMode {
case .aspectFit:
contentOffset = CGPoint.zero
case .aspectFill:
contentOffset = CGPoint(x: xOffset, y: yOffset)
case .heightFill:
contentOffset = CGPoint(x: xOffset, y: 0)
case .widthFill:
contentOffset = CGPoint(x: 0, y: yOffset)
}
}
}
private func setMaxMinZoomScalesForCurrentBounds() {
// calculate min/max zoomscale
let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise
let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise
var minScale: CGFloat = 1
switch imageContentMode {
case .aspectFill:
minScale = max(xScale, yScale)
case .aspectFit:
minScale = min(xScale, yScale)
case .widthFill:
minScale = xScale
case .heightFill:
minScale = yScale
}
let maxScale = maxScaleFromMinScale*minScale
// don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
if minScale > maxScale {
minScale = maxScale
}
maximumZoomScale = maxScale
minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController
}
// MARK: - Gesture
@objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
// zoom out if it bigger than middle scale point. Else, zoom in
if zoomScale >= maximumZoomScale / 2.0 {
setZoomScale(minimumZoomScale, animated: true)
} else {
let center = gestureRecognizer.location(in: gestureRecognizer.view)
let zoomRect = zoomRectForScale(ImageScrollView.kZoomInFactorFromMinWhenDoubleTap * minimumZoomScale, center: center)
zoom(to: zoomRect, animated: true)
}
}
@objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == .ended {
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self)
}
}
@objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
@objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == .ended {
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self)
}
}
private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
// the zoom rect is in the content view's coordinates.
// at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
// as the zoom scale decreases, so more content is visible, the size of the rect grows.
zoomRect.size.height = frame.size.height / scale
zoomRect.size.width = frame.size.width / scale
// choose an origin so as to get the right center.
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
return zoomRect
}
open func refresh() {
if let image = zoomView?.image {
display(image: image)
}
}
// MARK: - Actions
@objc func changeOrientationNotification() {
// A weird bug that frames are not update right after orientation changed. Need delay a little bit with async.
DispatchQueue.main.async {
self.configureImageForSize(self.imageSize)
self.imageScrollViewDelegate?.imageScrollViewDidChangeOrientation(imageScrollView: self)
}
}
var zoomRect = CGRect.zero
// the zoom rect is in the content view's coordinates.
// at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
// as the zoom scale decreases, so more content is visible, the size of the rect grows.
zoomRect.size.height = frame.size.height / scale
zoomRect.size.width = frame.size.width / scale
// choose an origin so as to get the right center.
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
return zoomRect
}
open func refresh() {
if let image = zoomView?.image {
display(image: image)
}
}
open func resize() {
self.configureImageForSize(self.imageSize)
}
}
extension ImageScrollView: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidScroll?(scrollView)
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
}
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
}
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
}
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
}
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return false
}
@available(iOS 11.0, *)
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView)
}
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomView
}
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
adjustFrameToCenter()
imageScrollViewDelegate?.scrollViewDidZoom?(scrollView)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidScroll?(scrollView)
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
}
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
}
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
}
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
}
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return false
}
@available(iOS 11.0, *)
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView)
}
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomView
}
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
adjustFrameToCenter()
imageScrollViewDelegate?.scrollViewDidZoom?(scrollView)
}
}

View File

@@ -10,38 +10,41 @@ import UIKit
class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.3
private weak var articleController: ArticleViewController?
private let duration = 0.4
var presenting = true
var originFrame: CGRect!
var maskFrame: CGRect!
var originImage: UIImage!
init(controller: ArticleViewController) {
self.articleController = controller
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let destFrame: CGRect = {
if presenting {
let imageController = transitionContext.viewController(forKey: .to) as! ImageViewController
return imageController.zoomedFrame
} else {
let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController
return imageController.zoomedFrame
}
}()
if presenting {
animateTransitionPresenting(using: transitionContext)
} else {
animateTransitionReturning(using: transitionContext)
}
}
let initialFrame = presenting ? originFrame! : destFrame
let targetFrame = presenting ? destFrame : originFrame!
private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) {
let imageView = UIImageView(image: originImage)
imageView.frame = initialFrame
imageView.frame = originFrame
let fromView = transitionContext.view(forKey: .from)!
fromView.removeFromSuperview()
transitionContext.containerView.backgroundColor = UIColor.systemBackground
transitionContext.containerView.backgroundColor = AppAssets.fullScreenBackgroundColor
transitionContext.containerView.addSubview(imageView)
articleController?.hideClickedImage()
UIView.animate(
withDuration: duration,
@@ -49,14 +52,54 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
animations: {
imageView.frame = targetFrame
let imageController = transitionContext.viewController(forKey: .to) as! ImageViewController
imageView.frame = imageController.zoomedFrame
}, completion: { _ in
imageView.removeFromSuperview()
let toView = transitionContext.view(forKey: .to)!
transitionContext.containerView.addSubview(toView)
transitionContext.completeTransition(true)
})
}
private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) {
let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController
let imageView = UIImageView(image: originImage)
imageView.frame = imageController.zoomedFrame
let fromView = transitionContext.view(forKey: .from)!
let windowFrame = fromView.window!.frame
fromView.removeFromSuperview()
let toView = transitionContext.view(forKey: .to)!
transitionContext.containerView.addSubview(toView)
let maskingView = UIView()
let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height)
let path = UIBezierPath(rect: fullMaskFrame)
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskingView.layer.mask = maskLayer
maskingView.addSubview(imageView)
transitionContext.containerView.addSubview(maskingView)
UIView.animate(
withDuration: duration,
delay:0.0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
animations: {
imageView.frame = self.originFrame
}, completion: { _ in
self.articleController?.showClickedImage() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
imageView.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
})
}
}

View File

@@ -28,6 +28,13 @@ class ImageViewController: UIViewController {
imageScrollView.display(image: image)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] context in
self?.imageScrollView.resize()
})
}
@IBAction func share(_ sender: Any) {
guard let image = image else { return }
let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
@@ -46,9 +53,6 @@ class ImageViewController: UIViewController {
extension ImageViewController: ImageScrollViewDelegate {
func imageScrollViewDidChangeOrientation(imageScrollView: ImageScrollView) {
}
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) {
dismiss(animated: true)
}

View File

@@ -265,7 +265,7 @@
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<color key="backgroundColor" name="fullScreenBackgroundColor"/>
<constraints>
<constraint firstItem="RmY-a3-hUg" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="A0i-Hs-1Ac"/>
<constraint firstAttribute="bottom" secondItem="msG-pz-EKk" secondAttribute="bottom" id="AtA-bA-jDr"/>
@@ -299,6 +299,9 @@
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
<image name="square.and.arrow.up.fill" catalog="system" width="56" height="64"/>
<image name="star" catalog="system" width="64" height="58"/>
<namedColor name="fullScreenBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</namedColor>
<namedColor name="primaryAccentColor">
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>

View File

@@ -0,0 +1,31 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"platform" : "ios",
"reference" : "systemBackgroundColor"
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "gray-gamma-22",
"components" : {
"white" : "0.000",
"alpha" : "1.000"
}
}
}
]
}

View File

@@ -1,5 +1,7 @@
// Used to pop a resizable image view
async function imageWasClicked(img) {
img.classList.add("nnwClicked");
const rect = img.getBoundingClientRect();
var message = {
@@ -31,7 +33,21 @@ async function imageWasClicked(img) {
}
// Add the click listeners for images
// Used to animate the transition to a fullscreen image
function hideClickedImage() {
var img = document.querySelector('.nnwClicked')
img.style.opacity = 0
}
// Used to animate the transition from a fullscreen image
function showClickedImage() {
var img = document.querySelector('.nnwClicked')
img.classList.remove("nnwClicked");
img.style.opacity = 1
window.webkit.messageHandlers.imageWasShown.postMessage("");
}
// Add the click listener for images
function imageClicks() {
window.onclick = function(event) {
if (event.target.matches('img')) {

View File

@@ -14,6 +14,7 @@ class RootSplitViewController: UISplitViewController {
var coordinator: SceneCoordinator!
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] context in
self?.coordinator.configureThreePanelMode(for: size)
})

View File

@@ -819,6 +819,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
masterFeedViewController.present(addViewController, animated: true)
}
func showFullScreenImage(image: UIImage, transitioningDelegate: UIViewControllerTransitioningDelegate) {
let imageVC = UIStoryboard.main.instantiateController(ofType: ImageViewController.self)
imageVC.image = image
imageVC.modalPresentationStyle = .currentContext
imageVC.transitioningDelegate = transitioningDelegate
rootSplitViewController.present(imageVC, animated: true)
}
func toggleArticleExtractor() {
guard let article = currentArticle else {

View File

@@ -14,7 +14,7 @@ import RSWeb
struct SettingsDetailAccountView : View {
@Environment(\.presentationMode) var presentation
@ObservedObject var viewModel: ViewModel
@State private var accountType: AccountType = nil
@State private var credentialsAction: Int? = nil
@State private var isDeleteAlertPresented = false
var body: some View {
@@ -28,20 +28,21 @@ struct SettingsDetailAccountView : View {
}
}
if viewModel.isCreditialsAvailable {
Section {
Button(action: {
self.accountType = self.viewModel.account.type
}) {
if viewModel.account.type == .feedbin {
NavigationLink(destination: self.settingsFeedbinAccountView, tag: 1, selection: $credentialsAction) {
Text("Credentials")
}
.modifier(VibrantSelectAction(action: {
self.credentialsAction = 1
}))
}
.sheet(item: $accountType) { type in
if type == .feedbin {
self.settingsFeedbinAccountView
}
if type == .freshRSS {
self.settingsReaderAPIAccountView
if viewModel.account.type == .freshRSS {
NavigationLink(destination: self.settingsReaderAPIAccountView, tag: 1, selection: $credentialsAction) {
Text("Credentials")
}
.modifier(VibrantSelectAction(action: {
self.credentialsAction = 1
}))
}
}
if viewModel.isDeletable {