mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'master' into feature/feed-wrangler
# Conflicts: # Frameworks/Account/Account.xcodeproj/project.pbxproj # submodules/RSWeb
This commit is contained in:
@@ -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 */,
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")!
|
||||
}()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user