forked from shadowfacts/Tusker
parent
f5e9f71586
commit
9768097488
|
@ -17,6 +17,7 @@ struct ImageGrayscalifier {
|
||||||
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
|
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
|
||||||
if Preferences.shared.grayscaleImages,
|
if Preferences.shared.grayscaleImages,
|
||||||
let source = image.cgImage {
|
let source = image.cgImage {
|
||||||
|
// todo: should this return the original image if conversion fails?
|
||||||
return convert(url: url, cgImage: source)
|
return convert(url: url, cgImage: source)
|
||||||
} else {
|
} else {
|
||||||
return image
|
return image
|
||||||
|
|
|
@ -11,10 +11,12 @@ import Pachyderm
|
||||||
|
|
||||||
class AttachmentPreviewViewController: UIViewController {
|
class AttachmentPreviewViewController: UIViewController {
|
||||||
|
|
||||||
let attachment: Attachment
|
private let attachment: Attachment
|
||||||
|
private let sourceView: AttachmentView
|
||||||
init(attachment: Attachment) {
|
|
||||||
self.attachment = attachment
|
init(sourceView: AttachmentView) {
|
||||||
|
self.attachment = sourceView.attachment
|
||||||
|
self.sourceView = sourceView
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +31,9 @@ class AttachmentPreviewViewController: UIViewController {
|
||||||
let imageView: UIImageView
|
let imageView: UIImageView
|
||||||
if attachment.url.pathExtension == "gif" {
|
if attachment.url.pathExtension == "gif" {
|
||||||
let gifView = GIFImageView(image: image)
|
let gifView = GIFImageView(image: image)
|
||||||
gifView.animate(withGIFData: data)
|
let controller = sourceView.gifController ?? GIFController(gifData: data)
|
||||||
|
controller.attach(to: gifView)
|
||||||
|
controller.startAnimating()
|
||||||
imageView = gifView
|
imageView = gifView
|
||||||
} else {
|
} else {
|
||||||
imageView = UIImageView(image: image)
|
imageView = UIImageView(image: image)
|
||||||
|
|
|
@ -41,14 +41,6 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
return animationSourceView?.image
|
return animationSourceView?.image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var animationGifData: Data? {
|
|
||||||
let attachment = attachments[currentIndex]
|
|
||||||
if attachment.url.pathExtension == "gif" {
|
|
||||||
return ImageCache.attachments.getData(attachment.url)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import AVFoundation
|
||||||
|
|
||||||
protocol LargeImageContentView: UIView {
|
protocol LargeImageContentView: UIView {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var animationGifData: Data? { get }
|
|
||||||
var activityItemsForSharing: [Any] { get }
|
var activityItemsForSharing: [Any] { get }
|
||||||
func grayscaleStateChanged()
|
func grayscaleStateChanged()
|
||||||
}
|
}
|
||||||
|
@ -20,7 +19,6 @@ protocol LargeImageContentView: UIView {
|
||||||
class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
||||||
|
|
||||||
var animationImage: UIImage? { image! }
|
var animationImage: UIImage? { image! }
|
||||||
let animationGifData: Data?
|
|
||||||
|
|
||||||
var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
[image!]
|
[image!]
|
||||||
|
@ -28,22 +26,12 @@ class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
||||||
|
|
||||||
private var sourceData: Data?
|
private var sourceData: Data?
|
||||||
|
|
||||||
convenience init(sourceData data: Data, isGif: Bool) {
|
|
||||||
self.init(image: UIImage(data: data)!, gifData: isGif ? data : nil)
|
|
||||||
|
|
||||||
self.sourceData = data
|
|
||||||
}
|
|
||||||
|
|
||||||
init(image: UIImage, gifData: Data?) {
|
init(image: UIImage) {
|
||||||
self.animationGifData = gifData
|
|
||||||
|
|
||||||
super.init(image: image)
|
super.init(image: image)
|
||||||
|
|
||||||
contentMode = .scaleAspectFit
|
contentMode = .scaleAspectFit
|
||||||
|
|
||||||
if let data = gifData {
|
|
||||||
self.animate(withGIFData: data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -68,9 +56,35 @@ class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||||
|
var animationImage: UIImage? { image }
|
||||||
|
|
||||||
|
var activityItemsForSharing: [Any] {
|
||||||
|
// todo: should gifs share the data?
|
||||||
|
[image].compactMap { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(gifController: GIFController) {
|
||||||
|
super.init(image: gifController.lastFrame?.image)
|
||||||
|
|
||||||
|
contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
gifController.attach(to: self)
|
||||||
|
// todo: doing this in the init feels wrong
|
||||||
|
gifController.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func grayscaleStateChanged() {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
private(set) var animationImage: UIImage?
|
private(set) var animationImage: UIImage?
|
||||||
var animationGifData: Data? { nil }
|
|
||||||
var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
// todo: what should we share for gifvs?
|
// todo: what should we share for gifvs?
|
||||||
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
||||||
|
|
|
@ -13,7 +13,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
weak var animationSourceView: UIImageView?
|
weak var animationSourceView: UIImageView?
|
||||||
var largeImageController: LargeImageViewController? { self }
|
var largeImageController: LargeImageViewController? { self }
|
||||||
var animationImage: UIImage? { contentView.animationImage }
|
var animationImage: UIImage? { contentView.animationImage }
|
||||||
var animationGifData: Data? { contentView.animationGifData }
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
@IBOutlet weak var scrollView: UIScrollView!
|
@IBOutlet weak var scrollView: UIScrollView!
|
||||||
|
|
|
@ -40,7 +40,6 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
weak var animationSourceView: UIImageView?
|
weak var animationSourceView: UIImageView?
|
||||||
var largeImageController: LargeImageViewController? { largeImageVC }
|
var largeImageController: LargeImageViewController? { largeImageVC }
|
||||||
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
|
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
|
||||||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
@ -93,6 +92,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
|
|
||||||
// always load full resolution from disk for large image, in case the cache is scaled
|
// always load full resolution from disk for large image, in case the cache is scaled
|
||||||
if let entry = cache.get(url, loadOriginal: true) {
|
if let entry = cache.get(url, loadOriginal: true) {
|
||||||
|
// todo: if load original is true, is there any way entry.data could be nil?
|
||||||
|
// feels like the data param of createLargeImage shouldn't be optional
|
||||||
createLargeImage(data: entry.data, image: entry.image, url: url)
|
createLargeImage(data: entry.data, image: entry.image, url: url)
|
||||||
} else {
|
} else {
|
||||||
createPreview()
|
createPreview()
|
||||||
|
@ -126,19 +127,31 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
guard !loaded else { return }
|
guard !loaded else { return }
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
let content: LargeImageContentView
|
||||||
let gifData = url.pathExtension == "gif" ? data : nil
|
|
||||||
createLargeImage(image: transformedImage, gifData: gifData)
|
// todo: p sure grayscaling gifs has never worked
|
||||||
|
if url.pathExtension == "gif", let data = data {
|
||||||
|
// todo: pulling the gif controller out of the source view feels icky
|
||||||
|
// is it possible for the source view's gif controller to have different data than we just got?
|
||||||
|
// should this be a property set by the animation controller instead?
|
||||||
|
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
||||||
|
content = LargeImageGifContentView(gifController: gifController)
|
||||||
|
} else {
|
||||||
|
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||||
|
content = LargeImageImageContentView(image: transformedImage)
|
||||||
|
} else {
|
||||||
|
content = LargeImageImageContentView(image: image)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createLargeImage(image: UIImage, gifData: Data?) {
|
private func setContent(_ content: LargeImageContentView) {
|
||||||
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
|
|
||||||
|
|
||||||
if let existing = largeImageVC {
|
if let existing = largeImageVC {
|
||||||
existing.contentView = imageView
|
existing.contentView = content
|
||||||
} else {
|
} else {
|
||||||
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
|
largeImageVC = LargeImageViewController(contentView: content, description: imageDescription, sourceView: animationSourceView)
|
||||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||||
largeImageVC!.shrinkGestureEnabled = false
|
largeImageVC!.shrinkGestureEnabled = false
|
||||||
embedChild(largeImageVC!)
|
embedChild(largeImageVC!)
|
||||||
|
@ -154,7 +167,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||||
image = grayscale
|
image = grayscale
|
||||||
}
|
}
|
||||||
self.createLargeImage(image: image, gifData: nil)
|
setContent(LargeImageImageContentView(image: image))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ protocol LargeImageAnimatableViewController: UIViewController {
|
||||||
var animationSourceView: UIImageView? { get }
|
var animationSourceView: UIImageView? { get }
|
||||||
var largeImageController: LargeImageViewController? { get }
|
var largeImageController: LargeImageViewController? { get }
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var animationGifData: Data? { get }
|
|
||||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||||
var isInteractivelyAnimatingDismissal: Bool { get set }
|
var isInteractivelyAnimatingDismissal: Bool { get set }
|
||||||
}
|
}
|
||||||
|
@ -86,8 +85,8 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: sourceFrame)
|
let imageView = GIFImageView(frame: sourceFrame)
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
if let gifData = toVC.animationGifData {
|
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
||||||
imageView.animate(withGIFData: gifData)
|
gifController.attach(to: imageView)
|
||||||
}
|
}
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView.layer.cornerRadius = sourceView.layer.cornerRadius
|
imageView.layer.cornerRadius = sourceView.layer.cornerRadius
|
||||||
|
|
|
@ -56,8 +56,10 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: originalFrame)
|
let imageView = GIFImageView(frame: originalFrame)
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
if let gifData = fromVC.animationGifData {
|
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
||||||
imageView.animate(withGIFData: gifData)
|
gifController.attach(to: imageView)
|
||||||
|
// todo: this might not be necessary, the large image content view should have started it
|
||||||
|
gifController.startAnimating()
|
||||||
}
|
}
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView.layer.cornerRadius = 0
|
imageView.layer.cornerRadius = 0
|
||||||
|
|
|
@ -29,14 +29,6 @@ class AttachmentView: GIFImageView {
|
||||||
private var attachmentRequest: ImageCache.Request?
|
private var attachmentRequest: ImageCache.Request?
|
||||||
private var source: Source?
|
private var source: Source?
|
||||||
|
|
||||||
var gifData: Data? {
|
|
||||||
switch source {
|
|
||||||
case let .gifData(_, data):
|
|
||||||
return data
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var autoplayGifs: Bool {
|
private var autoplayGifs: Bool {
|
||||||
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
||||||
}
|
}
|
||||||
|
@ -91,11 +83,13 @@ class AttachmentView: GIFImageView {
|
||||||
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.attachment.kind == .image,
|
if self.attachment.kind == .image,
|
||||||
let gifData = self.gifData {
|
let gifController = self.gifController {
|
||||||
if self.autoplayGifs && !self.isAnimatingGIF {
|
if self.autoplayGifs && !self.isAnimatingGIF {
|
||||||
self.animate(withGIFData: gifData)
|
gifController.attach(to: self)
|
||||||
|
gifController.startAnimating()
|
||||||
} else if !self.autoplayGifs && self.isAnimatingGIF {
|
} else if !self.autoplayGifs && self.isAnimatingGIF {
|
||||||
self.stopAnimatingGIF()
|
// detach instead of stopping so that any other attached gif views keep animating
|
||||||
|
self.detachGIFController()
|
||||||
}
|
}
|
||||||
} else if self.attachment.kind == .gifv,
|
} else if self.attachment.kind == .gifv,
|
||||||
let gifvView = self.gifvView {
|
let gifvView = self.gifvView {
|
||||||
|
@ -166,13 +160,18 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
if self.attachment.url.pathExtension == "gif" {
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
self.source = .gifData(attachmentURL, data)
|
self.source = .gifData(attachmentURL, data)
|
||||||
if self.autoplayGifs {
|
let controller = GIFController(gifData: data)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.animate(withGIFData: data)
|
controller.attach(to: self)
|
||||||
|
if self.autoplayGifs {
|
||||||
|
controller.startAnimating()
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if !self.autoplayGifs {
|
||||||
self.displayImage()
|
self.displayImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
self.source = .imageData(attachmentURL, data)
|
self.source = .imageData(attachmentURL, data)
|
||||||
self.displayImage()
|
self.displayImage()
|
||||||
|
@ -322,7 +321,7 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
||||||
if self.attachment.kind == .image {
|
if self.attachment.kind == .image {
|
||||||
return AttachmentPreviewViewController(attachment: self.attachment)
|
return AttachmentPreviewViewController(sourceView: self)
|
||||||
} else if self.attachment.kind == .gifv {
|
} else if self.attachment.kind == .gifv {
|
||||||
let vc = GifvAttachmentViewController(attachment: self.attachment)
|
let vc = GifvAttachmentViewController(attachment: self.attachment)
|
||||||
vc.preferredContentSize = self.image?.size ?? .zero
|
vc.preferredContentSize = self.image?.size ?? .zero
|
||||||
|
|
|
@ -10,25 +10,82 @@ import UIKit
|
||||||
|
|
||||||
class GIFImageView: UIImageView {
|
class GIFImageView: UIImageView {
|
||||||
|
|
||||||
private(set) var isAnimatingGIF: Bool = false
|
fileprivate(set) var gifController: GIFController? = nil
|
||||||
private var shouldStopAnimatingGIF = false
|
var isAnimatingGIF: Bool { gifController?.state == .playing }
|
||||||
|
|
||||||
|
/// Detaches the current GIF controller from this view.
|
||||||
|
/// If this view is the GIF controller's only one, it will stop itself.
|
||||||
|
func detachGIFController() {
|
||||||
|
gifController?.detach(from: self)
|
||||||
|
}
|
||||||
|
|
||||||
func animate(withGIFData data: Data) {
|
}
|
||||||
CGAnimateImageDataWithBlock(data as CFData, nil) { [weak self] (frameIndex, frame, stop) in
|
|
||||||
|
/// A `GIFController` controls the animation of one or more `GIFImageView`s.
|
||||||
|
class GIFController {
|
||||||
|
|
||||||
|
// GIFImageView strongly holds the controller so that when the last view detaches, the controller is freed
|
||||||
|
private var imageViews = WeakArray<GIFImageView>()
|
||||||
|
|
||||||
|
private(set) var gifData: Data
|
||||||
|
private(set) var state: State = .stopped
|
||||||
|
private(set) var lastFrame: (image: UIImage, index: Int)? = nil
|
||||||
|
|
||||||
|
init(gifData: Data) {
|
||||||
|
self.gifData = gifData
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches another view to this controller, letting it play back alongside the others.
|
||||||
|
/// Immediately brings it into sync with the others, setting the last frame if there was one.
|
||||||
|
func attach(to view: GIFImageView) {
|
||||||
|
imageViews.append(view)
|
||||||
|
view.gifController = self
|
||||||
|
|
||||||
|
if let lastFrame = lastFrame {
|
||||||
|
view.image = lastFrame.image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detaches the given view from this controller.
|
||||||
|
/// If no views attached views remain, the last strong reference to this controller is nilled out
|
||||||
|
/// and image animation will stop at the next CGAnimateImageDataWithBlock callback.
|
||||||
|
func detach(from view: GIFImageView) {
|
||||||
|
// todo: does === work the way i want here
|
||||||
|
imageViews.removeAll(where: { $0 === view })
|
||||||
|
view.gifController = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAnimating() {
|
||||||
|
guard state.shouldStop else { return }
|
||||||
|
|
||||||
|
state = .playing
|
||||||
|
|
||||||
|
CGAnimateImageDataWithBlock(gifData as CFData, nil) { [weak self] (frameIndex, cgImage, stop) in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
stop.pointee = true
|
stop.pointee = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.image = UIImage(cgImage: frame)
|
let image = UIImage(cgImage: cgImage)
|
||||||
stop.pointee = self.shouldStopAnimatingGIF
|
self.lastFrame = (image, frameIndex)
|
||||||
|
for case let .some(view) in self.imageViews {
|
||||||
|
view.image = image
|
||||||
|
}
|
||||||
|
stop.pointee = self.state.shouldStop
|
||||||
}
|
}
|
||||||
isAnimatingGIF = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopAnimatingGIF() {
|
func stopAnimating() {
|
||||||
shouldStopAnimatingGIF = true
|
guard state == .playing else { return }
|
||||||
isAnimatingGIF = false
|
|
||||||
|
state = .stopping
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State: Equatable {
|
||||||
|
case stopped, playing, stopping
|
||||||
|
|
||||||
|
var shouldStop: Bool {
|
||||||
|
self == .stopped || self == .stopping
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue