Skip to content

Commit

Permalink
Add ability to update screenshots:
Browse files Browse the repository at this point in the history
- Update API
- Update ScreenshotsFeature
Add new methods to update screenshots to CrowdinSDK
Add ability to enter screenshot name when capture from floating button.
Add message to logs with result of screenshot capturing: new or updated.
  • Loading branch information
serhii-londar committed Nov 14, 2024
1 parent 9cf5b3b commit 8dc4ea3
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 28 deletions.
4 changes: 2 additions & 2 deletions CrowdinSDK.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Pod::Spec.new do |spec|
subspec.name = 'CrowdinAPI'
subspec.source_files = 'Sources/CrowdinSDK/CrowdinAPI/**/*.swift'
subspec.dependency 'CrowdinSDK/Core'
subspec.dependency 'BaseAPI', '~> 0.2.1'
subspec.dependency 'BaseAPI', '~> 0.2.2'
end

spec.test_spec 'CrowdinAPI_Tests' do |test_spec|
Expand Down Expand Up @@ -114,7 +114,7 @@ Pod::Spec.new do |spec|
feature.dependency 'CrowdinSDK/Core'
feature.dependency 'CrowdinSDK/CrowdinProvider'
feature.dependency 'CrowdinSDK/CrowdinAPI'
feature.dependency 'BaseAPI', '~> 0.2.1'
feature.dependency 'BaseAPI', '~> 0.2.2'
end

spec.subspec 'IntervalUpdate' do |feature|
Expand Down
10 changes: 5 additions & 5 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
PODS:
- BaseAPI (0.2.1)
- BaseAPI (0.2.2)
- CrowdinSDK (1.9.0):
- CrowdinSDK/Core (= 1.9.0)
- CrowdinSDK/CrowdinProvider (= 1.9.0)
- CrowdinSDK/Core (1.9.0):
- CrowdinSDK/CrowdinFileSystem
- CrowdinSDK/CrowdinAPI (1.9.0):
- BaseAPI (~> 0.2.1)
- BaseAPI (~> 0.2.2)
- CrowdinSDK/Core
- CrowdinSDK/CrowdinFileSystem (1.9.0)
- CrowdinSDK/CrowdinProvider (1.9.0):
Expand All @@ -18,7 +18,7 @@ PODS:
- CrowdinSDK/CrowdinAPI
- CrowdinSDK/CrowdinProvider
- CrowdinSDK/LoginFeature (1.9.0):
- BaseAPI (~> 0.2.1)
- BaseAPI (~> 0.2.2)
- CrowdinSDK/Core
- CrowdinSDK/CrowdinAPI
- CrowdinSDK/CrowdinProvider
Expand Down Expand Up @@ -79,8 +79,8 @@ EXTERNAL SOURCES:
:path: "../"

SPEC CHECKSUMS:
BaseAPI: 7a3abac9fa1e19147a5c87dcfbb1829a584cd1ca
CrowdinSDK: 65fd7989c86e5ff79c8734979bc61510238d8725
BaseAPI: 7d1c79778a5c85f8e05f5e5c9d7f9be6474a53eb
CrowdinSDK: 176f6db0a3681cbcf5a1c64bb2f8e264a042c0a6
netfox: 9d5cc727fe7576c4c7688a2504618a156b7d44b7
Realm: 490aad28f1360e58fc22256d5d686d3a36525346
RealmSwift: f6a9b56d747bbdd7931de1835896c5f024b6898a
Expand Down
45 changes: 45 additions & 0 deletions Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,51 @@ class CrowdinAPI: BaseAPI {
}
}

func cw_put<T: Decodable>(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil, body: Data?, completion: @escaping (T?, Error?) -> Swift.Void) {
self.put(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), body: body, completion: { data, response, error in
if self.isUnautorized(response: response) {
CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data)
NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil)
return
}
guard let data = data else {
completion(nil, error)
return
}

CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data)

do {
let response = try JSONDecoder().decode(T.self, from: data)
completion(response, error)
} catch {
print(String(data: data, encoding: .utf8) ?? "Data is empty")
completion(nil, error)
}
})
}

func cw_putSync<T: Decodable>(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil, body: Data?) -> (T?, Error?) {
let result = self.put(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), body: body)
CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), body: body, responseData: result.data)

if self.isUnautorized(response: result.response) {
NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil)
return (nil, nil);
}
guard let data = result.data else {
return (nil, result.error)
}

do {
let response = try JSONDecoder().decode(T.self, from: data)
return (response, result.error)
} catch {
print(String(data: data, encoding: .utf8) ?? "Data is empty")
return (nil, error)
}
}

func cw_get<T: Decodable>(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil, completion: @escaping (T?, Error?) -> Swift.Void) {
self.get(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), completion: { data, response, error in
if self.isUnautorized(response: response) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// ScreenshotsListResponse.swift
// Pods
//
// Created by Serhii Londar on 10.11.2024.
//

import Foundation

// MARK: - ScreenshotsListResponse
struct ScreenshotsListResponse: Codable, Hashable {
let data: [ScreenshotsListResponseDatum]
let pagination: ScreenshotsListResponsePagination

enum CodingKeys: String, CodingKey {
case data = "data"
case pagination = "pagination"
}
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - ScreenshotsListResponseDatum
struct ScreenshotsListResponseDatum: Codable, Hashable {
let data: ScreenshotsListResponseData

enum CodingKeys: String, CodingKey {
case data = "data"
}
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - ScreenshotsListResponseData
struct ScreenshotsListResponseData: Codable, Hashable {
let id: Int
let userID: Int
let url: String
let webURL: String
let name: String
let size: ScreenshotsListResponseSize
let tagsCount: Int
let tags: [ScreenshotsListResponseTag]
let labels: [Int]
let labelIDS: [Int]
let createdAt: String
let updatedAt: String

enum CodingKeys: String, CodingKey {
case id = "id"
case userID = "userId"
case url = "url"
case webURL = "webUrl"
case name = "name"
case size = "size"
case tagsCount = "tagsCount"
case tags = "tags"
case labels = "labels"
case labelIDS = "labelIds"
case createdAt = "createdAt"
case updatedAt = "updatedAt"
}
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - ScreenshotsListResponseSize
struct ScreenshotsListResponseSize: Codable, Hashable {
let width: Int
let height: Int

enum CodingKeys: String, CodingKey {
case width = "width"
case height = "height"
}
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - ScreenshotsListResponseTag
struct ScreenshotsListResponseTag: Codable, Hashable {
let id: Int
let screenshotID: Int
let stringID: Int
let position: ScreenshotsListResponsePosition
let createdAt: String

enum CodingKeys: String, CodingKey {
case id = "id"
case screenshotID = "screenshotId"
case stringID = "stringId"
case position = "position"
case createdAt = "createdAt"
}
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - ScreenshotsListResponsePosition
struct ScreenshotsListResponsePosition: Codable, Hashable {
let x: Int
let y: Int
let width: Int
let height: Int

enum CodingKeys: String, CodingKey {
case x = "x"
case y = "y"
case width = "width"
case height = "height"
}
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - ScreenshotsListResponsePagination
struct ScreenshotsListResponsePagination: Codable, Hashable {
let offset: Int
let limit: Int

enum CodingKeys: String, CodingKey {
case offset = "offset"
case limit = "limit"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// UpdateScreenshotRequest.swift
// Pods
//
// Created by Serhii Londar on 09.11.2024.
//


struct UpdateScreenshotRequest: Codable {
let storageId: Int
let name: String
var usePreviousTags: Bool = true
}
32 changes: 32 additions & 0 deletions Sources/CrowdinSDK/CrowdinAPI/ScreenshotsAPI/ScreenshotsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ class ScreenshotsAPI: CrowdinAPI {
self.cw_post(url: url, headers: headers, body: requestData, completion: completion)
}

func updateScreenshot(projectId: Int, screnshotId: Int, storageId: Int, name: String, usePreviousTags: Bool = false, completion: @escaping (CreateScreenshotResponse?, Error?) -> Void) {
let request = UpdateScreenshotRequest(storageId: storageId, name: name, usePreviousTags: usePreviousTags)
let requestData = try? JSONEncoder().encode(request)
let url = baseUrl(with: projectId) + "/" + String(screnshotId)
let headers = [RequestHeaderFields.contentType.rawValue: "application/json"]
self.cw_put(url: url, headers: headers, body: requestData, completion: completion)
}

func createScreenshotTags(projectId: Int, screenshotId: Int, frames: [(id: Int, rect: CGRect)], completion: @escaping (CreateScreenshotTagResponse?, Error?) -> Void) {
var elements = [CreateScreenshotTagRequestElement]()
for frame in frames {
Expand All @@ -39,4 +47,28 @@ class ScreenshotsAPI: CrowdinAPI {
let headers = [RequestHeaderFields.contentType.rawValue: "application/json"]
self.cw_post(url: url, headers: headers, body: requestData, completion: completion)
}

enum ListScreenshotsParameters: String {
case search
case orderBy
case limit
case offset
}

func listScreenshots(projectId: Int, query: String, completion: @escaping (ScreenshotsListResponse?, Error?) -> Void) {
let parameters = [
ListScreenshotsParameters.search.rawValue: query,
ListScreenshotsParameters.orderBy.rawValue: "createdAt desc,updatedAt desc",
ListScreenshotsParameters.offset.rawValue: "0",
ListScreenshotsParameters.limit.rawValue: "1"
]
let url = baseUrl(with: projectId)
self.cw_get(url: url, parameters: parameters, completion: completion)
}
}

extension String {
func urlEncoded() -> String {
return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import Foundation

#if !os(watchOS)

/// Extension that adds screenshot capture functionality to CrowdinSDK
extension CrowdinSDK {
/// Initializes the screenshot feature with the current SDK configuration.
/// This method sets up the screenshot feature if it's enabled in the configuration,
/// creating necessary uploaders and processors, and configuring method swizzling.
@objc class func initializeScreenshotFeature() {
guard let config = CrowdinSDK.config else { return }
if config.screenshotsEnabled {
Expand All @@ -20,6 +24,12 @@ extension CrowdinSDK {
}
}

/// Captures a screenshot of the current top view controller and upload it to Crowdin.
/// - Parameters:
/// - name: The name to be assigned to the screenshot.
/// - success: A closure to be called when the screenshot is successfully captured and uploaded.
/// - errorHandler: A closure to be called if an error occurs during the process.
/// The closure receives an optional Error parameter indicating what went wrong.
public class func captureScreenshot(name: String, success: @escaping (() -> Void), errorHandler: @escaping ((Error?) -> Void)) {
guard let screenshotFeature = ScreenshotFeature.shared else {
errorHandler(NSError(domain: "Screenshots feature disabled", code: defaultCrowdinErrorCode, userInfo: nil))
Expand All @@ -28,13 +38,53 @@ extension CrowdinSDK {
screenshotFeature.captureScreenshot(name: name, success: success, errorHandler: errorHandler)
}

/// Captures a screenshot of a specific view and upload it to Crowdin.
/// - Parameters:
/// - view: The view to capture in the screenshot.
/// - name: The name to be assigned to the screenshot.
/// - success: A closure to be called when the screenshot is successfully captured and uploaded.
/// - errorHandler: A closure to be called if an error occurs during the process.
/// The closure receives an optional Error parameter indicating what went wrong.
public class func captureScreenshot(view: View, name: String, success: @escaping (() -> Void), errorHandler: @escaping ((Error?) -> Void)) {
guard let screenshotFeature = ScreenshotFeature.shared else {
errorHandler(NSError(domain: "Screenshots feature disabled", code: defaultCrowdinErrorCode, userInfo: nil))
return
}
screenshotFeature.captureScreenshot(view: view, name: name, success: success, errorHandler: errorHandler)
}

/// Captures a screenshot of the current top view controller and updates it if it already exists in Crowdin.
/// If several screnshots with passed name exist it will update the newest one.
/// If screenshot with fiven name not exist new one will be created.
/// - Parameters:
/// - name: The name to be assigned to the screenshot.
/// - success: A closure to be called when the screenshot is successfully captured and updated.
/// - errorHandler: A closure to be called if an error occurs during the process.
/// The closure receives an optional Error parameter indicating what went wrong.
public class func captureAndUpdateScreenshot(name: String, success: @escaping (() -> Void), errorHandler: @escaping ((Error?) -> Void)) {
guard let screenshotFeature = ScreenshotFeature.shared else {
errorHandler(NSError(domain: "Screenshots feature disabled", code: defaultCrowdinErrorCode, userInfo: nil))
return
}
screenshotFeature.captureScreenshot(name: name, success: success, errorHandler: errorHandler)
}

/// Captures a screenshot of a specific view and updates it if it already exists in Crowdin.
/// If several screnshots with passed name exist it will update the newest one.
/// If screenshot with fiven name not exist new one will be created.
/// - Parameters:
/// - view: The view to capture in the screenshot.
/// - name: The name to be assigned to the screenshot.
/// - success: A closure to be called when the screenshot is successfully captured and updated.
/// - errorHandler: A closure to be called if an error occurs during the process.
/// The closure receives an optional Error parameter indicating what went wrong.
public class func captureAndUpdateScreenshot(view: View, name: String, success: @escaping (() -> Void), errorHandler: @escaping ((Error?) -> Void)) {
guard let screenshotFeature = ScreenshotFeature.shared else {
errorHandler(NSError(domain: "Screenshots feature disabled", code: defaultCrowdinErrorCode, userInfo: nil))
return
}
screenshotFeature.captureScreenshot(view: view, name: name, success: success, errorHandler: errorHandler)
}
}

#endif
Loading

0 comments on commit 8dc4ea3

Please sign in to comment.