mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Camera and editor improvements
This commit is contained in:
parent
65ed79b44d
commit
97e871fa22
@ -56,6 +56,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ImageBlur:ImageBlur",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
|
||||
private final class CameraContext {
|
||||
private let queue: Queue
|
||||
@ -38,6 +40,19 @@ private final class CameraContext {
|
||||
}
|
||||
}
|
||||
|
||||
private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
let ciContext = CIContext()
|
||||
var ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
||||
ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33))
|
||||
if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||
CameraSimplePreviewView.saveLastState(uiImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var videoOrientation: AVCaptureVideoOrientation?
|
||||
init(queue: Queue, session: AVCaptureSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?) {
|
||||
self.queue = queue
|
||||
@ -59,24 +74,30 @@ private final class CameraContext {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let previewView = self.previewView, !self.changingPosition {
|
||||
let videoOrientation = connection.videoOrientation
|
||||
if #available(iOS 13.0, *) {
|
||||
previewView.mirroring = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
if let rotation = CameraPreviewView.Rotation(with: .portrait, videoOrientation: videoOrientation, cameraPosition: self.device.position) {
|
||||
previewView.rotation = rotation
|
||||
}
|
||||
if #available(iOS 13.0, *), connection.inputPorts.first?.sourceDevicePosition == .front {
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
previewView.captureDeviceResolution = CGSize(width: width, height: height)
|
||||
}
|
||||
previewView.pixelBuffer = pixelBuffer
|
||||
Queue.mainQueue().async {
|
||||
self.videoOrientation = videoOrientation
|
||||
}
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 5.0 {
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
// if let previewView = self.previewView, !self.changingPosition {
|
||||
// let videoOrientation = connection.videoOrientation
|
||||
// if #available(iOS 13.0, *) {
|
||||
// previewView.mirroring = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
// }
|
||||
// if let rotation = CameraPreviewView.Rotation(with: .portrait, videoOrientation: videoOrientation, cameraPosition: self.device.position) {
|
||||
// previewView.rotation = rotation
|
||||
// }
|
||||
// if #available(iOS 13.0, *), connection.inputPorts.first?.sourceDevicePosition == .front {
|
||||
// let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
// let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
// previewView.captureDeviceResolution = CGSize(width: width, height: height)
|
||||
// }
|
||||
// previewView.pixelBuffer = pixelBuffer
|
||||
// Queue.mainQueue().async {
|
||||
// self.videoOrientation = videoOrientation
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
self.output.processFaceLandmarks = { [weak self] observations in
|
||||
|
@ -225,6 +225,10 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA
|
||||
}
|
||||
}
|
||||
|
||||
if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||
self.processSampleBuffer?(videoPixelBuffer, connection)
|
||||
}
|
||||
|
||||
// let finalSampleBuffer: CMSampleBuffer = sampleBuffer
|
||||
// if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {
|
||||
// var finalVideoPixelBuffer = videoPixelBuffer
|
||||
|
@ -7,8 +7,65 @@ import Metal
|
||||
import MetalKit
|
||||
import CoreMedia
|
||||
import Vision
|
||||
import ImageBlur
|
||||
|
||||
public class CameraSimplePreviewView: UIView {
|
||||
static func lastStateImage() -> UIImage {
|
||||
let imagePath = NSTemporaryDirectory() + "cameraImage.jpg"
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) {
|
||||
return image
|
||||
} else {
|
||||
return UIImage(bundleImageName: "Camera/Placeholder")!
|
||||
}
|
||||
}
|
||||
|
||||
static func saveLastState(_ image: UIImage) {
|
||||
let imagePath = NSTemporaryDirectory() + "cameraImage.jpg"
|
||||
if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: imagePath))
|
||||
}
|
||||
}
|
||||
|
||||
private var previewingDisposable: Disposable?
|
||||
private let placeholderView = UIImageView()
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.placeholderView.image = CameraSimplePreviewView.lastStateImage()
|
||||
self.addSubview(self.placeholderView)
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
self.previewingDisposable = (self.isPreviewing
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self?.placeholderView.alpha = 0.0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Queue.mainQueue().after(0.5) {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.placeholderView.alpha = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.previewingDisposable?.dispose()
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.placeholderView.frame = self.bounds
|
||||
}
|
||||
|
||||
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
||||
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
|
||||
fatalError()
|
||||
|
12
submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/Contents.json
vendored
Normal file
12
submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_editor_brush1.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -254,10 +254,6 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let controller = environment.controller
|
||||
let availableSize = context.availableSize
|
||||
|
||||
let accountContext = component.context
|
||||
let push = component.push
|
||||
let completion = component.completion
|
||||
|
||||
let topControlInset: CGFloat = 20.0
|
||||
|
||||
component.changeMode.connect({ [weak state] mode in
|
||||
@ -394,15 +390,10 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
state.camera.togglePosition()
|
||||
},
|
||||
galleryTapped: {
|
||||
var dismissGalleryControllerImpl: (() -> Void)?
|
||||
let controller = accountContext.sharedContext.makeMediaPickerScreen(context: accountContext, completion: { asset in
|
||||
dismissGalleryControllerImpl?()
|
||||
completion.invoke(.single(.asset(asset)))
|
||||
})
|
||||
dismissGalleryControllerImpl = { [weak controller] in
|
||||
controller?.dismiss(animated: true)
|
||||
guard let controller = environment.controller() as? CameraScreen else {
|
||||
return
|
||||
}
|
||||
push(controller)
|
||||
controller.presentGallery()
|
||||
},
|
||||
swipeHintUpdated: { hint in
|
||||
state.updateSwipeHint(hint)
|
||||
@ -824,6 +815,10 @@ public class CameraScreen: ViewController {
|
||||
self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
} else if translation.y < -10.0 {
|
||||
self.controller?.presentGallery()
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
}
|
||||
}
|
||||
case .ended:
|
||||
@ -872,12 +867,13 @@ public class CameraScreen: ViewController {
|
||||
|
||||
if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
|
||||
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
|
||||
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.previewContainerView.frame.minX, y: sourceLocalFrame.minY - self.previewContainerView.frame.minY), size: sourceLocalFrame.size)
|
||||
|
||||
|
||||
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animate(
|
||||
from: transitionIn.sourceCornerRadius as NSNumber,
|
||||
from: self.previewContainerView.bounds.width / 2.0 as NSNumber,
|
||||
to: self.previewContainerView.layer.cornerRadius as NSNumber,
|
||||
keyPath: "cornerRadius",
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
@ -886,7 +882,7 @@ public class CameraScreen: ViewController {
|
||||
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: view.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -902,16 +898,17 @@ public class CameraScreen: ViewController {
|
||||
})
|
||||
|
||||
if let transitionOut = self.controller?.transitionOut(false), let destinationView = transitionOut.destinationView {
|
||||
let sourceLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
|
||||
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.previewContainerView.frame.minX, y: sourceLocalFrame.minY - self.previewContainerView.frame.minY), size: sourceLocalFrame.size)
|
||||
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width
|
||||
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animate(
|
||||
from: self.previewContainerView.layer.cornerRadius as NSNumber,
|
||||
to: transitionOut.destinationCornerRadius as NSNumber,
|
||||
to: self.previewContainerView.bounds.width / 2.0 as NSNumber,
|
||||
keyPath: "cornerRadius",
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
duration: 0.3,
|
||||
@ -919,8 +916,8 @@ public class CameraScreen: ViewController {
|
||||
)
|
||||
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animatePosition(from: view.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateBounds(from: view.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animatePosition(from: view.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1075,10 +1072,6 @@ public class CameraScreen: ViewController {
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
transition.setFrame(view: componentView, frame: componentFrame)
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
@ -1090,6 +1083,10 @@ public class CameraScreen: ViewController {
|
||||
transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1140,6 +1137,20 @@ public class CameraScreen: ViewController {
|
||||
public func returnFromEditor() {
|
||||
self.node.animateInFromEditor()
|
||||
}
|
||||
|
||||
func presentGallery() {
|
||||
var dismissGalleryControllerImpl: (() -> Void)?
|
||||
let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] asset in
|
||||
dismissGalleryControllerImpl?()
|
||||
if let self {
|
||||
self.completion(.single(.asset(asset)))
|
||||
}
|
||||
})
|
||||
dismissGalleryControllerImpl = { [weak controller] in
|
||||
controller?.dismiss(animated: true)
|
||||
}
|
||||
push(controller)
|
||||
}
|
||||
|
||||
private var isDismissed = false
|
||||
fileprivate func requestDismiss(animated: Bool) {
|
||||
|
@ -364,6 +364,8 @@ final class CaptureControlsComponent: Component {
|
||||
|
||||
private let lockImage = UIImage(bundleImageName: "Camera/LockIcon")
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
@ -392,7 +394,6 @@ final class CaptureControlsComponent: Component {
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.hapticFeedback.impact(.click05)
|
||||
self.component?.shutterPressed()
|
||||
self.component?.swipeHintUpdated(.zoom)
|
||||
self.shutterUpdateOffset.invoke((0.0, .immediate))
|
||||
@ -415,8 +416,6 @@ final class CaptureControlsComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var didFlip = false
|
||||
private var wasBanding: Bool?
|
||||
private var panBlobState: ShutterBlobView.BlobState?
|
||||
@ -667,6 +666,7 @@ final class CaptureControlsComponent: Component {
|
||||
),
|
||||
automaticHighlight: false,
|
||||
action: { [weak self] in
|
||||
self?.hapticFeedback.impact(.light)
|
||||
self?.shutterUpdateOffset.invoke((0.0, .immediate))
|
||||
component.shutterTapped()
|
||||
},
|
||||
|
@ -988,7 +988,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
completion()
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.frame.height - self.previewContainerView.frame.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animate(
|
||||
from: self.previewContainerView.layer.cornerRadius as NSNumber,
|
||||
to: self.previewContainerView.bounds.width / 2.0 as NSNumber,
|
||||
@ -1002,7 +1002,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
componentView.clipsToBounds = true
|
||||
componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.frame.height - componentView.frame.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.bounds.height - componentView.bounds.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animate(
|
||||
from: componentView.layer.cornerRadius as NSNumber,
|
||||
|
BIN
submodules/TelegramUI/Images.xcassets/Camera/Placeholder.imageset/CameraPlaceholder.jpg
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Camera/Placeholder.imageset/CameraPlaceholder.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
12
submodules/TelegramUI/Images.xcassets/Camera/Placeholder.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Camera/Placeholder.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "CameraPlaceholder.jpg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user