initial
22
Pods/ZLPhotoBrowser/LICENSE
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 long
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
199
Pods/ZLPhotoBrowser/README.md
generated
Normal file
@@ -0,0 +1,199 @@
|
||||
[](https://cocoapods.org/pods/ZLPhotoBrowser)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://swift.org/package-manager/)
|
||||
[](https://raw.githubusercontent.com/longitachi/ZLPhotoBrowser/master/LICENSE)
|
||||
[](https://img.shields.io/badge/Platforms-iOS-blue?style=flat)
|
||||

|
||||
[](https://github.com/longitachi/ZLPhotoBrowser/wiki/How-to-use-(Swift))
|
||||
|
||||

|
||||
|
||||
----------------------------------------
|
||||
|
||||
English | [简体中文](https://github.com/longitachi/ZLPhotoBrowser/blob/master/README_CN.md)
|
||||
|
||||
ZLPhotoBrowser is a Wechat-like image picker. Support select normal photos, videos, gif and livePhoto. Support edit image and crop video.
|
||||
|
||||
### Directory
|
||||
* [Features](#features)
|
||||
* [Requirements](#requirements)
|
||||
* [Usage](#usage)
|
||||
* [Change Log](#change-log)
|
||||
* [Languages](#languages)
|
||||
* [Installation(Support Cocoapods/Carthage/SPM)](#installation)
|
||||
* [Support](#support)
|
||||
* [Demo Effect](#demo-effect)
|
||||
|
||||
Detailed usage of `Swift` and `OC`, please refer to [Wiki](https://github.com/longitachi/ZLPhotoBrowser/wiki).
|
||||
|
||||
If you only want to use the image edit feature, please move to [ZLImageEditor](https://github.com/longitachi/ZLImageEditor).
|
||||
|
||||
### Features
|
||||
- [x] Portrait and landscape.
|
||||
- [x] Two framework style.
|
||||
- [x] Preview selection (Support drag and drop).
|
||||
- [x] Library selection (Support sliding selection).
|
||||
- [x] Image/Gif/LivePhoto/Video.
|
||||
- [x] Customize the maximum number of previews or selection, the maximum and minimum optional duration of the video.
|
||||
- [x] Customize the number of columns displayed in each row.
|
||||
- [x] Image editor (Draw/Crop/Image sticker/Text sticker/Mosaic/Filter/Adjust(Brightness, Contrast and Saturation)), (Draw color can be customized; Crop ratio can be customized; Filter effect can be customized; You can choose the editing tool you want).
|
||||
- [x] Video editor.
|
||||
- [x] Custom camera.
|
||||
- [x] Multi-language.
|
||||
- [x] Selected index.
|
||||
- [x] Selected/unselectable state shadow mask.
|
||||
- [x] The selected photos are displayed at the bottom of the big picture interface, which can be dragged and sorted.
|
||||
- [x] The camera's internal photo cell can displays the captured images of the camera.
|
||||
- [x] Customize font.
|
||||
- [x] The color of each part of the framework can be customized (Provide dynamic color can support light/dark mode).
|
||||
- [x] Customize images.
|
||||
|
||||
> If you have good needs and suggestions in use, or encounter any bugs, please create an issue and I will reply in time.
|
||||
|
||||
### Requirements
|
||||
* iOS 10.0
|
||||
* Swift 5.x
|
||||
* Xcode 12.x
|
||||
|
||||
### Usage
|
||||
- Preview selection
|
||||
```swift
|
||||
let ps = ZLPhotoPreviewSheet()
|
||||
ps.selectImageBlock = { [weak self] results, isOriginal in
|
||||
// your code
|
||||
}
|
||||
ps.showPreview(animate: true, sender: self)
|
||||
```
|
||||
|
||||
- Library selection
|
||||
```swift
|
||||
let ps = ZLPhotoPreviewSheet()
|
||||
ps.selectImageBlock = { [weak self] results, isOriginal in
|
||||
// your code
|
||||
}
|
||||
ps.showPhotoLibrary(sender: self)
|
||||
```
|
||||
|
||||
- Pay attention, you need to add the following key-value pairs in your app's Info.plist
|
||||
|
||||
```
|
||||
// If you don’t add this key-value pair, multiple languages are not supported, and the album name defaults to English
|
||||
Localized resources can be mixed YES
|
||||
|
||||
Privacy - Photo Library Usage Description
|
||||
|
||||
Privacy - Camera Usage Description
|
||||
|
||||
Privacy - Microphone Usage Description
|
||||
```
|
||||
|
||||
|
||||
### Change Log
|
||||
> [More logs](https://github.com/longitachi/ZLPhotoBrowser/blob/master/CHANGELOG.md)
|
||||
```
|
||||
● 4.4.3.1, 4.4.3.2 - 4.4.3 Patch
|
||||
Fix:
|
||||
Delete some time-consuming codes to improve the image loading speed of the thumbnail interface.
|
||||
Disable TextView when user ends editing.
|
||||
● 4.4.3
|
||||
Add:
|
||||
Support to limit the data size of the video.
|
||||
Add two blocks, called when asset is selected and deselected.
|
||||
Support setting video codec type in custom camera.
|
||||
Text stickers support display background color.
|
||||
● 4.4.2
|
||||
Add:
|
||||
Preserve the alpha channel of the edited image.
|
||||
Fix:
|
||||
Fix a crash caused by UI modification in a sub-thread.
|
||||
● 4.4.1
|
||||
Add:
|
||||
Adapt to RTL.
|
||||
Fix:
|
||||
Fix the problem that the image editor does not work properly when the scale of the picture is not 1.
|
||||
Fixed some UI display issue in the image preview interface.
|
||||
...
|
||||
```
|
||||
|
||||
### Languages
|
||||
🇨🇳 Chinese, 🇺🇸 English, 🇯🇵 Japanese, 🇫🇷 French, 🇩🇪 German, 🇷🇺 Russian, 🇻🇳 Vietnamese, 🇰🇷 Korean, 🇲🇾 Malay, 🇮🇹 Italian, 🇮🇩 Indonesian, 🇪🇸 Spanish, 🇵🇹 Portuguese, 🇹🇷 Turkish, 🇸🇦 Arabic.
|
||||
|
||||
### Installation
|
||||
There are four ways to use ZLPhotoBrowser in your project:
|
||||
|
||||
- using CocoaPods
|
||||
- using Carthage
|
||||
- using Swift Package Manager
|
||||
- manual install (build frameworks or embed Xcode Project)
|
||||
|
||||
#### CocoaPods
|
||||
To integrate ZLPhotoBrowser into your Xcode project using CocoaPods, specify it to a target in your Podfile:
|
||||
|
||||
```
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
platform :ios, '10.0'
|
||||
use_frameworks!
|
||||
|
||||
target 'MyApp' do
|
||||
# your other pod
|
||||
# ...
|
||||
pod 'ZLPhotoBrowser'
|
||||
end
|
||||
```
|
||||
|
||||
Then, run the following command:
|
||||
|
||||
```
|
||||
$ pod install
|
||||
```
|
||||
|
||||
> If you cannot find the latest version, you can execute `pod repo update` first
|
||||
|
||||
#### Carthage
|
||||
To integrate ZLPhotoBrowser into your Xcode project using Carthage, specify it in your Cartfile:
|
||||
|
||||
```
|
||||
github "longitachi/ZLPhotoBrowser"
|
||||
```
|
||||
|
||||
Then, run the following command to build the ZLPhotoBrowser framework:
|
||||
|
||||
```shell
|
||||
$ carthage update
|
||||
```
|
||||
|
||||
If you get an error like `Building universal frameworks with common architectures is not possible. The device and simulator slices for "ZLPhotoBrowser" both build for: arm64
|
||||
Rebuild with --use-xcframeworks to create an xcframework bundle instead.` [Click this link](https://github.com/Carthage/Carthage/blob/master/Documentation/Xcode12Workaround.md).
|
||||
|
||||
#### Swift Package Manager
|
||||
1. Select File > Swift Packages > Add Package Dependency. Enter https://github.com/longitachi/ZLPhotoBrowser.git in the "Choose Package Repository" dialog.
|
||||
2. In the next page, specify the version resolving rule as "Up to Next Major" with "4.0.9" as its earliest version.
|
||||
3. After Xcode checking out the source and resolving the version, you can choose the "ZLPhotoBrowser" library and add it to your app target.
|
||||
|
||||
### Support
|
||||
* [**★ Star**](#) this repo.
|
||||
* Support with <img src="https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/ap.png" width = "100" height = "125" /> or <img src="https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/wp.png" width = "100" height = "125" /> or <img src="https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/pp.png" width = "150" height = "125" />
|
||||
|
||||
### Demo Effect
|
||||
- Selection
|
||||

|
||||

|
||||

|
||||
|
||||
- Image editor
|
||||
|
||||

|
||||
|
||||
- Video editor
|
||||
|
||||

|
||||
|
||||
- Multi-language
|
||||
|
||||

|
||||
|
||||
- Custom camera
|
||||
|
||||

|
||||
|
||||
|
||||
57
Pods/ZLPhotoBrowser/Sources/Animation/ZLClipImageDismissAnimatedTransition.swift
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// ZLClipImageDismissAnimatedTransition.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/9/8.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLClipImageDismissAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
return 0.25
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? ZLClipImageViewController, let toVC = transitionContext.viewController(forKey: .to) as? ZLEditImageViewController else {
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
return
|
||||
}
|
||||
|
||||
let containerView = transitionContext.containerView
|
||||
containerView.addSubview(toVC.view)
|
||||
|
||||
let imageView = UIImageView(frame: fromVC.dismissAnimateFromRect)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
imageView.image = fromVC.dismissAnimateImage
|
||||
containerView.addSubview(imageView)
|
||||
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
imageView.frame = toVC.originalFrame
|
||||
}) { _ in
|
||||
toVC.finishClipDismissAnimate()
|
||||
imageView.removeFromSuperview()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Pods/ZLPhotoBrowser/Sources/Animation/ZLPhotoPreviewAnimatedTransition.swift
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// ZLPhotoPreviewAnimatedTransition.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/9/3.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLPhotoPreviewAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
return 0.25
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}
|
||||
}
|
||||
246
Pods/ZLPhotoBrowser/Sources/Animation/ZLPhotoPreviewPopInteractiveTransition.swift
generated
Normal file
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// ZLPhotoPreviewPopInteractiveTransition.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/9/3.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLPhotoPreviewPopInteractiveTransition: UIPercentDrivenInteractiveTransition {
|
||||
weak var transitionContext: UIViewControllerContextTransitioning?
|
||||
|
||||
weak var viewController: ZLPhotoPreviewController?
|
||||
|
||||
var shadowView: UIView?
|
||||
|
||||
var imageView: UIImageView?
|
||||
|
||||
var imageViewOriginalFrame: CGRect = .zero
|
||||
|
||||
var startPanPoint: CGPoint = .zero
|
||||
|
||||
var interactive: Bool = false
|
||||
|
||||
var shouldStartTransition: ((CGPoint) -> Bool)?
|
||||
|
||||
var startTransition: (() -> Void)?
|
||||
|
||||
var cancelTransition: (() -> Void)?
|
||||
|
||||
var finishTransition: (() -> Void)?
|
||||
|
||||
init(viewController: ZLPhotoPreviewController) {
|
||||
super.init()
|
||||
self.viewController = viewController
|
||||
let dismissPan = UIPanGestureRecognizer(target: self, action: #selector(dismissPanAction(_:)))
|
||||
viewController.view.addGestureRecognizer(dismissPan)
|
||||
}
|
||||
|
||||
@objc func dismissPanAction(_ pan: UIPanGestureRecognizer) {
|
||||
let point = pan.location(in: viewController?.view)
|
||||
|
||||
if pan.state == .began {
|
||||
guard shouldStartTransition?(point) == true else {
|
||||
interactive = false
|
||||
return
|
||||
}
|
||||
startPanPoint = point
|
||||
interactive = true
|
||||
startTransition?()
|
||||
viewController?.navigationController?.popViewController(animated: true)
|
||||
} else if pan.state == .changed {
|
||||
guard interactive else {
|
||||
return
|
||||
}
|
||||
let result = panResult(pan)
|
||||
imageView?.frame = result.frame
|
||||
shadowView?.alpha = pow(result.scale, 2)
|
||||
|
||||
update(result.scale)
|
||||
} else if pan.state == .cancelled || pan.state == .ended {
|
||||
guard interactive else {
|
||||
return
|
||||
}
|
||||
|
||||
let vel = pan.velocity(in: viewController?.view)
|
||||
let p = pan.translation(in: viewController?.view)
|
||||
let percent: CGFloat = max(0.0, p.y / (viewController?.view.bounds.height ?? UIScreen.main.bounds.height))
|
||||
|
||||
let dismiss = vel.y > 300 || (percent > 0.1 && vel.y > -300)
|
||||
|
||||
if dismiss {
|
||||
finish()
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
imageViewOriginalFrame = .zero
|
||||
startPanPoint = .zero
|
||||
interactive = false
|
||||
}
|
||||
}
|
||||
|
||||
func panResult(_ pan: UIPanGestureRecognizer) -> (frame: CGRect, scale: CGFloat) {
|
||||
// 拖动偏移量
|
||||
let translation = pan.translation(in: viewController?.view)
|
||||
let currentTouch = pan.location(in: viewController?.view)
|
||||
|
||||
// 由下拉的偏移值决定缩放比例,越往下偏移,缩得越小。scale值区间[0.3, 1.0]
|
||||
let scale = min(1.0, max(0.3, 1 - translation.y / UIScreen.main.bounds.height))
|
||||
|
||||
let width = imageViewOriginalFrame.size.width * scale
|
||||
let height = imageViewOriginalFrame.size.height * scale
|
||||
|
||||
// 计算x和y。保持手指在图片上的相对位置不变。
|
||||
let xRate = (startPanPoint.x - imageViewOriginalFrame.origin.x) / imageViewOriginalFrame.size.width
|
||||
let currentTouchDeltaX = xRate * width
|
||||
let x = currentTouch.x - currentTouchDeltaX
|
||||
|
||||
let yRate = (startPanPoint.y - imageViewOriginalFrame.origin.y) / imageViewOriginalFrame.size.height
|
||||
let currentTouchDeltaY = yRate * height
|
||||
let y = currentTouch.y - currentTouchDeltaY
|
||||
|
||||
return (CGRect(x: x.isNaN ? 0 : x, y: y.isNaN ? 0 : y, width: width, height: height), scale)
|
||||
}
|
||||
|
||||
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.transitionContext = transitionContext
|
||||
startAnimate()
|
||||
}
|
||||
|
||||
func startAnimate() {
|
||||
guard let transitionContext = transitionContext else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? ZLPhotoPreviewController,
|
||||
let toVC = transitionContext.viewController(forKey: .to) as? ZLThumbnailViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
let containerView = transitionContext.containerView
|
||||
containerView.addSubview(toVC.view)
|
||||
|
||||
guard let cell = fromVC.collectionView.cellForItem(at: IndexPath(row: fromVC.currentIndex, section: 0)) as? ZLPreviewBaseCell else {
|
||||
return
|
||||
}
|
||||
|
||||
shadowView = UIView(frame: containerView.bounds)
|
||||
shadowView?.backgroundColor = ZLPhotoUIConfiguration.default().previewVCBgColor
|
||||
containerView.addSubview(shadowView!)
|
||||
|
||||
let fromImageViewFrame = cell.animateImageFrame(convertTo: containerView)
|
||||
|
||||
imageView = UIImageView(frame: fromImageViewFrame)
|
||||
imageView?.contentMode = .scaleAspectFill
|
||||
imageView?.clipsToBounds = true
|
||||
imageView?.image = cell.currentImage
|
||||
containerView.addSubview(imageView!)
|
||||
|
||||
imageViewOriginalFrame = imageView!.frame
|
||||
}
|
||||
|
||||
override func finish() {
|
||||
super.finish()
|
||||
finishAnimate()
|
||||
}
|
||||
|
||||
func finishAnimate() {
|
||||
guard let transitionContext = transitionContext else {
|
||||
return
|
||||
}
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? ZLPhotoPreviewController,
|
||||
let toVC = transitionContext.viewController(forKey: .to) as? ZLThumbnailViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
let fromVCModel = fromVC.arrDataSources[fromVC.currentIndex]
|
||||
let toVCVisiableIndexPaths = toVC.collectionView.indexPathsForVisibleItems
|
||||
|
||||
var diff = 0
|
||||
if !ZLPhotoConfiguration.default().sortAscending {
|
||||
if toVC.showCameraCell {
|
||||
diff = -1
|
||||
}
|
||||
if #available(iOS 14.0, *), toVC.showAddPhotoCell {
|
||||
diff -= 1
|
||||
}
|
||||
}
|
||||
var toIndex: Int?
|
||||
for indexPath in toVCVisiableIndexPaths {
|
||||
let idx = indexPath.row + diff
|
||||
if idx >= toVC.arrDataSources.count || idx < 0 {
|
||||
continue
|
||||
}
|
||||
let m = toVC.arrDataSources[idx]
|
||||
if m == fromVCModel {
|
||||
toIndex = indexPath.row
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var toFrame: CGRect?
|
||||
|
||||
if let toIndex = toIndex, let toCell = toVC.collectionView.cellForItem(at: IndexPath(row: toIndex, section: 0)) {
|
||||
toFrame = toVC.collectionView.convert(toCell.frame, to: transitionContext.containerView)
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
if let toFrame = toFrame {
|
||||
self.imageView?.frame = toFrame
|
||||
} else {
|
||||
self.imageView?.alpha = 0
|
||||
}
|
||||
self.shadowView?.alpha = 0
|
||||
}) { _ in
|
||||
self.imageView?.removeFromSuperview()
|
||||
self.shadowView?.removeFromSuperview()
|
||||
self.imageView = nil
|
||||
self.shadowView = nil
|
||||
self.finishTransition?()
|
||||
transitionContext.finishInteractiveTransition()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
cancelAnimate()
|
||||
}
|
||||
|
||||
func cancelAnimate() {
|
||||
guard let transitionContext = transitionContext else {
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.imageView?.frame = self.imageViewOriginalFrame
|
||||
self.shadowView?.alpha = 1
|
||||
}) { _ in
|
||||
self.imageView?.removeFromSuperview()
|
||||
self.shadowView?.removeFromSuperview()
|
||||
self.cancelTransition?()
|
||||
transitionContext.cancelInteractiveTransition()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
1347
Pods/ZLPhotoBrowser/Sources/Camera/ZLCustomCamera.swift
generated
Normal file
199
Pods/ZLPhotoBrowser/Sources/Edit/ZLAdjustSlider.swift
generated
Normal file
@@ -0,0 +1,199 @@
|
||||
//
|
||||
// ZLAdjustSlider.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/12/17.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLAdjustSlider: UIView {
|
||||
static let maximumValue: Float = 1
|
||||
|
||||
static let minimumValue: Float = -1
|
||||
|
||||
let sliderWidth: CGFloat = 5
|
||||
|
||||
lazy var valueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont.systemFont(ofSize: 12)
|
||||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.6).cgColor
|
||||
label.layer.shadowOffset = .zero
|
||||
label.layer.shadowOpacity = 1
|
||||
label.textColor = .white
|
||||
label.textAlignment = ZLPhotoUIConfiguration.default().adjustSliderType == .vertical ? .right : .center
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.6
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var separator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.rgba(230, 230, 230)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var shadowView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.adjustSliderNormalColor
|
||||
view.layer.cornerRadius = sliderWidth / 2
|
||||
view.layer.shadowColor = UIColor.black.withAlphaComponent(0.4).cgColor
|
||||
view.layer.shadowOffset = .zero
|
||||
view.layer.shadowOpacity = 1
|
||||
view.layer.shadowRadius = 3
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var whiteView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.adjustSliderNormalColor
|
||||
view.layer.cornerRadius = sliderWidth / 2
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var tintView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.adjustSliderTintColor
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
|
||||
|
||||
private var impactFeedback: UIImpactFeedbackGenerator?
|
||||
|
||||
private var valueForPanBegan: Float = 0
|
||||
|
||||
var value: Float = 0 {
|
||||
didSet {
|
||||
valueLabel.text = String(Int(roundf(value * 100)))
|
||||
tintView.frame = calculateTintFrame()
|
||||
}
|
||||
}
|
||||
|
||||
private var isVertical = ZLPhotoUIConfiguration.default().adjustSliderType == .vertical
|
||||
|
||||
var beginAdjust: (() -> Void)?
|
||||
|
||||
var valueChanged: ((Float) -> Void)?
|
||||
|
||||
var endAdjust: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLAdjustSlider deinit")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
|
||||
let editConfig = ZLPhotoConfiguration.default().editImageConfiguration
|
||||
if editConfig.impactFeedbackWhenAdjustSliderValueIsZero {
|
||||
impactFeedback = UIImpactFeedbackGenerator(style: editConfig.impactFeedbackStyle)
|
||||
}
|
||||
|
||||
addGestureRecognizer(pan)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if isVertical {
|
||||
shadowView.frame = CGRect(x: 40, y: 0, width: sliderWidth, height: bounds.height)
|
||||
whiteView.frame = shadowView.frame
|
||||
tintView.frame = calculateTintFrame()
|
||||
let separatorH: CGFloat = 1
|
||||
separator.frame = CGRect(x: 0, y: (bounds.height - separatorH) / 2, width: sliderWidth, height: separatorH)
|
||||
valueLabel.frame = CGRect(x: 0, y: bounds.height / 2 - 10, width: 38, height: 20)
|
||||
} else {
|
||||
valueLabel.frame = CGRect(x: 0, y: 0, width: zl.width, height: 38)
|
||||
shadowView.frame = CGRect(x: 0, y: valueLabel.zl.bottom + 2, width: zl.width, height: sliderWidth)
|
||||
whiteView.frame = shadowView.frame
|
||||
tintView.frame = calculateTintFrame()
|
||||
let separatorW: CGFloat = 1
|
||||
separator.frame = CGRect(x: (zl.width - separatorW) / 2, y: 0, width: separatorW, height: sliderWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(shadowView)
|
||||
addSubview(whiteView)
|
||||
whiteView.addSubview(tintView)
|
||||
whiteView.addSubview(separator)
|
||||
addSubview(valueLabel)
|
||||
}
|
||||
|
||||
private func calculateTintFrame() -> CGRect {
|
||||
if isVertical {
|
||||
let totalH = zl.height / 2
|
||||
let tintH = totalH * abs(CGFloat(value)) / CGFloat(ZLAdjustSlider.maximumValue)
|
||||
if value > 0 {
|
||||
return CGRect(x: 0, y: totalH - tintH, width: sliderWidth, height: tintH)
|
||||
} else {
|
||||
return CGRect(x: 0, y: totalH, width: sliderWidth, height: tintH)
|
||||
}
|
||||
} else {
|
||||
let totalW = zl.width / 2
|
||||
let tintW = totalW * abs(CGFloat(value)) / CGFloat(ZLAdjustSlider.maximumValue)
|
||||
if value > 0 {
|
||||
return CGRect(x: totalW, y: 0, width: tintW, height: sliderWidth)
|
||||
} else {
|
||||
return CGRect(x: totalW - tintW, y: 0, width: tintW, height: sliderWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func panAction(_ pan: UIPanGestureRecognizer) {
|
||||
let translation = pan.translation(in: self)
|
||||
|
||||
if pan.state == .began {
|
||||
valueForPanBegan = value
|
||||
beginAdjust?()
|
||||
impactFeedback?.prepare()
|
||||
} else if pan.state == .changed {
|
||||
let transValue = isVertical ? -translation.y : translation.x
|
||||
let totalLength = isVertical ? zl.height / 2 : zl.width / 2
|
||||
var temp = valueForPanBegan + Float(transValue / totalLength)
|
||||
temp = max(ZLAdjustSlider.minimumValue, min(ZLAdjustSlider.maximumValue, temp))
|
||||
|
||||
if (-0.0049..<0.005) ~= temp {
|
||||
temp = 0
|
||||
}
|
||||
|
||||
guard value != temp else { return }
|
||||
|
||||
value = temp
|
||||
valueChanged?(value)
|
||||
|
||||
guard #available(iOS 10.0, *) else { return }
|
||||
if value == 0 {
|
||||
impactFeedback?.impactOccurred()
|
||||
}
|
||||
} else {
|
||||
valueForPanBegan = value
|
||||
endAdjust?()
|
||||
}
|
||||
}
|
||||
}
|
||||
376
Pods/ZLPhotoBrowser/Sources/Edit/ZLBaseStickerView.swift
generated
Normal file
@@ -0,0 +1,376 @@
|
||||
//
|
||||
// ZLBaseStickerView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/11/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol ZLStickerViewDelegate: NSObject {
|
||||
// Called when scale or rotate or move.
|
||||
func stickerBeginOperation(_ sticker: UIView)
|
||||
|
||||
// Called during scale or rotate or move.
|
||||
func stickerOnOperation(_ sticker: UIView, panGes: UIPanGestureRecognizer)
|
||||
|
||||
// Called after scale or rotate or move.
|
||||
func stickerEndOperation(_ sticker: UIView, panGes: UIPanGestureRecognizer)
|
||||
|
||||
// Called when tap sticker.
|
||||
func stickerDidTap(_ sticker: UIView)
|
||||
|
||||
func sticker(_ textSticker: ZLTextStickerView, editText text: String)
|
||||
}
|
||||
|
||||
protocol ZLStickerViewAdditional: NSObject {
|
||||
var gesIsEnabled: Bool { get set }
|
||||
|
||||
func resetState()
|
||||
|
||||
func moveToAshbin()
|
||||
|
||||
func addScale(_ scale: CGFloat)
|
||||
}
|
||||
|
||||
class ZLBaseStickerView<T>: UIView, UIGestureRecognizerDelegate {
|
||||
private enum Direction: Int {
|
||||
case up = 0
|
||||
case right = 90
|
||||
case bottom = 180
|
||||
case left = 270
|
||||
}
|
||||
|
||||
var borderWidth = 1 / UIScreen.main.scale
|
||||
|
||||
var firstLayout = true
|
||||
|
||||
let originScale: CGFloat
|
||||
|
||||
let originAngle: CGFloat
|
||||
|
||||
var maxGesScale: CGFloat
|
||||
|
||||
var originTransform: CGAffineTransform = .identity
|
||||
|
||||
var timer: Timer?
|
||||
|
||||
var totalTranslationPoint: CGPoint = .zero
|
||||
|
||||
var gesTranslationPoint: CGPoint = .zero
|
||||
|
||||
var gesRotation: CGFloat = 0
|
||||
|
||||
var gesScale: CGFloat = 1
|
||||
|
||||
var onOperation = false
|
||||
|
||||
var gesIsEnabled = true
|
||||
|
||||
var originFrame: CGRect
|
||||
|
||||
lazy var tapGes = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
|
||||
|
||||
lazy var pinchGes: UIPinchGestureRecognizer = {
|
||||
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(_:)))
|
||||
pinch.delegate = self
|
||||
return pinch
|
||||
}()
|
||||
|
||||
lazy var panGes: UIPanGestureRecognizer = {
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
|
||||
pan.delegate = self
|
||||
return pan
|
||||
}()
|
||||
|
||||
var state: T {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
var borderView: UIView {
|
||||
return self
|
||||
}
|
||||
|
||||
weak var delegate: ZLStickerViewDelegate?
|
||||
|
||||
deinit {
|
||||
cleanTimer()
|
||||
}
|
||||
|
||||
init(
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat = 1,
|
||||
gesRotation: CGFloat = 0,
|
||||
totalTranslationPoint: CGPoint = .zero,
|
||||
showBorder: Bool = true
|
||||
) {
|
||||
self.originScale = originScale
|
||||
self.originAngle = originAngle
|
||||
self.originFrame = originFrame
|
||||
self.maxGesScale = 4 / originScale
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.gesScale = gesScale
|
||||
self.gesRotation = gesRotation
|
||||
self.totalTranslationPoint = totalTranslationPoint
|
||||
|
||||
borderView.layer.borderWidth = borderWidth
|
||||
hideBorder()
|
||||
if showBorder {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
addGestureRecognizer(tapGes)
|
||||
addGestureRecognizer(pinchGes)
|
||||
|
||||
let rotationGes = UIRotationGestureRecognizer(target: self, action: #selector(rotationAction(_:)))
|
||||
rotationGes.delegate = self
|
||||
addGestureRecognizer(rotationGes)
|
||||
|
||||
addGestureRecognizer(panGes)
|
||||
tapGes.require(toFail: panGes)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
guard firstLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
// Rotate must be first when first layout.
|
||||
transform = transform.rotated(by: originAngle.zl.toPi)
|
||||
|
||||
if totalTranslationPoint != .zero {
|
||||
let direction = direction(for: originAngle)
|
||||
if direction == .right {
|
||||
transform = transform.translatedBy(x: totalTranslationPoint.y, y: -totalTranslationPoint.x)
|
||||
} else if direction == .bottom {
|
||||
transform = transform.translatedBy(x: -totalTranslationPoint.x, y: -totalTranslationPoint.y)
|
||||
} else if direction == .left {
|
||||
transform = transform.translatedBy(x: -totalTranslationPoint.y, y: totalTranslationPoint.x)
|
||||
} else {
|
||||
transform = transform.translatedBy(x: totalTranslationPoint.x, y: totalTranslationPoint.y)
|
||||
}
|
||||
}
|
||||
|
||||
transform = transform.scaledBy(x: originScale, y: originScale)
|
||||
|
||||
originTransform = transform
|
||||
|
||||
if gesScale != 1 {
|
||||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||||
}
|
||||
if gesRotation != 0 {
|
||||
transform = transform.rotated(by: gesRotation)
|
||||
}
|
||||
|
||||
firstLayout = false
|
||||
setupUIFrameWhenFirstLayout()
|
||||
}
|
||||
|
||||
func setupUIFrameWhenFirstLayout() {}
|
||||
|
||||
private func direction(for angle: CGFloat) -> ZLBaseStickerView.Direction {
|
||||
// 将角度转换为0~360,并对360取余
|
||||
let angle = ((Int(angle) % 360) + 360) % 360
|
||||
return ZLBaseStickerView.Direction(rawValue: angle) ?? .up
|
||||
}
|
||||
|
||||
@objc func tapAction(_ ges: UITapGestureRecognizer) {
|
||||
guard gesIsEnabled else { return }
|
||||
|
||||
superview?.bringSubviewToFront(self)
|
||||
delegate?.stickerDidTap(self)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
@objc func pinchAction(_ ges: UIPinchGestureRecognizer) {
|
||||
guard gesIsEnabled else { return }
|
||||
|
||||
let scale = min(maxGesScale, gesScale * ges.scale)
|
||||
ges.scale = 1
|
||||
|
||||
guard scale != gesScale else {
|
||||
return
|
||||
}
|
||||
|
||||
gesScale = scale
|
||||
|
||||
if ges.state == .began {
|
||||
setOperation(true)
|
||||
} else if ges.state == .changed {
|
||||
updateTransform()
|
||||
} else if ges.state == .ended || ges.state == .cancelled {
|
||||
setOperation(false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func rotationAction(_ ges: UIRotationGestureRecognizer) {
|
||||
guard gesIsEnabled else { return }
|
||||
|
||||
gesRotation += ges.rotation
|
||||
ges.rotation = 0
|
||||
|
||||
if ges.state == .began {
|
||||
setOperation(true)
|
||||
} else if ges.state == .changed {
|
||||
updateTransform()
|
||||
} else if ges.state == .ended || ges.state == .cancelled {
|
||||
setOperation(false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panAction(_ ges: UIPanGestureRecognizer) {
|
||||
guard gesIsEnabled else { return }
|
||||
|
||||
let point = ges.translation(in: superview)
|
||||
gesTranslationPoint = CGPoint(x: point.x / originScale, y: point.y / originScale)
|
||||
|
||||
if ges.state == .began {
|
||||
setOperation(true)
|
||||
} else if ges.state == .changed {
|
||||
updateTransform()
|
||||
} else if ges.state == .ended || ges.state == .cancelled {
|
||||
totalTranslationPoint.x += point.x
|
||||
totalTranslationPoint.y += point.y
|
||||
setOperation(false)
|
||||
let direction = direction(for: originAngle)
|
||||
if direction == .right {
|
||||
originTransform = originTransform.translatedBy(x: gesTranslationPoint.y, y: -gesTranslationPoint.x)
|
||||
} else if direction == .bottom {
|
||||
originTransform = originTransform.translatedBy(x: -gesTranslationPoint.x, y: -gesTranslationPoint.y)
|
||||
} else if direction == .left {
|
||||
originTransform = originTransform.translatedBy(x: -gesTranslationPoint.y, y: gesTranslationPoint.x)
|
||||
} else {
|
||||
originTransform = originTransform.translatedBy(x: gesTranslationPoint.x, y: gesTranslationPoint.y)
|
||||
}
|
||||
gesTranslationPoint = .zero
|
||||
}
|
||||
}
|
||||
|
||||
func setOperation(_ isOn: Bool) {
|
||||
if isOn, !onOperation {
|
||||
onOperation = true
|
||||
cleanTimer()
|
||||
borderView.layer.borderColor = UIColor.white.cgColor
|
||||
superview?.bringSubviewToFront(self)
|
||||
delegate?.stickerBeginOperation(self)
|
||||
} else if !isOn, onOperation {
|
||||
onOperation = false
|
||||
startTimer()
|
||||
delegate?.stickerEndOperation(self, panGes: panGes)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransform() {
|
||||
var transform = originTransform
|
||||
|
||||
let direction = direction(for: originAngle)
|
||||
if direction == .right {
|
||||
transform = transform.translatedBy(x: gesTranslationPoint.y, y: -gesTranslationPoint.x)
|
||||
} else if direction == .bottom {
|
||||
transform = transform.translatedBy(x: -gesTranslationPoint.x, y: -gesTranslationPoint.y)
|
||||
} else if direction == .left {
|
||||
transform = transform.translatedBy(x: -gesTranslationPoint.y, y: gesTranslationPoint.x)
|
||||
} else {
|
||||
transform = transform.translatedBy(x: gesTranslationPoint.x, y: gesTranslationPoint.y)
|
||||
}
|
||||
// Scale must after translate.
|
||||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||||
// Rotate must after scale.
|
||||
transform = transform.rotated(by: gesRotation)
|
||||
self.transform = transform
|
||||
|
||||
delegate?.stickerOnOperation(self, panGes: panGes)
|
||||
}
|
||||
|
||||
@objc private func hideBorder() {
|
||||
borderView.layer.borderColor = UIColor.clear.cgColor
|
||||
}
|
||||
|
||||
func startTimer() {
|
||||
cleanTimer()
|
||||
borderView.layer.borderColor = UIColor.white.cgColor
|
||||
timer = Timer.scheduledTimer(timeInterval: 2, target: ZLWeakProxy(target: self), selector: #selector(hideBorder), userInfo: nil, repeats: false)
|
||||
RunLoop.current.add(timer!, forMode: .common)
|
||||
}
|
||||
|
||||
private func cleanTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
// MARK: UIGestureRecognizerDelegate
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLBaseStickerView: ZLStickerViewAdditional {
|
||||
func resetState() {
|
||||
onOperation = false
|
||||
cleanTimer()
|
||||
hideBorder()
|
||||
}
|
||||
|
||||
func moveToAshbin() {
|
||||
cleanTimer()
|
||||
removeFromSuperview()
|
||||
}
|
||||
|
||||
func addScale(_ scale: CGFloat) {
|
||||
// Revert zoom scale.
|
||||
transform = transform.scaledBy(x: 1 / originScale, y: 1 / originScale)
|
||||
// Revert ges scale.
|
||||
transform = transform.scaledBy(x: 1 / gesScale, y: 1 / gesScale)
|
||||
// Revert ges rotation.
|
||||
transform = transform.rotated(by: -gesRotation)
|
||||
|
||||
var origin = frame.origin
|
||||
origin.x *= scale
|
||||
origin.y *= scale
|
||||
|
||||
let newSize = CGSize(width: frame.width * scale, height: frame.height * scale)
|
||||
let newOrigin = CGPoint(x: frame.minX + (frame.width - newSize.width) / 2, y: frame.minY + (frame.height - newSize.height) / 2)
|
||||
let diffX: CGFloat = (origin.x - newOrigin.x)
|
||||
let diffY: CGFloat = (origin.y - newOrigin.y)
|
||||
|
||||
let direction = direction(for: originScale)
|
||||
if direction == .right {
|
||||
transform = transform.translatedBy(x: diffY, y: -diffX)
|
||||
originTransform = originTransform.translatedBy(x: diffY / originScale, y: -diffX / originScale)
|
||||
} else if direction == .bottom {
|
||||
transform = transform.translatedBy(x: -diffX, y: -diffY)
|
||||
originTransform = originTransform.translatedBy(x: -diffX / originScale, y: -diffY / originScale)
|
||||
} else if direction == .left {
|
||||
transform = transform.translatedBy(x: -diffY, y: diffX)
|
||||
originTransform = originTransform.translatedBy(x: -diffY / originScale, y: diffX / originScale)
|
||||
} else {
|
||||
transform = transform.translatedBy(x: diffX, y: diffY)
|
||||
originTransform = originTransform.translatedBy(x: diffX / originScale, y: diffY / originScale)
|
||||
}
|
||||
totalTranslationPoint.x += diffX
|
||||
totalTranslationPoint.y += diffY
|
||||
|
||||
transform = transform.scaledBy(x: scale, y: scale)
|
||||
|
||||
// Readd zoom scale.
|
||||
transform = transform.scaledBy(x: originScale, y: originScale)
|
||||
// Readd ges scale.
|
||||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||||
// Readd ges rotation.
|
||||
transform = transform.rotated(by: gesRotation)
|
||||
|
||||
gesScale *= scale
|
||||
maxGesScale *= scale
|
||||
}
|
||||
}
|
||||
1351
Pods/ZLPhotoBrowser/Sources/Edit/ZLClipImageViewController.swift
generated
Normal file
1818
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditImageViewController.swift
generated
Normal file
223
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditToolCells.swift
generated
Normal file
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// ZLEditToolCells.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/12/16.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: Edit tool cell
|
||||
|
||||
class ZLEditToolCell: UICollectionViewCell {
|
||||
var toolType: ZLEditImageConfiguration.EditTool = .draw {
|
||||
didSet {
|
||||
switch toolType {
|
||||
case .draw:
|
||||
icon.image = .zl.getImage("zl_drawLine")
|
||||
icon.highlightedImage = .zl.getImage("zl_drawLine_selected")
|
||||
case .clip:
|
||||
icon.image = .zl.getImage("zl_clip")
|
||||
icon.highlightedImage = .zl.getImage("zl_clip")
|
||||
case .imageSticker:
|
||||
icon.image = .zl.getImage("zl_imageSticker")
|
||||
icon.highlightedImage = .zl.getImage("zl_imageSticker")
|
||||
case .textSticker:
|
||||
icon.image = .zl.getImage("zl_textSticker")
|
||||
icon.highlightedImage = .zl.getImage("zl_textSticker")
|
||||
case .mosaic:
|
||||
icon.image = .zl.getImage("zl_mosaic")
|
||||
icon.highlightedImage = .zl.getImage("zl_mosaic_selected")
|
||||
case .filter:
|
||||
icon.image = .zl.getImage("zl_filter")
|
||||
icon.highlightedImage = .zl.getImage("zl_filter_selected")
|
||||
case .adjust:
|
||||
icon.image = .zl.getImage("zl_adjust")
|
||||
icon.highlightedImage = .zl.getImage("zl_adjust_selected")
|
||||
}
|
||||
if let color = UIColor.zl.imageEditorToolIconTintColor {
|
||||
icon.highlightedImage = icon.highlightedImage?
|
||||
.zl.fillColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy var icon = UIImageView(frame: contentView.bounds)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(icon)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: draw color cell
|
||||
|
||||
class ZLDrawColorCell: UICollectionViewCell {
|
||||
lazy var colorView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = 10
|
||||
view.layer.masksToBounds = true
|
||||
view.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var bgWhiteView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = 12
|
||||
view.layer.masksToBounds = true
|
||||
view.frame = CGRect(x: 0, y: 0, width: 24, height: 24)
|
||||
return view
|
||||
}()
|
||||
|
||||
var color: UIColor = .clear {
|
||||
didSet {
|
||||
colorView.backgroundColor = color
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(bgWhiteView)
|
||||
contentView.addSubview(colorView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
colorView.center = contentView.center
|
||||
bgWhiteView.center = contentView.center
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: filter cell
|
||||
|
||||
class ZLFilterImageCell: UICollectionViewCell {
|
||||
lazy var nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.frame = CGRect(x: 0, y: bounds.height - 20, width: bounds.width, height: 20)
|
||||
label.font = .zl.font(ofSize: 12)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.3).cgColor
|
||||
label.layer.shadowOffset = .zero
|
||||
label.layer.shadowOpacity = 1
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.width)
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: adjust tool cell
|
||||
|
||||
class ZLAdjustToolCell: UICollectionViewCell {
|
||||
lazy var nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.frame = CGRect(x: 0, y: bounds.height - 30, width: bounds.width, height: 30)
|
||||
label.font = .zl.font(ofSize: 12)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 2
|
||||
label.lineBreakMode = .byCharWrapping
|
||||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.3).cgColor
|
||||
label.layer.shadowOffset = .zero
|
||||
label.layer.shadowOpacity = 1
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.frame = CGRect(x: (bounds.width - 30) / 2, y: 0, width: 30, height: 30)
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
var adjustTool: ZLEditImageConfiguration.AdjustTool = .brightness {
|
||||
didSet {
|
||||
switch adjustTool {
|
||||
case .brightness:
|
||||
imageView.image = .zl.getImage("zl_brightness")
|
||||
imageView.highlightedImage = .zl.getImage("zl_brightness_selected")
|
||||
nameLabel.text = localLanguageTextValue(.brightness)
|
||||
case .contrast:
|
||||
imageView.image = .zl.getImage("zl_contrast")
|
||||
imageView.highlightedImage = .zl.getImage("zl_contrast_selected")
|
||||
nameLabel.text = localLanguageTextValue(.contrast)
|
||||
case .saturation:
|
||||
imageView.image = .zl.getImage("zl_saturation")
|
||||
imageView.highlightedImage = .zl.getImage("zl_saturation_selected")
|
||||
nameLabel.text = localLanguageTextValue(.saturation)
|
||||
}
|
||||
if let color = UIColor.zl.imageEditorToolIconTintColor {
|
||||
imageView.highlightedImage = imageView.highlightedImage?
|
||||
.zl.fillColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
679
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditVideoViewController.swift
generated
Normal file
@@ -0,0 +1,679 @@
|
||||
//
|
||||
// ZLEditVideoViewController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/30.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
public class ZLEditVideoViewController: UIViewController {
|
||||
private static let frameImageSize = CGSize(width: CGFloat(round(50.0 * 2.0 / 3.0)), height: 50.0)
|
||||
|
||||
private let avAsset: AVAsset
|
||||
|
||||
private let animateDismiss: Bool
|
||||
|
||||
private lazy var cancelBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.cancel), for: .normal)
|
||||
btn.setTitleColor(.zl.bottomToolViewBtnNormalTitleColor, for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var doneBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.done), for: .normal)
|
||||
btn.setTitleColor(.zl.bottomToolViewBtnNormalTitleColor, for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
|
||||
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColor
|
||||
btn.layer.masksToBounds = true
|
||||
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
|
||||
return btn
|
||||
}()
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
private lazy var playerLayer: AVPlayerLayer = {
|
||||
let layer = AVPlayerLayer()
|
||||
layer.videoGravity = .resizeAspect
|
||||
return layer
|
||||
}()
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = ZLCollectionViewFlowLayout()
|
||||
layout.itemSize = ZLEditVideoViewController.frameImageSize
|
||||
layout.minimumLineSpacing = 0
|
||||
layout.minimumInteritemSpacing = 0
|
||||
layout.scrollDirection = .horizontal
|
||||
|
||||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
view.backgroundColor = .clear
|
||||
view.delegate = self
|
||||
view.dataSource = self
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
ZLEditVideoFrameImageCell.zl.register(view)
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var frameImageBorderView: ZLEditVideoFrameImageBorderView = {
|
||||
let view = ZLEditVideoFrameImageBorderView()
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var leftSideView: UIImageView = {
|
||||
let view = UIImageView(image: .zl.getImage("zl_ic_left"))
|
||||
view.isUserInteractionEnabled = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var rightSideView: UIImageView = {
|
||||
let view = UIImageView(image: .zl.getImage("zl_ic_right"))
|
||||
view.isUserInteractionEnabled = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var leftSidePan: UIPanGestureRecognizer = {
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(leftSidePanAction(_:)))
|
||||
pan.delegate = self
|
||||
return pan
|
||||
}()
|
||||
|
||||
private lazy var rightSidePan: UIPanGestureRecognizer = {
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(rightSidePanAction(_:)))
|
||||
pan.delegate = self
|
||||
return pan
|
||||
}()
|
||||
|
||||
private lazy var indicator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white.withAlphaComponent(0.7)
|
||||
return view
|
||||
}()
|
||||
|
||||
private var measureCount = 0
|
||||
|
||||
private lazy var interval: TimeInterval = {
|
||||
let assetDuration = round(self.avAsset.duration.seconds)
|
||||
return min(assetDuration, TimeInterval(ZLPhotoConfiguration.default().maxEditVideoTime)) / 10
|
||||
}()
|
||||
|
||||
private lazy var requestFrameImageQueue: OperationQueue = {
|
||||
let queue = OperationQueue()
|
||||
queue.maxConcurrentOperationCount = 10
|
||||
return queue
|
||||
}()
|
||||
|
||||
private lazy var avAssetRequestID = PHInvalidImageRequestID
|
||||
|
||||
private lazy var videoRequestID = PHInvalidImageRequestID
|
||||
|
||||
private var frameImageCache: [Int: UIImage] = [:]
|
||||
|
||||
private var requestFailedFrameImageIndex: [Int] = []
|
||||
|
||||
private var shouldLayout = true
|
||||
|
||||
private lazy var generator: AVAssetImageGenerator = {
|
||||
let g = AVAssetImageGenerator(asset: self.avAsset)
|
||||
g.maximumSize = CGSize(width: ZLEditVideoViewController.frameImageSize.width * 3, height: ZLEditVideoViewController.frameImageSize.height * 3)
|
||||
g.appliesPreferredTrackTransform = true
|
||||
g.requestedTimeToleranceBefore = .zero
|
||||
g.requestedTimeToleranceAfter = .zero
|
||||
g.apertureMode = .productionAperture
|
||||
return g
|
||||
}()
|
||||
|
||||
@objc public var editFinishBlock: ((URL?) -> Void)?
|
||||
|
||||
override public var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLEditVideoViewController deinit")
|
||||
cleanTimer()
|
||||
requestFrameImageQueue.cancelAllOperations()
|
||||
if avAssetRequestID > PHInvalidImageRequestID {
|
||||
PHImageManager.default().cancelImageRequest(avAssetRequestID)
|
||||
}
|
||||
if videoRequestID > PHInvalidImageRequestID {
|
||||
PHImageManager.default().cancelImageRequest(videoRequestID)
|
||||
}
|
||||
}
|
||||
|
||||
/// initialize
|
||||
/// - Parameters:
|
||||
/// - avAsset: AVAsset对象,需要传入本地视频,网络视频不支持
|
||||
/// - animateDismiss: 退出界面时是否显示dismiss动画
|
||||
@objc public init(avAsset: AVAsset, animateDismiss: Bool = false) {
|
||||
self.avAsset = avAsset
|
||||
self.animateDismiss = animateDismiss
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupUI()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
analysisAssetImages()
|
||||
}
|
||||
|
||||
override public func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
guard shouldLayout else {
|
||||
return
|
||||
}
|
||||
shouldLayout = false
|
||||
|
||||
zl_debugPrint("edit video layout subviews")
|
||||
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
|
||||
if #available(iOS 11.0, *) {
|
||||
insets = self.view.safeAreaInsets
|
||||
}
|
||||
|
||||
let btnH = ZLLayout.bottomToolBtnH
|
||||
let bottomBtnAndColSpacing: CGFloat = 20
|
||||
let playerLayerY = insets.top + 20
|
||||
let diffBottom = btnH + ZLEditVideoViewController.frameImageSize.height + bottomBtnAndColSpacing + insets.bottom + 30
|
||||
|
||||
playerLayer.frame = CGRect(x: 15, y: insets.top + 20, width: view.bounds.width - 30, height: view.bounds.height - playerLayerY - diffBottom)
|
||||
|
||||
let cancelBtnW = localLanguageTextValue(.cancel).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: btnH)).width
|
||||
cancelBtn.frame = CGRect(x: 20, y: view.bounds.height - insets.bottom - btnH, width: cancelBtnW, height: btnH)
|
||||
let doneBtnW = localLanguageTextValue(.done).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: btnH)).width + 20
|
||||
doneBtn.frame = CGRect(x: view.bounds.width - doneBtnW - 20, y: view.bounds.height - insets.bottom - btnH, width: doneBtnW, height: btnH)
|
||||
|
||||
collectionView.frame = CGRect(x: 0, y: doneBtn.frame.minY - bottomBtnAndColSpacing - ZLEditVideoViewController.frameImageSize.height, width: view.bounds.width, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
|
||||
let frameViewW = ZLEditVideoViewController.frameImageSize.width * 10
|
||||
frameImageBorderView.frame = CGRect(x: (view.bounds.width - frameViewW) / 2, y: collectionView.frame.minY, width: frameViewW, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
// 左右拖动view
|
||||
let leftRightSideViewW = ZLEditVideoViewController.frameImageSize.width / 2
|
||||
leftSideView.frame = CGRect(x: frameImageBorderView.frame.minX, y: collectionView.frame.minY, width: leftRightSideViewW, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
let rightSideViewX = view.bounds.width - frameImageBorderView.frame.minX - leftRightSideViewW
|
||||
rightSideView.frame = CGRect(x: rightSideViewX, y: collectionView.frame.minY, width: leftRightSideViewW, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
|
||||
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .black
|
||||
|
||||
view.layer.addSublayer(playerLayer)
|
||||
view.addSubview(collectionView)
|
||||
view.addSubview(frameImageBorderView)
|
||||
view.addSubview(indicator)
|
||||
view.addSubview(leftSideView)
|
||||
view.addSubview(rightSideView)
|
||||
|
||||
view.addGestureRecognizer(leftSidePan)
|
||||
view.addGestureRecognizer(rightSidePan)
|
||||
|
||||
collectionView.panGestureRecognizer.require(toFail: leftSidePan)
|
||||
collectionView.panGestureRecognizer.require(toFail: rightSidePan)
|
||||
rightSidePan.require(toFail: leftSidePan)
|
||||
|
||||
view.addSubview(cancelBtn)
|
||||
view.addSubview(doneBtn)
|
||||
}
|
||||
|
||||
@objc private func cancelBtnClick() {
|
||||
dismiss(animated: animateDismiss, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func doneBtnClick() {
|
||||
cleanTimer()
|
||||
|
||||
let d = CGFloat(interval) * clipRect().width / ZLEditVideoViewController.frameImageSize.width
|
||||
if ZLPhotoConfiguration.Second(round(d)) < ZLPhotoConfiguration.default().minSelectVideoDuration {
|
||||
let message = String(format: localLanguageTextValue(.shorterThanMinVideoDuration), ZLPhotoConfiguration.default().minSelectVideoDuration)
|
||||
showAlertView(message, self)
|
||||
return
|
||||
}
|
||||
if ZLPhotoConfiguration.Second(round(d)) > ZLPhotoConfiguration.default().maxSelectVideoDuration {
|
||||
let message = String(format: localLanguageTextValue(.longerThanMaxVideoDuration), ZLPhotoConfiguration.default().maxSelectVideoDuration)
|
||||
showAlertView(message, self)
|
||||
return
|
||||
}
|
||||
|
||||
// Max deviation is 0.01
|
||||
if abs(d - round(CGFloat(avAsset.duration.seconds))) <= 0.01 {
|
||||
dismiss(animated: animateDismiss) {
|
||||
self.editFinishBlock?(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let hud = ZLProgressHUD.show()
|
||||
|
||||
ZLVideoManager.exportEditVideo(for: avAsset, range: getTimeRange()) { [weak self] url, error in
|
||||
hud.hide()
|
||||
if let er = error {
|
||||
showAlertView(er.localizedDescription, self)
|
||||
} else if url != nil {
|
||||
self?.dismiss(animated: self?.animateDismiss ?? false) {
|
||||
self?.editFinishBlock?(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func leftSidePanAction(_ pan: UIPanGestureRecognizer) {
|
||||
let point = pan.location(in: view)
|
||||
|
||||
if pan.state == .began {
|
||||
frameImageBorderView.layer.borderColor = UIColor(white: 1, alpha: 0.4).cgColor
|
||||
cleanTimer()
|
||||
} else if pan.state == .changed {
|
||||
let minX = frameImageBorderView.frame.minX
|
||||
let maxX = rightSideView.frame.minX - leftSideView.frame.width
|
||||
|
||||
var frame = leftSideView.frame
|
||||
frame.origin.x = min(maxX, max(minX, point.x))
|
||||
leftSideView.frame = frame
|
||||
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
|
||||
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
} else if pan.state == .ended || pan.state == .cancelled {
|
||||
frameImageBorderView.layer.borderColor = UIColor.clear.cgColor
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func rightSidePanAction(_ pan: UIPanGestureRecognizer) {
|
||||
let point = pan.location(in: view)
|
||||
|
||||
if pan.state == .began {
|
||||
frameImageBorderView.layer.borderColor = UIColor(white: 1, alpha: 0.4).cgColor
|
||||
cleanTimer()
|
||||
} else if pan.state == .changed {
|
||||
let minX = leftSideView.frame.maxX
|
||||
let maxX = frameImageBorderView.frame.maxX - rightSideView.frame.width
|
||||
|
||||
var frame = rightSideView.frame
|
||||
frame.origin.x = min(maxX, max(minX, point.x))
|
||||
rightSideView.frame = frame
|
||||
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
|
||||
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
} else if pan.state == .ended || pan.state == .cancelled {
|
||||
frameImageBorderView.layer.borderColor = UIColor.clear.cgColor
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func appWillResignActive() {
|
||||
cleanTimer()
|
||||
indicator.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
@objc private func appDidBecomeActive() {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
private func analysisAssetImages() {
|
||||
let duration = round(avAsset.duration.seconds)
|
||||
guard duration > 0 else {
|
||||
showFetchFailedAlert()
|
||||
return
|
||||
}
|
||||
let item = AVPlayerItem(asset: avAsset)
|
||||
let player = AVPlayer(playerItem: item)
|
||||
playerLayer.player = player
|
||||
startTimer()
|
||||
|
||||
measureCount = Int(duration / interval)
|
||||
collectionView.reloadData()
|
||||
requestVideoMeasureFrameImage()
|
||||
}
|
||||
|
||||
private func requestVideoMeasureFrameImage() {
|
||||
for i in 0..<measureCount {
|
||||
let mes = TimeInterval(i) * interval
|
||||
let time = CMTimeMakeWithSeconds(Float64(mes), preferredTimescale: avAsset.duration.timescale)
|
||||
|
||||
let operation = ZLEditVideoFetchFrameImageOperation(generator: generator, time: time) { [weak self] image, _ in
|
||||
self?.frameImageCache[Int(i)] = image
|
||||
let cell = self?.collectionView.cellForItem(at: IndexPath(row: Int(i), section: 0)) as? ZLEditVideoFrameImageCell
|
||||
cell?.imageView.image = image
|
||||
if image == nil {
|
||||
self?.requestFailedFrameImageIndex.append(i)
|
||||
}
|
||||
}
|
||||
requestFrameImageQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func playPartVideo() {
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
if (playerLayer.player?.rate ?? 0) == 0 {
|
||||
playerLayer.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
cleanTimer()
|
||||
let duration = interval * TimeInterval(clipRect().width / ZLEditVideoViewController.frameImageSize.width)
|
||||
|
||||
timer = Timer.scheduledTimer(timeInterval: duration, target: ZLWeakProxy(target: self), selector: #selector(playPartVideo), userInfo: nil, repeats: true)
|
||||
timer?.fire()
|
||||
RunLoop.main.add(timer!, forMode: .common)
|
||||
|
||||
indicator.isHidden = false
|
||||
|
||||
|
||||
let indicatorW: CGFloat = 2
|
||||
let indicatorH = leftSideView.zl.height
|
||||
let indicatorY = leftSideView.zl.top
|
||||
var indicatorFromX = leftSideView.zl.left
|
||||
var indicatorToX = rightSideView.zl.right - indicatorW
|
||||
|
||||
if isRTL() {
|
||||
swap(&indicatorFromX, &indicatorToX)
|
||||
}
|
||||
|
||||
let fromFrame = CGRect(x: indicatorFromX, y: indicatorY, width: indicatorW, height: indicatorH)
|
||||
indicator.frame = fromFrame
|
||||
|
||||
var toFrame = fromFrame
|
||||
toFrame.origin.x = indicatorToX
|
||||
|
||||
indicator.layer.removeAllAnimations()
|
||||
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .curveLinear, .repeat], animations: {
|
||||
self.indicator.frame = toFrame
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
private func cleanTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
indicator.layer.removeAllAnimations()
|
||||
indicator.isHidden = true
|
||||
playerLayer.player?.pause()
|
||||
}
|
||||
|
||||
private func getStartTime() -> CMTime {
|
||||
var rect = collectionView.convert(clipRect(), from: view)
|
||||
rect.origin.x -= frameImageBorderView.frame.minX
|
||||
let second = max(0, CGFloat(interval) * rect.minX / ZLEditVideoViewController.frameImageSize.width)
|
||||
return CMTimeMakeWithSeconds(Float64(second), preferredTimescale: avAsset.duration.timescale)
|
||||
}
|
||||
|
||||
private func getTimeRange() -> CMTimeRange {
|
||||
let start = getStartTime()
|
||||
let d = CGFloat(interval) * clipRect().width / ZLEditVideoViewController.frameImageSize.width
|
||||
let duration = CMTimeMakeWithSeconds(Float64(d), preferredTimescale: avAsset.duration.timescale)
|
||||
return CMTimeRangeMake(start: start, duration: duration)
|
||||
}
|
||||
|
||||
private func clipRect() -> CGRect {
|
||||
var frame = CGRect.zero
|
||||
frame.origin.x = leftSideView.frame.minX
|
||||
frame.origin.y = leftSideView.frame.minY
|
||||
frame.size.width = rightSideView.frame.maxX - frame.minX
|
||||
frame.size.height = leftSideView.frame.height
|
||||
return frame
|
||||
}
|
||||
|
||||
private func showFetchFailedAlert() {
|
||||
let action = ZLCustomAlertAction(title: localLanguageTextValue(.ok), style: .default) { [weak self] _ in
|
||||
self?.dismiss(animated: false)
|
||||
}
|
||||
showAlertController(title: nil, message: localLanguageTextValue(.iCloudVideoLoadFaild), style: .alert, actions: [action], sender: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLEditVideoViewController: UIGestureRecognizerDelegate {
|
||||
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer == leftSidePan {
|
||||
let point = gestureRecognizer.location(in: view)
|
||||
let frame = leftSideView.frame
|
||||
let outerFrame = frame.inset(by: UIEdgeInsets(top: -20, left: -40, bottom: -20, right: -20))
|
||||
return outerFrame.contains(point)
|
||||
} else if gestureRecognizer == rightSidePan {
|
||||
let point = gestureRecognizer.location(in: view)
|
||||
let frame = rightSideView.frame
|
||||
let outerFrame = frame.inset(by: UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -40))
|
||||
return outerFrame.contains(point)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLEditVideoViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
cleanTimer()
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
let w = ZLEditVideoViewController.frameImageSize.width * 10
|
||||
let leftRight = (collectionView.frame.width - w) / 2
|
||||
return UIEdgeInsets(top: 0, left: leftRight, bottom: 0, right: leftRight)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return measureCount
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLEditVideoFrameImageCell.zl.identifier, for: indexPath) as! ZLEditVideoFrameImageCell
|
||||
|
||||
if let image = frameImageCache[indexPath.row] {
|
||||
cell.imageView.image = image
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
if requestFailedFrameImageIndex.contains(indexPath.row) {
|
||||
let mes = TimeInterval(indexPath.row) * interval
|
||||
let time = CMTimeMakeWithSeconds(Float64(mes), preferredTimescale: avAsset.duration.timescale)
|
||||
|
||||
let operation = ZLEditVideoFetchFrameImageOperation(generator: generator, time: time) { [weak self] image, _ in
|
||||
self?.frameImageCache[indexPath.row] = image
|
||||
let cell = self?.collectionView.cellForItem(at: IndexPath(row: indexPath.row, section: 0)) as? ZLEditVideoFrameImageCell
|
||||
cell?.imageView.image = image
|
||||
if image != nil {
|
||||
self?.requestFailedFrameImageIndex.removeAll { $0 == indexPath.row }
|
||||
}
|
||||
}
|
||||
requestFrameImageQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZLEditVideoFrameImageBorderView: UIView {
|
||||
var validRect: CGRect = .zero {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
layer.borderWidth = 2
|
||||
layer.borderColor = UIColor.clear.cgColor
|
||||
backgroundColor = .clear
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
context?.setStrokeColor(UIColor.white.cgColor)
|
||||
context?.setLineWidth(4)
|
||||
|
||||
context?.move(to: CGPoint(x: validRect.minX, y: 0))
|
||||
context?.addLine(to: CGPoint(x: validRect.minX + validRect.width, y: 0))
|
||||
|
||||
context?.move(to: CGPoint(x: validRect.minX, y: rect.height))
|
||||
context?.addLine(to: CGPoint(x: validRect.minX + validRect.width, y: rect.height))
|
||||
|
||||
context?.strokePath()
|
||||
}
|
||||
}
|
||||
|
||||
class ZLEditVideoFrameImageCell: UICollectionViewCell {
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
imageView.frame = bounds
|
||||
}
|
||||
}
|
||||
|
||||
class ZLEditVideoFetchFrameImageOperation: Operation {
|
||||
private let generator: AVAssetImageGenerator
|
||||
|
||||
private let time: CMTime
|
||||
|
||||
let completion: (UIImage?, CMTime) -> Void
|
||||
|
||||
var pri_isExecuting = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
}
|
||||
|
||||
override var isExecuting: Bool {
|
||||
return pri_isExecuting
|
||||
}
|
||||
|
||||
var pri_isFinished = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isFinished")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isFinished")
|
||||
}
|
||||
}
|
||||
|
||||
override var isFinished: Bool {
|
||||
return pri_isFinished
|
||||
}
|
||||
|
||||
var pri_isCancelled = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isCancelled")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isCancelled")
|
||||
}
|
||||
}
|
||||
|
||||
override var isCancelled: Bool {
|
||||
return pri_isCancelled
|
||||
}
|
||||
|
||||
init(generator: AVAssetImageGenerator, time: CMTime, completion: @escaping ((UIImage?, CMTime) -> Void)) {
|
||||
self.generator = generator
|
||||
self.time = time
|
||||
self.completion = completion
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func start() {
|
||||
if isCancelled {
|
||||
fetchFinish()
|
||||
return
|
||||
}
|
||||
pri_isExecuting = true
|
||||
generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { _, cgImage, _, result, _ in
|
||||
if result == .succeeded, let cg = cgImage {
|
||||
let image = UIImage(cgImage: cg)
|
||||
ZLMainAsync {
|
||||
self.completion(image, self.time)
|
||||
}
|
||||
self.fetchFinish()
|
||||
} else {
|
||||
self.fetchFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
pri_isCancelled = true
|
||||
}
|
||||
|
||||
private func fetchFinish() {
|
||||
pri_isExecuting = false
|
||||
pri_isFinished = true
|
||||
}
|
||||
}
|
||||
281
Pods/ZLPhotoBrowser/Sources/Edit/ZLFilter.swift
generated
Normal file
@@ -0,0 +1,281 @@
|
||||
//
|
||||
// ZLFilter.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/9.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Filter code reference from https://github.com/Yummypets/YPImagePicker
|
||||
|
||||
public typealias ZLFilterApplierType = (_ image: UIImage) -> UIImage
|
||||
|
||||
@objc public enum ZLFilterType: Int {
|
||||
case normal
|
||||
case chrome
|
||||
case fade
|
||||
case instant
|
||||
case process
|
||||
case transfer
|
||||
case tone
|
||||
case linear
|
||||
case sepia
|
||||
case mono
|
||||
case noir
|
||||
case tonal
|
||||
|
||||
var coreImageFilterName: String {
|
||||
switch self {
|
||||
case .normal:
|
||||
return ""
|
||||
case .chrome:
|
||||
return "CIPhotoEffectChrome"
|
||||
case .fade:
|
||||
return "CIPhotoEffectFade"
|
||||
case .instant:
|
||||
return "CIPhotoEffectInstant"
|
||||
case .process:
|
||||
return "CIPhotoEffectProcess"
|
||||
case .transfer:
|
||||
return "CIPhotoEffectTransfer"
|
||||
case .tone:
|
||||
return "CILinearToSRGBToneCurve"
|
||||
case .linear:
|
||||
return "CISRGBToneCurveToLinear"
|
||||
case .sepia:
|
||||
return "CISepiaTone"
|
||||
case .mono:
|
||||
return "CIPhotoEffectMono"
|
||||
case .noir:
|
||||
return "CIPhotoEffectNoir"
|
||||
case .tonal:
|
||||
return "CIPhotoEffectTonal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ZLFilter: NSObject {
|
||||
public var name: String
|
||||
|
||||
let applier: ZLFilterApplierType?
|
||||
|
||||
@objc public init(name: String, filterType: ZLFilterType) {
|
||||
self.name = name
|
||||
|
||||
if filterType != .normal {
|
||||
applier = { image -> UIImage in
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let filter = CIFilter(name: filterType.coreImageFilterName)
|
||||
filter?.setValue(ciImage, forKey: kCIInputImageKey)
|
||||
guard let outputImage = filter?.outputImage?.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
} else {
|
||||
applier = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 可传入 applier 自定义滤镜
|
||||
@objc public init(name: String, applier: ZLFilterApplierType?) {
|
||||
self.name = name
|
||||
self.applier = applier
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLFilter {
|
||||
class func clarendonFilter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let backgroundImage = getColorImage(red: 127, green: 187, blue: 227, alpha: Int(255 * 0.2), rect: ciImage.extent)
|
||||
let outputCIImage = ciImage.applyingFilter("CIOverlayBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage,
|
||||
])
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.35,
|
||||
"inputBrightness": 0.05,
|
||||
"inputContrast": 1.1,
|
||||
])
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func nashvilleFilter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let backgroundImage = getColorImage(red: 247, green: 176, blue: 153, alpha: Int(255 * 0.56), rect: ciImage.extent)
|
||||
let backgroundImage2 = getColorImage(red: 0, green: 70, blue: 150, alpha: Int(255 * 0.4), rect: ciImage.extent)
|
||||
let outputCIImage = ciImage
|
||||
.applyingFilter("CIDarkenBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage,
|
||||
])
|
||||
.applyingFilter("CISepiaTone", parameters: [
|
||||
"inputIntensity": 0.2,
|
||||
])
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.2,
|
||||
"inputBrightness": 0.05,
|
||||
"inputContrast": 1.1,
|
||||
])
|
||||
.applyingFilter("CILightenBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage2,
|
||||
])
|
||||
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func apply1977Filter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let filterImage = getColorImage(red: 243, green: 106, blue: 188, alpha: Int(255 * 0.1), rect: ciImage.extent)
|
||||
let backgroundImage = ciImage
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.3,
|
||||
"inputBrightness": 0.1,
|
||||
"inputContrast": 1.05,
|
||||
])
|
||||
.applyingFilter("CIHueAdjust", parameters: [
|
||||
"inputAngle": 0.3,
|
||||
])
|
||||
|
||||
let outputCIImage = filterImage
|
||||
.applyingFilter("CIScreenBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage,
|
||||
])
|
||||
.applyingFilter("CIToneCurve", parameters: [
|
||||
"inputPoint0": CIVector(x: 0, y: 0),
|
||||
"inputPoint1": CIVector(x: 0.25, y: 0.20),
|
||||
"inputPoint2": CIVector(x: 0.5, y: 0.5),
|
||||
"inputPoint3": CIVector(x: 0.75, y: 0.80),
|
||||
"inputPoint4": CIVector(x: 1, y: 1),
|
||||
])
|
||||
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func toasterFilter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let width = ciImage.extent.width
|
||||
let height = ciImage.extent.height
|
||||
let centerWidth = width / 2.0
|
||||
let centerHeight = height / 2.0
|
||||
let radius0 = min(width / 4.0, height / 4.0)
|
||||
let radius1 = min(width / 1.5, height / 1.5)
|
||||
|
||||
let color0 = getColor(red: 128, green: 78, blue: 15, alpha: 255)
|
||||
let color1 = getColor(red: 79, green: 0, blue: 79, alpha: 255)
|
||||
let circle = CIFilter(name: "CIRadialGradient", parameters: [
|
||||
"inputCenter": CIVector(x: centerWidth, y: centerHeight),
|
||||
"inputRadius0": radius0,
|
||||
"inputRadius1": radius1,
|
||||
"inputColor0": color0,
|
||||
"inputColor1": color1,
|
||||
])?.outputImage?.cropped(to: ciImage.extent)
|
||||
|
||||
let outputCIImage = ciImage
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.0,
|
||||
"inputBrightness": 0.01,
|
||||
"inputContrast": 1.1,
|
||||
])
|
||||
.applyingFilter("CIScreenBlendMode", parameters: [
|
||||
"inputBackgroundImage": circle!,
|
||||
])
|
||||
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func getColor(red: Int, green: Int, blue: Int, alpha: Int = 255) -> CIColor {
|
||||
return CIColor(
|
||||
red: CGFloat(Double(red) / 255.0),
|
||||
green: CGFloat(Double(green) / 255.0),
|
||||
blue: CGFloat(Double(blue) / 255.0),
|
||||
alpha: CGFloat(Double(alpha) / 255.0)
|
||||
)
|
||||
}
|
||||
|
||||
class func getColorImage(red: Int, green: Int, blue: Int, alpha: Int = 255, rect: CGRect) -> CIImage {
|
||||
let color = getColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
return CIImage(color: color).cropped(to: rect)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLFilter {
|
||||
@objc static let all: [ZLFilter] = [.normal, .clarendon, .nashville, .apply1977, .toaster, .chrome, .fade, .instant, .process, .transfer, .tone, .linear, .sepia, .mono, .noir, .tonal]
|
||||
|
||||
@objc static let normal = ZLFilter(name: "Normal", filterType: .normal)
|
||||
|
||||
@objc static let clarendon = ZLFilter(name: "Clarendon", applier: ZLFilter.clarendonFilter)
|
||||
|
||||
@objc static let nashville = ZLFilter(name: "Nashville", applier: ZLFilter.nashvilleFilter)
|
||||
|
||||
@objc static let apply1977 = ZLFilter(name: "1977", applier: ZLFilter.apply1977Filter)
|
||||
|
||||
@objc static let toaster = ZLFilter(name: "Toaster", applier: ZLFilter.toasterFilter)
|
||||
|
||||
@objc static let chrome = ZLFilter(name: "Chrome", filterType: .chrome)
|
||||
|
||||
@objc static let fade = ZLFilter(name: "Fade", filterType: .fade)
|
||||
|
||||
@objc static let instant = ZLFilter(name: "Instant", filterType: .instant)
|
||||
|
||||
@objc static let process = ZLFilter(name: "Process", filterType: .process)
|
||||
|
||||
@objc static let transfer = ZLFilter(name: "Transfer", filterType: .transfer)
|
||||
|
||||
@objc static let tone = ZLFilter(name: "Tone", filterType: .tone)
|
||||
|
||||
@objc static let linear = ZLFilter(name: "Linear", filterType: .linear)
|
||||
|
||||
@objc static let sepia = ZLFilter(name: "Sepia", filterType: .sepia)
|
||||
|
||||
@objc static let mono = ZLFilter(name: "Mono", filterType: .mono)
|
||||
|
||||
@objc static let noir = ZLFilter(name: "Noir", filterType: .noir)
|
||||
|
||||
@objc static let tonal = ZLFilter(name: "Tonal", filterType: .tonal)
|
||||
}
|
||||
143
Pods/ZLPhotoBrowser/Sources/Edit/ZLImageStickerView.swift
generated
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// ZLImageStickerView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/11/20.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLImageStickerView: ZLBaseStickerView<ZLImageStickerState> {
|
||||
private let image: UIImage
|
||||
|
||||
private static let edgeInset: CGFloat = 20
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView(image: image)
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
// Convert all states to model.
|
||||
override var state: ZLImageStickerState {
|
||||
return ZLImageStickerState(
|
||||
image: image,
|
||||
originScale: originScale,
|
||||
originAngle: originAngle,
|
||||
originFrame: originFrame,
|
||||
gesScale: gesScale,
|
||||
gesRotation: gesRotation,
|
||||
totalTranslationPoint: totalTranslationPoint
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLImageStickerView deinit")
|
||||
}
|
||||
|
||||
convenience init(state: ZLImageStickerState) {
|
||||
self.init(
|
||||
image: state.image,
|
||||
originScale: state.originScale,
|
||||
originAngle: state.originAngle,
|
||||
originFrame: state.originFrame,
|
||||
gesScale: state.gesScale,
|
||||
gesRotation: state.gesRotation,
|
||||
totalTranslationPoint: state.totalTranslationPoint,
|
||||
showBorder: false
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat = 1,
|
||||
gesRotation: CGFloat = 0,
|
||||
totalTranslationPoint: CGPoint = .zero,
|
||||
showBorder: Bool = true
|
||||
) {
|
||||
self.image = image
|
||||
super.init(originScale: originScale, originAngle: originAngle, originFrame: originFrame, gesScale: gesScale, gesRotation: gesRotation, totalTranslationPoint: totalTranslationPoint, showBorder: showBorder)
|
||||
|
||||
borderView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupUIFrameWhenFirstLayout() {
|
||||
imageView.frame = bounds.insetBy(dx: Self.edgeInset, dy: Self.edgeInset)
|
||||
}
|
||||
|
||||
class func calculateSize(image: UIImage, width: CGFloat) -> CGSize {
|
||||
let maxSide = width / 2
|
||||
let minSide: CGFloat = 100
|
||||
let whRatio = image.size.width / image.size.height
|
||||
var size: CGSize = .zero
|
||||
if whRatio >= 1 {
|
||||
let w = min(maxSide, max(minSide, image.size.width))
|
||||
let h = w / whRatio
|
||||
size = CGSize(width: w, height: h)
|
||||
} else {
|
||||
let h = min(maxSide, max(minSide, image.size.width))
|
||||
let w = h * whRatio
|
||||
size = CGSize(width: w, height: h)
|
||||
}
|
||||
size.width += Self.edgeInset * 2
|
||||
size.height += Self.edgeInset * 2
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public class ZLImageStickerState: NSObject {
|
||||
let image: UIImage
|
||||
let originScale: CGFloat
|
||||
let originAngle: CGFloat
|
||||
let originFrame: CGRect
|
||||
let gesScale: CGFloat
|
||||
let gesRotation: CGFloat
|
||||
let totalTranslationPoint: CGPoint
|
||||
|
||||
init(
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat,
|
||||
gesRotation: CGFloat,
|
||||
totalTranslationPoint: CGPoint
|
||||
) {
|
||||
self.image = image
|
||||
self.originScale = originScale
|
||||
self.originAngle = originAngle
|
||||
self.originFrame = originFrame
|
||||
self.gesScale = gesScale
|
||||
self.gesRotation = gesRotation
|
||||
self.totalTranslationPoint = totalTranslationPoint
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
517
Pods/ZLPhotoBrowser/Sources/Edit/ZLInputTextViewController.swift
generated
Normal file
@@ -0,0 +1,517 @@
|
||||
//
|
||||
// ZLInputTextViewController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/30.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLInputTextViewController: UIViewController {
|
||||
private static let toolViewHeight: CGFloat = 70
|
||||
|
||||
private let image: UIImage?
|
||||
|
||||
private var text: String
|
||||
|
||||
private var currentColor: UIColor {
|
||||
didSet {
|
||||
refreshTextViewUI()
|
||||
}
|
||||
}
|
||||
|
||||
private var textStyle: ZLInputTextStyle
|
||||
|
||||
private lazy var cancelBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.cancel), for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var doneBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.done), for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.setTitleColor(.zl.bottomToolViewDoneBtnNormalTitleColor, for: .normal)
|
||||
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColor
|
||||
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
|
||||
btn.layer.masksToBounds = true
|
||||
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var textView: UITextView = {
|
||||
let y = max(deviceSafeAreaInsets().top, 20) + 20 + ZLLayout.bottomToolBtnH + 12
|
||||
let textView = UITextView(frame: CGRect(x: 10, y: y, width: view.zl.width - 20, height: 200))
|
||||
textView.keyboardAppearance = .dark
|
||||
textView.returnKeyType = .done
|
||||
textView.delegate = self
|
||||
textView.backgroundColor = .clear
|
||||
textView.tintColor = .zl.bottomToolViewBtnNormalBgColor
|
||||
textView.textColor = currentColor
|
||||
textView.text = text
|
||||
textView.font = .boldSystemFont(ofSize: ZLTextStickerView.fontSize)
|
||||
textView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.layoutManager.delegate = self
|
||||
return textView
|
||||
}()
|
||||
|
||||
private lazy var toolView = UIView(frame: CGRect(
|
||||
x: 0,
|
||||
y: view.zl.height - Self.toolViewHeight,
|
||||
width: view.zl.width,
|
||||
height: Self.toolViewHeight
|
||||
))
|
||||
|
||||
private lazy var textStyleBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.addTarget(self, action: #selector(textStyleBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = ZLCollectionViewFlowLayout()
|
||||
layout.itemSize = CGSize(width: 36, height: 36)
|
||||
layout.minimumLineSpacing = 0
|
||||
layout.minimumInteritemSpacing = 0
|
||||
layout.scrollDirection = .horizontal
|
||||
let inset = (Self.toolViewHeight - layout.itemSize.height) / 2
|
||||
layout.sectionInset = UIEdgeInsets(top: inset, left: 0, bottom: inset, right: 0)
|
||||
|
||||
let collectionView = UICollectionView(
|
||||
frame: .zero,
|
||||
collectionViewLayout: layout
|
||||
)
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
ZLDrawColorCell.zl.register(collectionView)
|
||||
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private lazy var textLayer = CAShapeLayer()
|
||||
|
||||
private let textLayerRadius: CGFloat = 10
|
||||
|
||||
private let maxTextCount = 100
|
||||
|
||||
/// text, textColor, image, style
|
||||
var endInput: ((String, UIColor, UIImage?, ZLInputTextStyle) -> Void)?
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLInputTextViewController deinit")
|
||||
}
|
||||
|
||||
init(image: UIImage?, text: String? = nil, textColor: UIColor? = nil, style: ZLInputTextStyle = .normal) {
|
||||
self.image = image
|
||||
self.text = text ?? ""
|
||||
if let textColor = textColor {
|
||||
currentColor = textColor
|
||||
} else {
|
||||
let editConfig = ZLPhotoConfiguration.default().editImageConfiguration
|
||||
if !editConfig.textStickerTextColors.contains(editConfig.textStickerDefaultTextColor) {
|
||||
currentColor = editConfig.textStickerTextColors.first!
|
||||
} else {
|
||||
currentColor = editConfig.textStickerDefaultTextColor
|
||||
}
|
||||
}
|
||||
self.textStyle = style
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupUI()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIApplication.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIApplication.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let btnY = max(deviceSafeAreaInsets().top, 20) + 20
|
||||
let cancelBtnW = localLanguageTextValue(.cancel).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: .greatestFiniteMagnitude, height: ZLLayout.bottomToolBtnH)).width + 20
|
||||
cancelBtn.frame = CGRect(x: 15, y: btnY, width: cancelBtnW, height: ZLLayout.bottomToolBtnH)
|
||||
|
||||
let doneBtnW = localLanguageTextValue(.done).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: .greatestFiniteMagnitude, height: ZLLayout.bottomToolBtnH)).width + 20
|
||||
doneBtn.frame = CGRect(x: view.zl.width - 20 - doneBtnW, y: btnY, width: doneBtnW, height: ZLLayout.bottomToolBtnH)
|
||||
|
||||
textStyleBtn.frame = CGRect(
|
||||
x: 12,
|
||||
y: 0,
|
||||
width: 50,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
collectionView.frame = CGRect(
|
||||
x: textStyleBtn.zl.right + 5,
|
||||
y: 0,
|
||||
width: view.zl.width - textStyleBtn.zl.right - 5 - 24,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
|
||||
if let index = ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors.firstIndex(where: { $0 == self.currentColor }) {
|
||||
collectionView.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .black
|
||||
|
||||
let bgImageView = UIImageView(image: image?.zl.blurImage(level: 4))
|
||||
bgImageView.frame = view.bounds
|
||||
bgImageView.contentMode = .scaleAspectFit
|
||||
view.addSubview(bgImageView)
|
||||
|
||||
let coverView = UIView(frame: bgImageView.bounds)
|
||||
coverView.backgroundColor = .black
|
||||
coverView.alpha = 0.4
|
||||
bgImageView.addSubview(coverView)
|
||||
|
||||
view.addSubview(cancelBtn)
|
||||
view.addSubview(doneBtn)
|
||||
view.addSubview(textView)
|
||||
view.addSubview(toolView)
|
||||
toolView.addSubview(textStyleBtn)
|
||||
toolView.addSubview(collectionView)
|
||||
|
||||
refreshTextViewUI()
|
||||
}
|
||||
|
||||
private func refreshTextViewUI() {
|
||||
textStyleBtn.setImage(textStyle.btnImage, for: .normal)
|
||||
textStyleBtn.setImage(textStyle.btnImage, for: .highlighted)
|
||||
|
||||
drawTextBackground()
|
||||
|
||||
guard textStyle == .bg else {
|
||||
textView.textColor = currentColor
|
||||
return
|
||||
}
|
||||
|
||||
if currentColor == .white {
|
||||
textView.textColor = .black
|
||||
} else if currentColor == .black {
|
||||
textView.textColor = .white
|
||||
} else {
|
||||
textView.textColor = .white
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func textStyleBtnClick() {
|
||||
if textStyle == .normal {
|
||||
textStyle = .bg
|
||||
} else {
|
||||
textStyle = .normal
|
||||
}
|
||||
|
||||
refreshTextViewUI()
|
||||
}
|
||||
|
||||
@objc private func cancelBtnClick() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func doneBtnClick() {
|
||||
textView.tintColor = .clear
|
||||
textView.endEditing(true)
|
||||
|
||||
var image: UIImage?
|
||||
|
||||
if !textView.text.isEmpty {
|
||||
for subview in textView.subviews {
|
||||
if NSStringFromClass(subview.classForCoder) == "_UITextContainerView" {
|
||||
let size = textView.sizeThatFits(subview.frame.size)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
if textStyle == .bg {
|
||||
textLayer.render(in: context)
|
||||
}
|
||||
|
||||
subview.layer.render(in: context)
|
||||
image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endInput?(textView.text, currentColor, image, textStyle)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func keyboardWillShow(_ notify: Notification) {
|
||||
let rect = notify.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect
|
||||
let keyboardH = rect?.height ?? 366
|
||||
let duration: TimeInterval = notify.userInfo?[UIApplication.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
|
||||
|
||||
let toolViewFrame = CGRect(
|
||||
x: 0,
|
||||
y: view.zl.height - keyboardH - Self.toolViewHeight,
|
||||
width: view.zl.width,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
|
||||
var textViewFrame = textView.frame
|
||||
textViewFrame.size.height = toolViewFrame.minY - textViewFrame.minY - 20
|
||||
|
||||
UIView.animate(withDuration: max(duration, 0.25)) {
|
||||
self.toolView.frame = toolViewFrame
|
||||
self.textView.frame = textViewFrame
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notify: Notification) {
|
||||
let duration: TimeInterval = notify.userInfo?[UIApplication.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
|
||||
|
||||
let toolViewFrame = CGRect(
|
||||
x: 0,
|
||||
y: view.zl.height - deviceSafeAreaInsets().bottom - Self.toolViewHeight,
|
||||
width: view.zl.width,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
|
||||
var textViewFrame = textView.frame
|
||||
textViewFrame.size.height = toolViewFrame.minY - textViewFrame.minY - 20
|
||||
|
||||
UIView.animate(withDuration: max(duration, 0.25)) {
|
||||
self.toolView.frame = toolViewFrame
|
||||
self.textView.frame = textViewFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLInputTextViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLDrawColorCell.zl.identifier, for: indexPath) as! ZLDrawColorCell
|
||||
|
||||
let c = ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors[indexPath.row]
|
||||
cell.color = c
|
||||
if c == currentColor {
|
||||
cell.bgWhiteView.layer.transform = CATransform3DMakeScale(1.33, 1.33, 1)
|
||||
cell.colorView.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1)
|
||||
} else {
|
||||
cell.bgWhiteView.layer.transform = CATransform3DIdentity
|
||||
cell.colorView.layer.transform = CATransform3DIdentity
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
currentColor = ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors[indexPath.row]
|
||||
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Draw text layer
|
||||
|
||||
extension ZLInputTextViewController {
|
||||
private func drawTextBackground() {
|
||||
guard textStyle == .bg, !textView.text.isEmpty else {
|
||||
textLayer.removeFromSuperlayer()
|
||||
return
|
||||
}
|
||||
|
||||
let rects = calculateTextRects()
|
||||
|
||||
let path = UIBezierPath()
|
||||
for (index, rect) in rects.enumerated() {
|
||||
if index == 0 {
|
||||
path.move(to: CGPoint(x: rect.minX, y: rect.minY + textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX + textLayerRadius, y: rect.minY + textLayerRadius), radius: textLayerRadius, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY + textLayerRadius), radius: textLayerRadius, startAngle: .pi * 1.5, endAngle: .pi * 2, clockwise: true)
|
||||
} else {
|
||||
let preRect = rects[index - 1]
|
||||
if rect.maxX > preRect.maxX {
|
||||
path.addLine(to: CGPoint(x: preRect.maxX, y: rect.minY - textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: preRect.maxX + textLayerRadius, y: rect.minY - textLayerRadius), radius: textLayerRadius, startAngle: -.pi, endAngle: -.pi * 1.5, clockwise: false)
|
||||
path.addLine(to: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY + textLayerRadius), radius: textLayerRadius, startAngle: .pi * 1.5, endAngle: .pi * 2, clockwise: true)
|
||||
} else if rect.maxX < preRect.maxX {
|
||||
path.addLine(to: CGPoint(x: preRect.maxX, y: preRect.maxY - textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: preRect.maxX - textLayerRadius, y: preRect.maxY - textLayerRadius), radius: textLayerRadius, startAngle: 0, endAngle: .pi / 2, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.maxX + textLayerRadius, y: preRect.maxY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX + textLayerRadius, y: preRect.maxY + textLayerRadius), radius: textLayerRadius, startAngle: -.pi / 2, endAngle: -.pi, clockwise: false)
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + textLayerRadius))
|
||||
}
|
||||
}
|
||||
|
||||
if index == rects.count - 1 {
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - textLayerRadius, y: rect.maxY - textLayerRadius), radius: textLayerRadius, startAngle: 0, endAngle: .pi / 2, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.minX + textLayerRadius, y: rect.maxY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX + textLayerRadius, y: rect.maxY - textLayerRadius), radius: textLayerRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: true)
|
||||
|
||||
let firstRect = rects[0]
|
||||
path.addLine(to: CGPoint(x: firstRect.minX, y: firstRect.minY + textLayerRadius))
|
||||
path.close()
|
||||
}
|
||||
}
|
||||
|
||||
textLayer.path = path.cgPath
|
||||
textLayer.fillColor = currentColor.cgColor
|
||||
if textLayer.superlayer == nil {
|
||||
textView.layer.insertSublayer(textLayer, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateTextRects() -> [CGRect] {
|
||||
let layoutManager = textView.layoutManager
|
||||
|
||||
// 这里必须用utf16.count 或者 (text as NSString).length,因为用count的话不准,一个emoji表情的count为2
|
||||
let range = layoutManager.glyphRange(forCharacterRange: NSMakeRange(0, textView.text.utf16.count), actualCharacterRange: nil)
|
||||
let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
|
||||
|
||||
var rects: [CGRect] = []
|
||||
|
||||
let insetLeft = textView.textContainerInset.left
|
||||
let insetTop = textView.textContainerInset.top
|
||||
layoutManager.enumerateLineFragments(forGlyphRange: glyphRange) { _, usedRect, _, _, _ in
|
||||
rects.append(CGRect(x: usedRect.minX - 10 + insetLeft, y: usedRect.minY - 8 + insetTop, width: usedRect.width + 20, height: usedRect.height + 16))
|
||||
}
|
||||
|
||||
guard rects.count > 1 else {
|
||||
return rects
|
||||
}
|
||||
|
||||
for i in 1..<rects.count {
|
||||
processRects(&rects, index: i, maxIndex: i)
|
||||
}
|
||||
|
||||
return rects
|
||||
}
|
||||
|
||||
private func processRects(_ rects: inout [CGRect], index: Int, maxIndex: Int) {
|
||||
guard rects.count > 1, index > 0, index <= maxIndex else {
|
||||
return
|
||||
}
|
||||
|
||||
var preRect = rects[index - 1]
|
||||
var currRect = rects[index]
|
||||
|
||||
var preChanged = false
|
||||
var currChanged = false
|
||||
|
||||
// 当前rect宽度大于上方的rect,但差值小于2倍圆角
|
||||
if currRect.width > preRect.width, currRect.width - preRect.width < 2 * textLayerRadius {
|
||||
var size = preRect.size
|
||||
size.width = currRect.width
|
||||
preRect = CGRect(origin: preRect.origin, size: size)
|
||||
preChanged = true
|
||||
}
|
||||
|
||||
if currRect.width < preRect.width, preRect.width - currRect.width < 2 * textLayerRadius {
|
||||
var size = currRect.size
|
||||
size.width = preRect.width
|
||||
currRect = CGRect(origin: currRect.origin, size: size)
|
||||
currChanged = true
|
||||
}
|
||||
|
||||
if preChanged {
|
||||
rects[index - 1] = preRect
|
||||
processRects(&rects, index: index - 1, maxIndex: maxIndex)
|
||||
}
|
||||
|
||||
if currChanged {
|
||||
rects[index] = currRect
|
||||
processRects(&rects, index: index + 1, maxIndex: maxIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLInputTextViewController: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
let markedTextRange = textView.markedTextRange
|
||||
guard markedTextRange == nil || (markedTextRange?.isEmpty ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
let text = textView.text ?? ""
|
||||
if text.count > maxTextCount {
|
||||
let endIndex = text.index(text.startIndex, offsetBy: maxTextCount)
|
||||
textView.text = String(text[..<endIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if text == "\n" {
|
||||
doneBtnClick()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLInputTextViewController: NSLayoutManagerDelegate {
|
||||
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
|
||||
guard layoutFinishedFlag else {
|
||||
return
|
||||
}
|
||||
|
||||
drawTextBackground()
|
||||
}
|
||||
}
|
||||
|
||||
public enum ZLInputTextStyle {
|
||||
case normal
|
||||
case bg
|
||||
|
||||
fileprivate var btnImage: UIImage? {
|
||||
switch self {
|
||||
case .normal:
|
||||
return .zl.getImage("zl_input_font")
|
||||
case .bg:
|
||||
return .zl.getImage("zl_input_font_bg")
|
||||
}
|
||||
}
|
||||
}
|
||||
209
Pods/ZLPhotoBrowser/Sources/Edit/ZLTextStickerView.swift
generated
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// ZLTextStickerView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/30.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLTextStickerView: ZLBaseStickerView<ZLTextStickerState> {
|
||||
static let fontSize: CGFloat = 32
|
||||
|
||||
private static let edgeInset: CGFloat = 10
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView(image: image)
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
var text: String
|
||||
|
||||
var textColor: UIColor
|
||||
|
||||
var style: ZLInputTextStyle
|
||||
|
||||
var image: UIImage {
|
||||
didSet {
|
||||
imageView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all states to model.
|
||||
override var state: ZLTextStickerState {
|
||||
return ZLTextStickerState(
|
||||
text: text,
|
||||
textColor: textColor,
|
||||
style: style,
|
||||
image: image,
|
||||
originScale: originScale,
|
||||
originAngle: originAngle,
|
||||
originFrame: originFrame,
|
||||
gesScale: gesScale,
|
||||
gesRotation: gesRotation,
|
||||
totalTranslationPoint: totalTranslationPoint
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLTextStickerView deinit")
|
||||
}
|
||||
|
||||
convenience init(state: ZLTextStickerState) {
|
||||
self.init(
|
||||
text: state.text,
|
||||
textColor: state.textColor,
|
||||
style: state.style,
|
||||
image: state.image,
|
||||
originScale: state.originScale,
|
||||
originAngle: state.originAngle,
|
||||
originFrame: state.originFrame,
|
||||
gesScale: state.gesScale,
|
||||
gesRotation: state.gesRotation,
|
||||
totalTranslationPoint: state.totalTranslationPoint,
|
||||
showBorder: false
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
style: ZLInputTextStyle,
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat = 1,
|
||||
gesRotation: CGFloat = 0,
|
||||
totalTranslationPoint: CGPoint = .zero,
|
||||
showBorder: Bool = true
|
||||
) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.style = style
|
||||
self.image = image
|
||||
super.init(originScale: originScale, originAngle: originAngle, originFrame: originFrame, gesScale: gesScale, gesRotation: gesRotation, totalTranslationPoint: totalTranslationPoint, showBorder: showBorder)
|
||||
|
||||
borderView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupUIFrameWhenFirstLayout() {
|
||||
imageView.frame = borderView.bounds.insetBy(dx: Self.edgeInset, dy: Self.edgeInset)
|
||||
}
|
||||
|
||||
override func tapAction(_ ges: UITapGestureRecognizer) {
|
||||
guard gesIsEnabled else { return }
|
||||
|
||||
if let timer = timer, timer.isValid {
|
||||
delegate?.sticker(self, editText: text)
|
||||
} else {
|
||||
super.tapAction(ges)
|
||||
}
|
||||
}
|
||||
|
||||
func changeSize(to newSize: CGSize) {
|
||||
// Revert zoom scale.
|
||||
transform = transform.scaledBy(x: 1 / originScale, y: 1 / originScale)
|
||||
// Revert ges scale.
|
||||
transform = transform.scaledBy(x: 1 / gesScale, y: 1 / gesScale)
|
||||
// Revert ges rotation.
|
||||
transform = transform.rotated(by: -gesRotation)
|
||||
transform = transform.rotated(by: -originAngle.zl.toPi)
|
||||
|
||||
// Recalculate current frame.
|
||||
let center = CGPoint(x: self.frame.midX, y: self.frame.midY)
|
||||
var frame = self.frame
|
||||
frame.origin.x = center.x - newSize.width / 2
|
||||
frame.origin.y = center.y - newSize.height / 2
|
||||
frame.size = newSize
|
||||
self.frame = frame
|
||||
|
||||
let oc = CGPoint(x: originFrame.midX, y: originFrame.midY)
|
||||
var of = originFrame
|
||||
of.origin.x = oc.x - newSize.width / 2
|
||||
of.origin.y = oc.y - newSize.height / 2
|
||||
of.size = newSize
|
||||
originFrame = of
|
||||
|
||||
imageView.frame = borderView.bounds.insetBy(dx: Self.edgeInset, dy: Self.edgeInset)
|
||||
|
||||
// Readd zoom scale.
|
||||
transform = transform.scaledBy(x: originScale, y: originScale)
|
||||
// Readd ges scale.
|
||||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||||
// Readd ges rotation.
|
||||
transform = transform.rotated(by: gesRotation)
|
||||
transform = transform.rotated(by: originAngle.zl.toPi)
|
||||
}
|
||||
|
||||
class func calculateSize(image: UIImage) -> CGSize {
|
||||
var size = image.size
|
||||
size.width += Self.edgeInset * 2
|
||||
size.height += Self.edgeInset * 2
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public class ZLTextStickerState: NSObject {
|
||||
let text: String
|
||||
let textColor: UIColor
|
||||
let style: ZLInputTextStyle
|
||||
let image: UIImage
|
||||
let originScale: CGFloat
|
||||
let originAngle: CGFloat
|
||||
let originFrame: CGRect
|
||||
let gesScale: CGFloat
|
||||
let gesRotation: CGFloat
|
||||
let totalTranslationPoint: CGPoint
|
||||
|
||||
init(
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
style: ZLInputTextStyle,
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat,
|
||||
gesRotation: CGFloat,
|
||||
totalTranslationPoint: CGPoint
|
||||
) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.style = style
|
||||
self.image = image
|
||||
self.originScale = originScale
|
||||
self.originAngle = originAngle
|
||||
self.originFrame = originFrame
|
||||
self.gesScale = gesScale
|
||||
self.gesRotation = gesRotation
|
||||
self.totalTranslationPoint = totalTranslationPoint
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
44
Pods/ZLPhotoBrowser/Sources/Extensions/Array+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Array+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/9.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Photos
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base == [PHAsset] {
|
||||
func removeDuplicate() -> [PHAsset] {
|
||||
return base.enumerated().filter { index, value -> Bool in
|
||||
base.firstIndex(of: value) == index
|
||||
}.map { $0.element }
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base == [ZLResultModel] {
|
||||
func removeDuplicate() -> [ZLResultModel] {
|
||||
return base.enumerated().filter { index, value -> Bool in
|
||||
base.firstIndex(of: value) == index
|
||||
}.map { $0.element }
|
||||
}
|
||||
}
|
||||
33
Pods/ZLPhotoBrowser/Sources/Extensions/Bool+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Bool+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by ruby109 on 2020/11/3.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base == Bool {
|
||||
var intValue: Int {
|
||||
base ? 1 : 0
|
||||
}
|
||||
}
|
||||
106
Pods/ZLPhotoBrowser/Sources/Extensions/Bundle+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// Bundle+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/12.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
private class BundleFinder {}
|
||||
|
||||
extension Bundle {
|
||||
private static var bundle: Bundle?
|
||||
|
||||
static var normalModule: Bundle? = {
|
||||
let bundleName = "ZLPhotoBrowser"
|
||||
|
||||
var candidates = [
|
||||
// Bundle should be present here when the package is linked into an App.
|
||||
Bundle.main.resourceURL,
|
||||
|
||||
// Bundle should be present here when the package is linked into a framework.
|
||||
Bundle(for: ZLPhotoPreviewSheet.self).resourceURL,
|
||||
|
||||
// For command-line tools.
|
||||
Bundle.main.bundleURL,
|
||||
]
|
||||
|
||||
#if SWIFT_PACKAGE
|
||||
// For SWIFT_PACKAGE.
|
||||
candidates.append(Bundle.module.bundleURL)
|
||||
#endif
|
||||
|
||||
for candidate in candidates {
|
||||
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
|
||||
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
static var spmModule: Bundle? = {
|
||||
let bundleName = "ZLPhotoBrowser_ZLPhotoBrowser"
|
||||
|
||||
let candidates = [
|
||||
// Bundle should be present here when the package is linked into an App.
|
||||
Bundle.main.resourceURL,
|
||||
|
||||
// Bundle should be present here when the package is linked into a framework.
|
||||
Bundle(for: BundleFinder.self).resourceURL,
|
||||
|
||||
// For command-line tools.
|
||||
Bundle.main.bundleURL,
|
||||
]
|
||||
|
||||
for candidate in candidates {
|
||||
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
|
||||
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
static var zlPhotoBrowserBundle: Bundle? {
|
||||
return normalModule ?? spmModule
|
||||
}
|
||||
|
||||
class func resetLanguage() {
|
||||
bundle = nil
|
||||
}
|
||||
|
||||
class func zlLocalizedString(_ key: String) -> String {
|
||||
if bundle == nil {
|
||||
guard let path = Bundle.zlPhotoBrowserBundle?.path(forResource: ZLCustomLanguageDeploy.language.key, ofType: "lproj") else {
|
||||
return ""
|
||||
}
|
||||
bundle = Bundle(path: path)
|
||||
}
|
||||
|
||||
let value = bundle?.localizedString(forKey: key, value: nil, table: nil)
|
||||
return Bundle.main.localizedString(forKey: key, value: value, table: nil)
|
||||
}
|
||||
}
|
||||
33
Pods/ZLPhotoBrowser/Sources/Extensions/CGFloat+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// CGFloat+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/11/10.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base == CGFloat {
|
||||
var toPi: CGFloat {
|
||||
return base / 180 * .pi
|
||||
}
|
||||
}
|
||||
47
Pods/ZLPhotoBrowser/Sources/Extensions/Cell+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Cell+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/13.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UICollectionViewCell {
|
||||
static var identifier: String {
|
||||
NSStringFromClass(Base.self)
|
||||
}
|
||||
|
||||
static func register(_ collectionView: UICollectionView) {
|
||||
collectionView.register(Base.self, forCellWithReuseIdentifier: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UITableViewCell {
|
||||
static var identifier: String {
|
||||
NSStringFromClass(Base.self)
|
||||
}
|
||||
|
||||
static func register(_ tableView: UITableView) {
|
||||
tableView.register(Base.self, forCellReuseIdentifier: identifier)
|
||||
}
|
||||
}
|
||||
44
Pods/ZLPhotoBrowser/Sources/Extensions/NSError+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// NSError+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/8/3.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension NSError {
|
||||
convenience init(message: String) {
|
||||
let userInfo = [NSLocalizedDescriptionKey: message]
|
||||
self.init(domain: "com.ZLPhotoBrowser.error", code: -1, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSError {
|
||||
static let videoMergeError = NSError(message: "video merge failed")
|
||||
|
||||
static let videoExportTypeError = NSError(message: "The mediaType of asset must be video")
|
||||
|
||||
static let videoExportError = NSError(message: "Video export failed")
|
||||
|
||||
static let assetSaveError = NSError(message: "Asset save failed")
|
||||
}
|
||||
53
Pods/ZLPhotoBrowser/Sources/Extensions/PHAsset+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// PHAsset+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/12/16.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Photos
|
||||
import MobileCoreServices
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: PHAsset {
|
||||
var isInCloud: Bool {
|
||||
guard let resource = resource else {
|
||||
return false
|
||||
}
|
||||
return !(resource.value(forKey: "locallyAvailable") as? Bool ?? true)
|
||||
}
|
||||
|
||||
var isGif: Bool {
|
||||
guard let filename = filename else {
|
||||
return false
|
||||
}
|
||||
|
||||
return filename.hasSuffix("GIF")
|
||||
}
|
||||
|
||||
var filename: String? {
|
||||
base.value(forKey: "filename") as? String
|
||||
}
|
||||
|
||||
var resource: PHAssetResource? {
|
||||
PHAssetResource.assetResources(for: base).first
|
||||
}
|
||||
}
|
||||
43
Pods/ZLPhotoBrowser/Sources/Extensions/String+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// String+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/18.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base == String {
|
||||
func boundingRect(font: UIFont, limitSize: CGSize) -> CGSize {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.lineBreakMode = .byCharWrapping
|
||||
|
||||
let att = [NSAttributedString.Key.font: font, NSAttributedString.Key.paragraphStyle: style]
|
||||
|
||||
let attContent = NSMutableAttributedString(string: base, attributes: att)
|
||||
|
||||
let size = attContent.boundingRect(with: limitSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).size
|
||||
|
||||
return CGSize(width: ceil(size.width), height: ceil(size.height))
|
||||
}
|
||||
}
|
||||
263
Pods/ZLPhotoBrowser/Sources/Extensions/UIColor+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// UIColor+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/18.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UIColor {
|
||||
static var navBarColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().navBarColor
|
||||
}
|
||||
|
||||
static var navBarColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().navBarColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 相册列表界面导航标题颜色
|
||||
static var navTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().navTitleColor
|
||||
}
|
||||
|
||||
/// 预览大图界面导航标题颜色
|
||||
static var navTitleColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().navTitleColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 框架样式为 embedAlbumList 时,title view 背景色
|
||||
static var navEmbedTitleViewBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().navEmbedTitleViewBgColor
|
||||
}
|
||||
|
||||
/// 预览选择模式下 上方透明背景色
|
||||
static var previewBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().sheetTranslucentColor
|
||||
}
|
||||
|
||||
/// 预览选择模式下 拍照/相册/取消 的背景颜色
|
||||
static var previewBtnBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().sheetBtnBgColor
|
||||
}
|
||||
|
||||
/// 预览选择模式下 拍照/相册/取消 的字体颜色
|
||||
static var previewBtnTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().sheetBtnTitleColor
|
||||
}
|
||||
|
||||
/// 预览选择模式下 选择照片大于0时,取消按钮title颜色
|
||||
static var previewBtnHighlightTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().sheetBtnTitleTintColor
|
||||
}
|
||||
|
||||
/// 相册列表界面背景色
|
||||
static var albumListBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().albumListBgColor
|
||||
}
|
||||
|
||||
/// 嵌入式相册列表下方透明区域颜色
|
||||
static var embedAlbumListTranslucentColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().embedAlbumListTranslucentColor
|
||||
}
|
||||
|
||||
/// 相册列表界面 相册title颜色
|
||||
static var albumListTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().albumListTitleColor
|
||||
}
|
||||
|
||||
/// 相册列表界面 数量label颜色
|
||||
static var albumListCountColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().albumListCountColor
|
||||
}
|
||||
|
||||
/// 分割线颜色
|
||||
static var separatorLineColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().separatorColor
|
||||
}
|
||||
|
||||
/// 小图界面背景色
|
||||
static var thumbnailBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().thumbnailBgColor
|
||||
}
|
||||
|
||||
/// 预览大图界面背景色
|
||||
static var previewVCBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().previewVCBgColor
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具条底色
|
||||
static var bottomToolViewBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBgColor
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具条底色
|
||||
static var bottomToolViewBgColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBgColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具栏按钮 可交互 状态标题颜色
|
||||
static var bottomToolViewBtnNormalTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnNormalTitleColor
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具栏 `完成` 按钮 可交互 状态标题颜色
|
||||
static var bottomToolViewDoneBtnNormalTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewDoneBtnNormalTitleColor
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具栏按钮 可交互 状态标题颜色
|
||||
static var bottomToolViewBtnNormalTitleColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnNormalTitleColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具栏 `完成` 按钮 可交互 状态标题颜色
|
||||
static var bottomToolViewDoneBtnNormalTitleColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewDoneBtnNormalTitleColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具栏按钮 不可交互 状态标题颜色
|
||||
static var bottomToolViewBtnDisableTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnDisableTitleColor
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具栏 `完成` 按钮 不可交互 状态标题颜色
|
||||
static var bottomToolViewDoneBtnDisableTitleColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewDoneBtnDisableTitleColor
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具栏按钮 不可交互 状态标题颜色
|
||||
static var bottomToolViewBtnDisableTitleColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnDisableTitleColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具栏 `完成` 按钮 不可交互 状态标题颜色
|
||||
static var bottomToolViewDoneBtnDisableTitleColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewDoneBtnDisableTitleColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具栏按钮 可交互 状态背景颜色
|
||||
static var bottomToolViewBtnNormalBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnNormalBgColor
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具栏按钮 可交互 状态背景颜色
|
||||
static var bottomToolViewBtnNormalBgColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnNormalBgColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// 相册列表界面底部工具栏按钮 不可交互 状态背景颜色
|
||||
static var bottomToolViewBtnDisableBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnDisableBgColor
|
||||
}
|
||||
|
||||
/// 预览大图界面底部工具栏按钮 不可交互 状态背景颜色
|
||||
static var bottomToolViewBtnDisableBgColorOfPreviewVC: UIColor {
|
||||
ZLPhotoUIConfiguration.default().bottomToolViewBtnDisableBgColorOfPreviewVC
|
||||
}
|
||||
|
||||
/// iOS14 limited 权限时候,小图界面下方显示 选择更多图片 标题颜色
|
||||
static var limitedAuthorityTipsColor: UIColor {
|
||||
return ZLPhotoUIConfiguration.default().limitedAuthorityTipsColor
|
||||
}
|
||||
|
||||
/// 自定义相机录制视频时,进度条颜色
|
||||
static var cameraRecodeProgressColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().cameraRecodeProgressColor
|
||||
}
|
||||
|
||||
/// 已选cell遮罩层颜色
|
||||
static var selectedMaskColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().selectedMaskColor
|
||||
}
|
||||
|
||||
/// 已选cell border颜色
|
||||
static var selectedBorderColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().selectedBorderColor
|
||||
}
|
||||
|
||||
/// 不能选择的cell上方遮罩层颜色
|
||||
static var invalidMaskColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().invalidMaskColor
|
||||
}
|
||||
|
||||
/// 选中图片右上角index text color
|
||||
static var indexLabelTextColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().indexLabelTextColor
|
||||
}
|
||||
|
||||
/// 选中图片右上角index background color
|
||||
static var indexLabelBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().indexLabelBgColor
|
||||
}
|
||||
|
||||
/// 拍照cell 背景颜色
|
||||
static var cameraCellBgColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().cameraCellBgColor
|
||||
}
|
||||
|
||||
/// 调整图片slider默认色
|
||||
static var adjustSliderNormalColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().adjustSliderNormalColor
|
||||
}
|
||||
|
||||
/// 调整图片slider高亮色
|
||||
static var adjustSliderTintColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().adjustSliderTintColor
|
||||
}
|
||||
|
||||
/// 图片编辑器中各种工具下方标题普通状态下的颜色
|
||||
static var imageEditorToolTitleNormalColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().imageEditorToolTitleNormalColor
|
||||
}
|
||||
|
||||
/// 图片编辑器中各种工具下方标题高亮状态下的颜色
|
||||
static var imageEditorToolTitleTintColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().imageEditorToolTitleTintColor
|
||||
}
|
||||
|
||||
/// 图片编辑器中各种工具图标高亮状态下的颜色
|
||||
static var imageEditorToolIconTintColor: UIColor? {
|
||||
ZLPhotoUIConfiguration.default().imageEditorToolIconTintColor
|
||||
}
|
||||
|
||||
/// 编辑器中垃圾箱普通状态下的颜色
|
||||
static var trashCanBackgroundNormalColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().trashCanBackgroundNormalColor
|
||||
}
|
||||
|
||||
/// 编辑器中垃圾箱高亮状态下的颜色
|
||||
static var trashCanBackgroundTintColor: UIColor {
|
||||
ZLPhotoUIConfiguration.default().trashCanBackgroundTintColor
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UIColor {
|
||||
/// - Parameters:
|
||||
/// - r: 0~255
|
||||
/// - g: 0~255
|
||||
/// - b: 0~255
|
||||
/// - a: 0~1
|
||||
static func rgba(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ a: CGFloat = 1) -> UIColor {
|
||||
return UIColor(red: r / 255, green: g / 255, blue: b / 255, alpha: a)
|
||||
}
|
||||
}
|
||||
37
Pods/ZLPhotoBrowser/Sources/Extensions/UIFont+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// UIFont+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/7/7.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UIFont {
|
||||
static func font(ofSize fontSize: CGFloat) -> UIFont {
|
||||
guard let name = ZLCustomFontDeploy.fontName else {
|
||||
return UIFont.systemFont(ofSize: fontSize)
|
||||
}
|
||||
|
||||
return UIFont(name: name, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize)
|
||||
}
|
||||
}
|
||||
543
Pods/ZLPhotoBrowser/Sources/Extensions/UIImage+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,543 @@
|
||||
//
|
||||
// UIImage+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/22.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Accelerate
|
||||
import MobileCoreServices
|
||||
|
||||
// MARK: data 转 gif image
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: UIImage {
|
||||
static func animateGifImage(data: Data) -> UIImage? {
|
||||
// Kingfisher
|
||||
let info: [String: Any] = [
|
||||
kCGImageSourceShouldCache as String: true,
|
||||
kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
|
||||
]
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
var frameCount = CGImageSourceGetCount(imageSource)
|
||||
guard frameCount > 1 else {
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
let maxFrameCount = ZLPhotoConfiguration.default().maxFrameCountForGIF
|
||||
|
||||
let ratio = CGFloat(max(frameCount, maxFrameCount)) / CGFloat(maxFrameCount)
|
||||
frameCount = min(frameCount, maxFrameCount)
|
||||
|
||||
var images = [UIImage]()
|
||||
var frameDuration = [Int]()
|
||||
|
||||
for i in 0..<frameCount {
|
||||
let index = Int(floor(CGFloat(i) * ratio))
|
||||
|
||||
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, info as CFDictionary) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get current animated GIF frame duration
|
||||
let currFrameDuration = getFrameDuration(from: imageSource, at: index) * min(ratio, 3)
|
||||
// Second to ms
|
||||
frameDuration.append(Int(currFrameDuration * 1000))
|
||||
|
||||
images.append(UIImage(cgImage: imageRef, scale: 1, orientation: .up))
|
||||
}
|
||||
|
||||
// https://github.com/kiritmodi2702/GIF-Swift
|
||||
let duration: Int = {
|
||||
var sum = 0
|
||||
for val in frameDuration {
|
||||
sum += val
|
||||
}
|
||||
return sum
|
||||
}()
|
||||
|
||||
// 求出每一帧的最大公约数
|
||||
let gcd = gcdForArray(frameDuration)
|
||||
var frames = [UIImage]()
|
||||
|
||||
for i in 0..<frameCount {
|
||||
let frameImage = images[i]
|
||||
// 每张图片的时长除以最大公约数,得出需要展示的张数
|
||||
let count = Int(frameDuration[i] / gcd)
|
||||
|
||||
for _ in 0..<count {
|
||||
frames.append(frameImage)
|
||||
}
|
||||
}
|
||||
|
||||
return .animatedImage(with: frames, duration: TimeInterval(duration) / 1000)
|
||||
}
|
||||
|
||||
/// Calculates frame duration at a specific index for a gif from an `imageSource`.
|
||||
static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval {
|
||||
guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil)
|
||||
as? [String: Any] else { return 0.0 }
|
||||
|
||||
let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any]
|
||||
return getFrameDuration(from: gifInfo)
|
||||
}
|
||||
|
||||
/// Calculates frame duration for a gif frame out of the kCGImagePropertyGIFDictionary dictionary.
|
||||
static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval {
|
||||
let defaultFrameDuration = 0.1
|
||||
guard let gifInfo = gifInfo else { return defaultFrameDuration }
|
||||
|
||||
let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
|
||||
let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
|
||||
let duration = unclampedDelayTime ?? delayTime
|
||||
|
||||
guard let frameDuration = duration else {
|
||||
return defaultFrameDuration
|
||||
}
|
||||
return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
|
||||
}
|
||||
|
||||
private static func gcdForArray(_ array: [Int]) -> Int {
|
||||
if array.isEmpty {
|
||||
return 1
|
||||
}
|
||||
|
||||
var gcd = array[0]
|
||||
|
||||
for val in array {
|
||||
gcd = gcdForPair(val, gcd)
|
||||
}
|
||||
|
||||
return gcd
|
||||
}
|
||||
|
||||
private static func gcdForPair(_ num1: Int?, _ num2: Int?) -> Int {
|
||||
guard var num1 = num1, var num2 = num2 else {
|
||||
return num1 ?? (num2 ?? 0)
|
||||
}
|
||||
|
||||
if num1 < num2 {
|
||||
swap(&num1, &num2)
|
||||
}
|
||||
|
||||
var rest: Int
|
||||
while true {
|
||||
rest = num1 % num2
|
||||
|
||||
if rest == 0 {
|
||||
return num2
|
||||
} else {
|
||||
num1 = num2
|
||||
num2 = rest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: image edit
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: UIImage {
|
||||
/// 修复转向
|
||||
func fixOrientation() -> UIImage {
|
||||
if base.imageOrientation == .up {
|
||||
return base
|
||||
}
|
||||
|
||||
var transform = CGAffineTransform.identity
|
||||
|
||||
switch base.imageOrientation {
|
||||
case .down, .downMirrored:
|
||||
transform = CGAffineTransform(translationX: width, y: height)
|
||||
transform = transform.rotated(by: .pi)
|
||||
case .left, .leftMirrored:
|
||||
transform = CGAffineTransform(translationX: width, y: 0)
|
||||
transform = transform.rotated(by: CGFloat.pi / 2)
|
||||
case .right, .rightMirrored:
|
||||
transform = CGAffineTransform(translationX: 0, y: height)
|
||||
transform = transform.rotated(by: -CGFloat.pi / 2)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch base.imageOrientation {
|
||||
case .upMirrored, .downMirrored:
|
||||
transform = transform.translatedBy(x: width, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
case .leftMirrored, .rightMirrored:
|
||||
transform = transform.translatedBy(x: height, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
guard let cgImage = base.cgImage, let colorSpace = cgImage.colorSpace else {
|
||||
return base
|
||||
}
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: Int(width),
|
||||
height: Int(height),
|
||||
bitsPerComponent: cgImage.bitsPerComponent,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace,
|
||||
bitmapInfo: cgImage.bitmapInfo.rawValue
|
||||
)
|
||||
context?.concatenate(transform)
|
||||
switch base.imageOrientation {
|
||||
case .left, .leftMirrored, .right, .rightMirrored:
|
||||
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: height, height: width))
|
||||
default:
|
||||
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
}
|
||||
|
||||
guard let newCgImage = context?.makeImage() else {
|
||||
return base
|
||||
}
|
||||
return UIImage(cgImage: newCgImage)
|
||||
}
|
||||
|
||||
/// 旋转方向
|
||||
func rotate(orientation: UIImage.Orientation) -> UIImage {
|
||||
guard let imagRef = base.cgImage else {
|
||||
return base
|
||||
}
|
||||
let rect = CGRect(origin: .zero, size: CGSize(width: CGFloat(imagRef.width), height: CGFloat(imagRef.height)))
|
||||
|
||||
var bnds = rect
|
||||
|
||||
var transform = CGAffineTransform.identity
|
||||
|
||||
switch orientation {
|
||||
case .up:
|
||||
return base
|
||||
case .upMirrored:
|
||||
transform = transform.translatedBy(x: rect.width, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
case .down:
|
||||
transform = transform.translatedBy(x: rect.width, y: rect.height)
|
||||
transform = transform.rotated(by: .pi)
|
||||
case .downMirrored:
|
||||
transform = transform.translatedBy(x: 0, y: rect.height)
|
||||
transform = transform.scaledBy(x: 1, y: -1)
|
||||
case .left:
|
||||
bnds = swapRectWidthAndHeight(bnds)
|
||||
transform = transform.translatedBy(x: 0, y: rect.width)
|
||||
transform = transform.rotated(by: CGFloat.pi * 3 / 2)
|
||||
case .leftMirrored:
|
||||
bnds = swapRectWidthAndHeight(bnds)
|
||||
transform = transform.translatedBy(x: rect.height, y: rect.width)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
transform = transform.rotated(by: CGFloat.pi * 3 / 2)
|
||||
case .right:
|
||||
bnds = swapRectWidthAndHeight(bnds)
|
||||
transform = transform.translatedBy(x: rect.height, y: 0)
|
||||
transform = transform.rotated(by: CGFloat.pi / 2)
|
||||
case .rightMirrored:
|
||||
bnds = swapRectWidthAndHeight(bnds)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
transform = transform.rotated(by: CGFloat.pi / 2)
|
||||
@unknown default:
|
||||
return base
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContext(bnds.size)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
switch orientation {
|
||||
case .left, .leftMirrored, .right, .rightMirrored:
|
||||
context?.scaleBy(x: -1, y: 1)
|
||||
context?.translateBy(x: -rect.height, y: 0)
|
||||
default:
|
||||
context?.scaleBy(x: 1, y: -1)
|
||||
context?.translateBy(x: 0, y: -rect.height)
|
||||
}
|
||||
context?.concatenate(transform)
|
||||
context?.draw(imagRef, in: rect)
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return newImage ?? base
|
||||
}
|
||||
|
||||
func swapRectWidthAndHeight(_ rect: CGRect) -> CGRect {
|
||||
var r = rect
|
||||
r.size.width = rect.height
|
||||
r.size.height = rect.width
|
||||
return r
|
||||
}
|
||||
|
||||
func rotate(degress: CGFloat) -> UIImage {
|
||||
guard degress != 0, let cgImage = base.cgImage else {
|
||||
return base
|
||||
}
|
||||
|
||||
let rotatedViewBox = UIView(frame: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
let t = CGAffineTransform(rotationAngle: degress)
|
||||
rotatedViewBox.transform = t
|
||||
let rotatedSize = rotatedViewBox.frame.size
|
||||
|
||||
UIGraphicsBeginImageContext(rotatedSize)
|
||||
|
||||
let bitmap = UIGraphicsGetCurrentContext()
|
||||
bitmap?.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
|
||||
bitmap?.rotate(by: degress)
|
||||
bitmap?.scaleBy(x: 1.0, y: -1.0)
|
||||
|
||||
bitmap?.draw(cgImage, in: CGRect(x: -width / 2, y: -height / 2, width: width, height: height))
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return newImage ?? base
|
||||
}
|
||||
|
||||
/// 加马赛克
|
||||
func mosaicImage() -> UIImage? {
|
||||
guard let cgImage = base.cgImage else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let scale = 8 * width / UIScreen.main.bounds.width
|
||||
let currCiImage = CIImage(cgImage: cgImage)
|
||||
let filter = CIFilter(name: "CIPixellate")
|
||||
filter?.setValue(currCiImage, forKey: kCIInputImageKey)
|
||||
filter?.setValue(scale, forKey: kCIInputScaleKey)
|
||||
guard let outputImage = filter?.outputImage else { return nil }
|
||||
|
||||
let context = CIContext()
|
||||
|
||||
if let cgImage = context.createCGImage(outputImage, from: CGRect(origin: .zero, size: base.size)) {
|
||||
return UIImage(cgImage: cgImage)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func resize(_ size: CGSize, scale: CGFloat? = nil) -> UIImage? {
|
||||
if size.width <= 0 || size.height <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, scale ?? base.scale)
|
||||
base.draw(in: CGRect(origin: .zero, size: size))
|
||||
let temp = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return temp
|
||||
}
|
||||
|
||||
/// Resize image. Processing speed is better than resize(:) method
|
||||
/// - Parameters:
|
||||
/// - size: Dest size of the image
|
||||
/// - scale: The scale factor of the image
|
||||
func resize_vI(_ size: CGSize, scale: CGFloat? = nil) -> UIImage? {
|
||||
guard let cgImage = base.cgImage else { return nil }
|
||||
|
||||
var format = vImage_CGImageFormat(
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 32,
|
||||
colorSpace: nil,
|
||||
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
|
||||
version: 0,
|
||||
decode: nil,
|
||||
renderingIntent: .defaultIntent
|
||||
)
|
||||
|
||||
var sourceBuffer = vImage_Buffer()
|
||||
defer {
|
||||
if #available(iOS 13.0, *) {
|
||||
sourceBuffer.free()
|
||||
} else {
|
||||
sourceBuffer.data.deallocate()
|
||||
}
|
||||
}
|
||||
|
||||
var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &format, nil, cgImage, numericCast(kvImageNoFlags))
|
||||
guard error == kvImageNoError else { return nil }
|
||||
|
||||
let destWidth = Int(size.width)
|
||||
let destHeight = Int(size.height)
|
||||
let bytesPerPixel = cgImage.bitsPerPixel / 8
|
||||
let destBytesPerRow = destWidth * bytesPerPixel
|
||||
|
||||
let destData = UnsafeMutablePointer<UInt8>.allocate(capacity: destHeight * destBytesPerRow)
|
||||
defer {
|
||||
destData.deallocate()
|
||||
}
|
||||
var destBuffer = vImage_Buffer(data: destData, height: vImagePixelCount(destHeight), width: vImagePixelCount(destWidth), rowBytes: destBytesPerRow)
|
||||
|
||||
// scale the image
|
||||
error = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, numericCast(kvImageHighQualityResampling))
|
||||
guard error == kvImageNoError else { return nil }
|
||||
|
||||
// create a CGImage from vImage_Buffer
|
||||
guard let destCGImage = vImageCreateCGImageFromBuffer(&destBuffer, &format, nil, nil, numericCast(kvImageNoFlags), &error)?.takeRetainedValue() else { return nil }
|
||||
guard error == kvImageNoError else { return nil }
|
||||
|
||||
// create a UIImage
|
||||
return UIImage(cgImage: destCGImage, scale: scale ?? base.scale, orientation: base.imageOrientation)
|
||||
}
|
||||
|
||||
func toCIImage() -> CIImage? {
|
||||
var ciImage = base.ciImage
|
||||
if ciImage == nil, let cgImage = base.cgImage {
|
||||
ciImage = CIImage(cgImage: cgImage)
|
||||
}
|
||||
return ciImage
|
||||
}
|
||||
|
||||
func clipImage(angle: CGFloat, editRect: CGRect, isCircle: Bool) -> UIImage? {
|
||||
let a = ((Int(angle) % 360) - 360) % 360
|
||||
var newImage: UIImage = base
|
||||
if a == -90 {
|
||||
newImage = rotate(orientation: .left)
|
||||
} else if a == -180 {
|
||||
newImage = rotate(orientation: .down)
|
||||
} else if a == -270 {
|
||||
newImage = rotate(orientation: .right)
|
||||
}
|
||||
guard editRect.size != newImage.size else {
|
||||
return newImage
|
||||
}
|
||||
let origin = CGPoint(x: -editRect.minX, y: -editRect.minY)
|
||||
UIGraphicsBeginImageContextWithOptions(editRect.size, false, newImage.scale)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
if isCircle {
|
||||
context?.addEllipse(in: CGRect(origin: .zero, size: editRect.size))
|
||||
context?.clip()
|
||||
}
|
||||
newImage.draw(at: origin)
|
||||
let temp = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
guard let cgi = temp?.cgImage else {
|
||||
return temp
|
||||
}
|
||||
let clipImage = UIImage(cgImage: cgi, scale: newImage.scale, orientation: .up)
|
||||
return clipImage
|
||||
}
|
||||
|
||||
func blurImage(level: CGFloat) -> UIImage? {
|
||||
guard let ciImage = toCIImage() else {
|
||||
return nil
|
||||
}
|
||||
let blurFilter = CIFilter(name: "CIGaussianBlur")
|
||||
blurFilter?.setValue(ciImage, forKey: "inputImage")
|
||||
blurFilter?.setValue(level, forKey: "inputRadius")
|
||||
|
||||
guard let outputImage = blurFilter?.outputImage else {
|
||||
return nil
|
||||
}
|
||||
let context = CIContext()
|
||||
guard let cgImage = context.createCGImage(outputImage, from: ciImage.extent) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
func hasAlphaChannel() -> Bool {
|
||||
guard let info = base.cgImage?.alphaInfo else {
|
||||
return false
|
||||
}
|
||||
|
||||
return info == .first || info == .last || info == .premultipliedFirst || info == .premultipliedLast
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: UIImage {
|
||||
/// 调整图片亮度、对比度、饱和度
|
||||
/// - Parameters:
|
||||
/// - brightness: value in [-1, 1]
|
||||
/// - contrast: value in [-1, 1]
|
||||
/// - saturation: value in [-1, 1]
|
||||
func adjust(brightness: Float, contrast: Float, saturation: Float) -> UIImage? {
|
||||
guard let ciImage = toCIImage() else {
|
||||
return base
|
||||
}
|
||||
|
||||
let filter = CIFilter(name: "CIColorControls")
|
||||
filter?.setValue(ciImage, forKey: kCIInputImageKey)
|
||||
filter?.setValue(ZLEditImageConfiguration.AdjustTool.brightness.filterValue(brightness), forKey: ZLEditImageConfiguration.AdjustTool.brightness.key)
|
||||
filter?.setValue(ZLEditImageConfiguration.AdjustTool.contrast.filterValue(contrast), forKey: ZLEditImageConfiguration.AdjustTool.contrast.key)
|
||||
filter?.setValue(ZLEditImageConfiguration.AdjustTool.saturation.filterValue(saturation), forKey: ZLEditImageConfiguration.AdjustTool.saturation.key)
|
||||
let outputCIImage = filter?.outputImage
|
||||
return outputCIImage?.zl.toUIImage()
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: UIImage {
|
||||
static func image(withColor color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage? {
|
||||
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||
UIGraphicsBeginImageContext(rect.size)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
context?.setFillColor(color.cgColor)
|
||||
context?.fill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
|
||||
func fillColor(_ color: UIColor) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(base.size, false, base.scale)
|
||||
let drawRect = CGRect(x: 0, y: 0, width: base.zl.width, height: base.zl.height)
|
||||
color.setFill()
|
||||
UIRectFill(drawRect)
|
||||
base.draw(in: drawRect, blendMode: .destinationIn, alpha: 1)
|
||||
|
||||
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return tintedImage
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: UIImage {
|
||||
var width: CGFloat {
|
||||
base.size.width
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
base.size.height
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UIImage {
|
||||
static func getImage(_ named: String) -> UIImage? {
|
||||
if ZLCustomImageDeploy.imageNames.contains(named), let image = UIImage(named: named) {
|
||||
return image
|
||||
}
|
||||
if let image = ZLCustomImageDeploy.imageForKey[named] {
|
||||
return image
|
||||
}
|
||||
return UIImage(named: named, in: Bundle.zlPhotoBrowserBundle, compatibleWith: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLPhotoBrowserWrapper where Base: CIImage {
|
||||
func toUIImage() -> UIImage? {
|
||||
let context = CIContext()
|
||||
guard let cgImage = context.createCGImage(base, from: base.extent) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
76
Pods/ZLPhotoBrowser/Sources/Extensions/UIView+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// UIView+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/9/27.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UIView {
|
||||
var top: CGFloat {
|
||||
base.frame.minY
|
||||
}
|
||||
|
||||
var bottom: CGFloat {
|
||||
base.frame.maxY
|
||||
}
|
||||
|
||||
var left: CGFloat {
|
||||
base.frame.minX
|
||||
}
|
||||
|
||||
var right: CGFloat {
|
||||
base.frame.maxX
|
||||
}
|
||||
|
||||
var width: CGFloat {
|
||||
base.frame.width
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
base.frame.height
|
||||
}
|
||||
|
||||
var snapshotImage: UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(base.bounds.size, base.isOpaque, UIScreen.main.scale)
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
base.layer.render(in: context)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
|
||||
func setCornerRadius(_ radius: CGFloat) {
|
||||
base.layer.cornerRadius = radius
|
||||
base.layer.masksToBounds = true
|
||||
}
|
||||
|
||||
func addBorder(color: UIColor, width: CGFloat) {
|
||||
base.layer.borderColor = color.cgColor
|
||||
base.layer.borderWidth = width
|
||||
}
|
||||
}
|
||||
|
||||
36
Pods/ZLPhotoBrowser/Sources/Extensions/UIViewController+ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// UIViewController+ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/12/28.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ZLPhotoBrowserWrapper where Base: UIViewController {
|
||||
func showAlertController(_ alertController: UIAlertController) {
|
||||
if deviceIsiPad() {
|
||||
alertController.popoverPresentationController?.sourceView = base.view
|
||||
}
|
||||
base.showDetailViewController(alertController, sender: nil)
|
||||
}
|
||||
}
|
||||
70
Pods/ZLPhotoBrowser/Sources/General/ZLAddPhotoCell.swift
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// ZLAddPhotoCell.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by ruby109 on 2020/11/3.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
class ZLAddPhotoCell: UICollectionViewCell {
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView(image: .zl.getImage("zl_addPhoto"))
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLAddPhotoCell deinit")
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
imageView.frame = CGRect(x: 0, y: 0, width: bounds.width / 3, height: bounds.width / 3)
|
||||
imageView.center = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
}
|
||||
|
||||
func setupUI() {
|
||||
if ZLPhotoUIConfiguration.default().cellCornerRadio > 0 {
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
|
||||
}
|
||||
|
||||
backgroundColor = .zl.cameraCellBgColor
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
}
|
||||
208
Pods/ZLPhotoBrowser/Sources/General/ZLAlbumListCell.swift
generated
Normal file
@@ -0,0 +1,208 @@
|
||||
//
|
||||
// ZLAlbumListCell.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/19.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLAlbumListCell: UITableViewCell {
|
||||
|
||||
private lazy var coverImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
if ZLPhotoUIConfiguration.default().cellCornerRadio > 0 {
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .zl.font(ofSize: 17)
|
||||
label.textColor = .zl.albumListTitleColor
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var countLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .zl.font(ofSize: 16)
|
||||
label.textColor = .zl.albumListCountColor
|
||||
return label
|
||||
}()
|
||||
|
||||
private var imageIdentifier: String?
|
||||
|
||||
private var model: ZLAlbumListModel!
|
||||
|
||||
private var style: ZLPhotoBrowserStyle = .embedAlbumList
|
||||
|
||||
private var indicator: UIImageView = {
|
||||
var image = UIImage.zl.getImage("zl_ablumList_arrow")
|
||||
if isRTL() {
|
||||
image = image?.imageFlippedForRightToLeftLayoutDirection()
|
||||
}
|
||||
|
||||
let view = UIImageView(image: image)
|
||||
view.contentMode = .scaleAspectFit
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var selectBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.isUserInteractionEnabled = false
|
||||
btn.isHidden = true
|
||||
btn.setImage(.zl.getImage("zl_albumSelect"), for: .selected)
|
||||
return btn
|
||||
}()
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let width = contentView.zl.width
|
||||
let height = contentView.zl.height
|
||||
|
||||
let coverImageW = height - 4
|
||||
let maxTitleW = width - coverImageW - 80
|
||||
|
||||
var titleW: CGFloat = 0
|
||||
var countW: CGFloat = 0
|
||||
if let model = model {
|
||||
titleW = min(
|
||||
bounds.width / 3 * 2,
|
||||
model.title.zl.boundingRect(
|
||||
font: .zl.font(ofSize: 17),
|
||||
limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)
|
||||
).width
|
||||
)
|
||||
titleW = min(titleW, maxTitleW)
|
||||
|
||||
countW = ("(" + String(model.count) + ")").zl
|
||||
.boundingRect(
|
||||
font: .zl.font(ofSize: 16),
|
||||
limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)
|
||||
).width
|
||||
}
|
||||
|
||||
if isRTL() {
|
||||
let imageViewX: CGFloat
|
||||
if style == .embedAlbumList {
|
||||
imageViewX = width - coverImageW
|
||||
} else {
|
||||
imageViewX = width - coverImageW - 12
|
||||
}
|
||||
|
||||
coverImageView.frame = CGRect(x: imageViewX, y: 2, width: coverImageW, height: coverImageW)
|
||||
titleLabel.frame = CGRect(
|
||||
x: coverImageView.zl.left - titleW - 10,
|
||||
y: (height - 30) / 2,
|
||||
width: titleW,
|
||||
height: 30
|
||||
)
|
||||
|
||||
countLabel.frame = CGRect(
|
||||
x: titleLabel.zl.left - countW - 10,
|
||||
y: (height - 30) / 2,
|
||||
width: countW,
|
||||
height: 30
|
||||
)
|
||||
selectBtn.frame = CGRect(x: 20, y: (height - 20) / 2, width: 20, height: 20)
|
||||
indicator.frame = CGRect(x: 20, y: (bounds.height - 15) / 2, width: 15, height: 15)
|
||||
return
|
||||
}
|
||||
|
||||
let imageViewX: CGFloat
|
||||
if style == .embedAlbumList {
|
||||
imageViewX = 0
|
||||
} else {
|
||||
imageViewX = 12
|
||||
}
|
||||
|
||||
coverImageView.frame = CGRect(x: imageViewX, y: 2, width: coverImageW, height: coverImageW)
|
||||
titleLabel.frame = CGRect(
|
||||
x: coverImageView.zl.right + 10,
|
||||
y: (bounds.height - 30) / 2,
|
||||
width: titleW,
|
||||
height: 30
|
||||
)
|
||||
countLabel.frame = CGRect(x: titleLabel.zl.right + 10, y: (height - 30) / 2, width: countW, height: 30)
|
||||
selectBtn.frame = CGRect(x: width - 20 - 20, y: (height - 20) / 2, width: 20, height: 20)
|
||||
indicator.frame = CGRect(x: width - 20 - 15, y: (height - 15) / 2, width: 15, height: 15)
|
||||
}
|
||||
|
||||
func setupUI() {
|
||||
backgroundColor = .zl.albumListBgColor
|
||||
selectionStyle = .none
|
||||
accessoryType = .none
|
||||
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(countLabel)
|
||||
contentView.addSubview(selectBtn)
|
||||
contentView.addSubview(indicator)
|
||||
}
|
||||
|
||||
func configureCell(model: ZLAlbumListModel, style: ZLPhotoBrowserStyle) {
|
||||
self.model = model
|
||||
self.style = style
|
||||
|
||||
titleLabel.text = self.model.title
|
||||
countLabel.text = "(" + String(self.model.count) + ")"
|
||||
|
||||
if style == .embedAlbumList {
|
||||
selectBtn.isHidden = false
|
||||
indicator.isHidden = true
|
||||
} else {
|
||||
indicator.isHidden = false
|
||||
selectBtn.isHidden = true
|
||||
}
|
||||
|
||||
imageIdentifier = self.model.headImageAsset?.localIdentifier
|
||||
if let asset = self.model.headImageAsset {
|
||||
let w = bounds.height * 2.5
|
||||
ZLPhotoManager.fetchImage(for: asset, size: CGSize(width: w, height: w)) { [weak self] image, _ in
|
||||
if self?.imageIdentifier == self?.model.headImageAsset?.localIdentifier {
|
||||
self?.coverImageView.image = image ?? .zl.getImage("zl_defaultphoto")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
158
Pods/ZLPhotoBrowser/Sources/General/ZLAlbumListController.swift
generated
Normal file
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// ZLAlbumListController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/18.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
class ZLAlbumListController: UIViewController {
|
||||
|
||||
private lazy var navView = ZLExternalAlbumListNavView(title: localLanguageTextValue(.photo))
|
||||
|
||||
private var navBlurView: UIVisualEffectView?
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let view = UITableView(frame: .zero, style: .plain)
|
||||
view.backgroundColor = .zl.albumListBgColor
|
||||
view.tableFooterView = UIView()
|
||||
view.rowHeight = 65
|
||||
view.separatorInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0)
|
||||
view.separatorColor = .zl.separatorLineColor
|
||||
view.delegate = self
|
||||
view.dataSource = self
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
view.contentInsetAdjustmentBehavior = .always
|
||||
}
|
||||
|
||||
ZLAlbumListCell.zl.register(view)
|
||||
return view
|
||||
}()
|
||||
|
||||
private var arrDataSource: [ZLAlbumListModel] = []
|
||||
|
||||
private var shouldReloadAlbumList = true
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return ZLPhotoUIConfiguration.default().statusBarStyle
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLAlbumListController deinit")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupUI()
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.navigationBar.isHidden = true
|
||||
|
||||
guard shouldReloadAlbumList else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global().async {
|
||||
ZLPhotoManager.getPhotoAlbumList(ascending: ZLPhotoConfiguration.default().sortAscending, allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage, allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo) { [weak self] albumList in
|
||||
self?.arrDataSource.removeAll()
|
||||
self?.arrDataSource.append(contentsOf: albumList)
|
||||
|
||||
self?.shouldReloadAlbumList = false
|
||||
ZLMainAsync {
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let navViewNormalH: CGFloat = 44
|
||||
|
||||
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
|
||||
var collectionViewInsetTop: CGFloat = 20
|
||||
if #available(iOS 11.0, *) {
|
||||
insets = view.safeAreaInsets
|
||||
collectionViewInsetTop = navViewNormalH
|
||||
} else {
|
||||
collectionViewInsetTop += navViewNormalH
|
||||
}
|
||||
|
||||
navView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: insets.top + navViewNormalH)
|
||||
|
||||
tableView.frame = CGRect(x: insets.left, y: 0, width: view.frame.width - insets.left - insets.right, height: view.frame.height)
|
||||
tableView.contentInset = UIEdgeInsets(top: collectionViewInsetTop, left: 0, bottom: 0, right: 0)
|
||||
tableView.scrollIndicatorInsets = UIEdgeInsets(top: 44, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .zl.albumListBgColor
|
||||
|
||||
view.addSubview(tableView)
|
||||
|
||||
navView.backBtn.isHidden = true
|
||||
navView.cancelBlock = { [weak self] in
|
||||
let nav = self?.navigationController as? ZLImageNavController
|
||||
nav?.cancelBlock?()
|
||||
nav?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
view.addSubview(navView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ZLAlbumListController: UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return arrDataSource.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ZLAlbumListCell.zl.identifier, for: indexPath) as! ZLAlbumListCell
|
||||
|
||||
cell.configureCell(model: arrDataSource[indexPath.row], style: .externalAlbumList)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let vc = ZLThumbnailViewController(albumList: arrDataSource[indexPath.row])
|
||||
show(vc, sender: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ZLAlbumListController: PHPhotoLibraryChangeObserver {
|
||||
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
shouldReloadAlbumList = true
|
||||
}
|
||||
|
||||
}
|
||||
96
Pods/ZLPhotoBrowser/Sources/General/ZLAlbumListModel.swift
generated
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// ZLAlbumListModel.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/11.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
public class ZLAlbumListModel: NSObject {
|
||||
|
||||
public let title: String
|
||||
|
||||
public var count: Int {
|
||||
return result.count
|
||||
}
|
||||
|
||||
public var result: PHFetchResult<PHAsset>
|
||||
|
||||
public let collection: PHAssetCollection
|
||||
|
||||
public let option: PHFetchOptions
|
||||
|
||||
public let isCameraRoll: Bool
|
||||
|
||||
public var headImageAsset: PHAsset? {
|
||||
return result.lastObject
|
||||
}
|
||||
|
||||
public var models: [ZLPhotoModel] = []
|
||||
|
||||
// 暂未用到
|
||||
private var selectedModels: [ZLPhotoModel] = []
|
||||
|
||||
// 暂未用到
|
||||
private var selectedCount: Int = 0
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
result: PHFetchResult<PHAsset>,
|
||||
collection: PHAssetCollection,
|
||||
option: PHFetchOptions,
|
||||
isCameraRoll: Bool
|
||||
) {
|
||||
self.title = title
|
||||
self.result = result
|
||||
self.collection = collection
|
||||
self.option = option
|
||||
self.isCameraRoll = isCameraRoll
|
||||
}
|
||||
|
||||
public func refetchPhotos() {
|
||||
let models = ZLPhotoManager.fetchPhoto(
|
||||
in: result,
|
||||
ascending: ZLPhotoConfiguration.default().sortAscending,
|
||||
allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage,
|
||||
allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo
|
||||
)
|
||||
self.models.removeAll()
|
||||
self.models.append(contentsOf: models)
|
||||
}
|
||||
|
||||
func refreshResult() {
|
||||
result = PHAsset.fetchAssets(in: collection, options: option)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLAlbumListModel {
|
||||
|
||||
static func ==(lhs: ZLAlbumListModel, rhs: ZLAlbumListModel) -> Bool {
|
||||
return lhs.title == rhs.title &&
|
||||
lhs.count == rhs.count &&
|
||||
lhs.headImageAsset?.localIdentifier == rhs.headImageAsset?.localIdentifier
|
||||
}
|
||||
|
||||
}
|
||||
65
Pods/ZLPhotoBrowser/Sources/General/ZLAnimationUtils.swift
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// ZLAnimationUtils.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2023/1/13.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLAnimationUtils: NSObject {
|
||||
enum AnimationType: String {
|
||||
case fade = "opacity"
|
||||
case scale = "transform.scale"
|
||||
case rotate = "transform.rotation"
|
||||
}
|
||||
|
||||
class func animation(
|
||||
type: ZLAnimationUtils.AnimationType,
|
||||
fromValue: CGFloat,
|
||||
toValue: CGFloat,
|
||||
duration: TimeInterval
|
||||
) -> CAAnimation {
|
||||
let animation = CABasicAnimation(keyPath: type.rawValue)
|
||||
animation.fromValue = fromValue
|
||||
animation.toValue = toValue
|
||||
animation.duration = duration
|
||||
animation.fillMode = .forwards
|
||||
animation.isRemovedOnCompletion = false
|
||||
return animation
|
||||
}
|
||||
|
||||
class func springAnimation() -> CAKeyframeAnimation {
|
||||
let animate = CAKeyframeAnimation(keyPath: "transform")
|
||||
animate.duration = ZLPhotoConfiguration.default().selectBtnAnimationDuration
|
||||
animate.isRemovedOnCompletion = true
|
||||
animate.fillMode = .forwards
|
||||
|
||||
animate.values = [
|
||||
CATransform3DMakeScale(0.7, 0.7, 1),
|
||||
CATransform3DMakeScale(1.2, 1.2, 1),
|
||||
CATransform3DMakeScale(0.8, 0.8, 1),
|
||||
CATransform3DMakeScale(1, 1, 1),
|
||||
]
|
||||
return animate
|
||||
}
|
||||
}
|
||||
157
Pods/ZLPhotoBrowser/Sources/General/ZLCameraCell.swift
generated
Normal file
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// ZLCameraCell.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/19.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
class ZLCameraCell: UICollectionViewCell {
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView(image: .zl.getImage("zl_takePhoto"))
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private var session: AVCaptureSession?
|
||||
|
||||
private var videoInput: AVCaptureDeviceInput?
|
||||
|
||||
private var photoOutput: AVCapturePhotoOutput?
|
||||
|
||||
private var previewLayer: AVCaptureVideoPreviewLayer?
|
||||
|
||||
var isEnable: Bool = true {
|
||||
didSet {
|
||||
contentView.alpha = isEnable ? 1 : 0.3
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
session?.stopRunning()
|
||||
session = nil
|
||||
zl_debugPrint("ZLCameraCell deinit")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
imageView.center = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
|
||||
previewLayer?.frame = contentView.layer.bounds
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
backgroundColor = .zl.cameraCellBgColor
|
||||
}
|
||||
|
||||
private func setupSession() {
|
||||
guard session == nil, (session?.isRunning ?? false) == false else {
|
||||
return
|
||||
}
|
||||
session?.stopRunning()
|
||||
if let input = videoInput {
|
||||
session?.removeInput(input)
|
||||
}
|
||||
if let output = photoOutput {
|
||||
session?.removeOutput(output)
|
||||
}
|
||||
session = nil
|
||||
previewLayer?.removeFromSuperlayer()
|
||||
previewLayer = nil
|
||||
|
||||
guard let camera = backCamera() else {
|
||||
return
|
||||
}
|
||||
guard let input = try? AVCaptureDeviceInput(device: camera) else {
|
||||
return
|
||||
}
|
||||
videoInput = input
|
||||
photoOutput = AVCapturePhotoOutput()
|
||||
|
||||
session = AVCaptureSession()
|
||||
|
||||
if session?.canAddInput(input) == true {
|
||||
session?.addInput(input)
|
||||
}
|
||||
if session?.canAddOutput(photoOutput!) == true {
|
||||
session?.addOutput(photoOutput!)
|
||||
}
|
||||
|
||||
previewLayer = AVCaptureVideoPreviewLayer(session: session!)
|
||||
contentView.layer.masksToBounds = true
|
||||
previewLayer?.frame = contentView.layer.bounds
|
||||
previewLayer?.videoGravity = .resizeAspectFill
|
||||
contentView.layer.insertSublayer(previewLayer!, at: 0)
|
||||
|
||||
session?.startRunning()
|
||||
}
|
||||
|
||||
private func backCamera() -> AVCaptureDevice? {
|
||||
let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices
|
||||
for device in devices {
|
||||
if device.position == .back {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func startCapture() {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
|
||||
if !UIImagePickerController.isSourceTypeAvailable(.camera) || status == .denied {
|
||||
return
|
||||
}
|
||||
|
||||
if status == .notDetermined {
|
||||
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||
if granted {
|
||||
ZLMainAsync {
|
||||
self.setupSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setupSession()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
286
Pods/ZLPhotoBrowser/Sources/General/ZLCameraConfiguration.swift
generated
Normal file
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// ZLCameraConfiguration.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/11/10.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
@objcMembers
|
||||
public class ZLCameraConfiguration: NSObject {
|
||||
private var pri_allowTakePhoto = true
|
||||
/// Allow taking photos in the camera (Need allowSelectImage to be true). Defaults to true.
|
||||
public var allowTakePhoto: Bool {
|
||||
get {
|
||||
pri_allowTakePhoto && ZLPhotoConfiguration.default().allowSelectImage
|
||||
}
|
||||
set {
|
||||
pri_allowTakePhoto = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var pri_allowRecordVideo = true
|
||||
/// Allow recording in the camera (Need allowSelectVideo to be true). Defaults to true.
|
||||
public var allowRecordVideo: Bool {
|
||||
get {
|
||||
pri_allowRecordVideo && ZLPhotoConfiguration.default().allowSelectVideo
|
||||
}
|
||||
set {
|
||||
pri_allowRecordVideo = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var pri_minRecordDuration: ZLPhotoConfiguration.Second = 0
|
||||
/// Minimum recording duration. Defaults to 0.
|
||||
public var minRecordDuration: ZLPhotoConfiguration.Second {
|
||||
get {
|
||||
pri_minRecordDuration
|
||||
}
|
||||
set {
|
||||
pri_minRecordDuration = max(0, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private var pri_maxRecordDuration: ZLPhotoConfiguration.Second = 20
|
||||
/// Maximum recording duration. Defaults to 20, minimum is 1.
|
||||
public var maxRecordDuration: ZLPhotoConfiguration.Second {
|
||||
get {
|
||||
pri_maxRecordDuration
|
||||
}
|
||||
set {
|
||||
pri_maxRecordDuration = max(1, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Video resolution. Defaults to hd1920x1080.
|
||||
public var sessionPreset: ZLCameraConfiguration.CaptureSessionPreset = .hd1920x1080
|
||||
|
||||
/// Camera focus mode. Defaults to continuousAutoFocus
|
||||
public var focusMode: ZLCameraConfiguration.FocusMode = .continuousAutoFocus
|
||||
|
||||
/// Camera exposure mode. Defaults to continuousAutoExposure
|
||||
public var exposureMode: ZLCameraConfiguration.ExposureMode = .continuousAutoExposure
|
||||
|
||||
/// Camera flahs switch. Defaults to true.
|
||||
public var showFlashSwitch = true
|
||||
|
||||
/// Whether to support switch camera. Defaults to true.
|
||||
public var allowSwitchCamera = true
|
||||
|
||||
/// Video export format for recording video and editing video. Defaults to mov.
|
||||
public var videoExportType: ZLCameraConfiguration.VideoExportType = .mov
|
||||
|
||||
/// The default camera position after entering the camera. Defaults to back.
|
||||
public var devicePosition: ZLCameraConfiguration.DevicePosition = .back
|
||||
|
||||
private var pri_videoCodecType: Any?
|
||||
/// The codecs for video capture. Defaults to .h264
|
||||
@available(iOS 11.0, *)
|
||||
public var videoCodecType: AVVideoCodecType {
|
||||
get {
|
||||
(pri_videoCodecType as? AVVideoCodecType) ?? .h264
|
||||
}
|
||||
set {
|
||||
pri_videoCodecType = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLCameraConfiguration {
|
||||
@objc enum CaptureSessionPreset: Int {
|
||||
var avSessionPreset: AVCaptureSession.Preset {
|
||||
switch self {
|
||||
case .cif352x288:
|
||||
return .cif352x288
|
||||
case .vga640x480:
|
||||
return .vga640x480
|
||||
case .hd1280x720:
|
||||
return .hd1280x720
|
||||
case .hd1920x1080:
|
||||
return .hd1920x1080
|
||||
case .photo:
|
||||
return .photo
|
||||
}
|
||||
}
|
||||
|
||||
case cif352x288
|
||||
case vga640x480
|
||||
case hd1280x720
|
||||
case hd1920x1080
|
||||
case photo
|
||||
}
|
||||
|
||||
@objc enum FocusMode: Int {
|
||||
var avFocusMode: AVCaptureDevice.FocusMode {
|
||||
switch self {
|
||||
case .autoFocus:
|
||||
return .autoFocus
|
||||
case .continuousAutoFocus:
|
||||
return .continuousAutoFocus
|
||||
}
|
||||
}
|
||||
|
||||
case autoFocus
|
||||
case continuousAutoFocus
|
||||
}
|
||||
|
||||
@objc enum ExposureMode: Int {
|
||||
var avFocusMode: AVCaptureDevice.ExposureMode {
|
||||
switch self {
|
||||
case .autoExpose:
|
||||
return .autoExpose
|
||||
case .continuousAutoExposure:
|
||||
return .continuousAutoExposure
|
||||
}
|
||||
}
|
||||
|
||||
case autoExpose
|
||||
case continuousAutoExposure
|
||||
}
|
||||
|
||||
@objc enum VideoExportType: Int {
|
||||
var format: String {
|
||||
switch self {
|
||||
case .mov:
|
||||
return "mov"
|
||||
case .mp4:
|
||||
return "mp4"
|
||||
}
|
||||
}
|
||||
|
||||
var avFileType: AVFileType {
|
||||
switch self {
|
||||
case .mov:
|
||||
return .mov
|
||||
case .mp4:
|
||||
return .mp4
|
||||
}
|
||||
}
|
||||
|
||||
case mov
|
||||
case mp4
|
||||
}
|
||||
|
||||
@objc enum DevicePosition: Int {
|
||||
case back
|
||||
case front
|
||||
|
||||
/// For custom camera
|
||||
var avDevicePosition: AVCaptureDevice.Position {
|
||||
switch self {
|
||||
case .back:
|
||||
return .back
|
||||
case .front:
|
||||
return .front
|
||||
}
|
||||
}
|
||||
|
||||
/// For system camera
|
||||
var cameraDevice: UIImagePickerController.CameraDevice {
|
||||
switch self {
|
||||
case .back:
|
||||
return .rear
|
||||
case .front:
|
||||
return .front
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: chaining
|
||||
|
||||
public extension ZLCameraConfiguration {
|
||||
@discardableResult
|
||||
func allowTakePhoto(_ value: Bool) -> ZLCameraConfiguration {
|
||||
allowTakePhoto = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowRecordVideo(_ value: Bool) -> ZLCameraConfiguration {
|
||||
allowRecordVideo = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func minRecordDuration(_ duration: ZLPhotoConfiguration.Second) -> ZLCameraConfiguration {
|
||||
minRecordDuration = duration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxRecordDuration(_ duration: ZLPhotoConfiguration.Second) -> ZLCameraConfiguration {
|
||||
maxRecordDuration = duration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sessionPreset(_ sessionPreset: ZLCameraConfiguration.CaptureSessionPreset) -> ZLCameraConfiguration {
|
||||
self.sessionPreset = sessionPreset
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func focusMode(_ mode: ZLCameraConfiguration.FocusMode) -> ZLCameraConfiguration {
|
||||
focusMode = mode
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func exposureMode(_ mode: ZLCameraConfiguration.ExposureMode) -> ZLCameraConfiguration {
|
||||
exposureMode = mode
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showFlashSwitch(_ value: Bool) -> ZLCameraConfiguration {
|
||||
showFlashSwitch = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowSwitchCamera(_ value: Bool) -> ZLCameraConfiguration {
|
||||
allowSwitchCamera = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func videoExportType(_ type: ZLCameraConfiguration.VideoExportType) -> ZLCameraConfiguration {
|
||||
videoExportType = type
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func devicePosition(_ position: ZLCameraConfiguration.DevicePosition) -> ZLCameraConfiguration {
|
||||
devicePosition = position
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
@discardableResult
|
||||
func videoCodecType(_ type: AVVideoCodecType) -> ZLCameraConfiguration {
|
||||
videoCodecType = type
|
||||
return self
|
||||
}
|
||||
}
|
||||
31
Pods/ZLPhotoBrowser/Sources/General/ZLCollectionViewFlowLayout.swift
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// ZLCollectionViewFlowLayout.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2023/4/20.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
override var flipsHorizontallyInOppositeLayoutDirection: Bool { isRTL() }
|
||||
}
|
||||
83
Pods/ZLPhotoBrowser/Sources/General/ZLCustomAlertProtocol.swift
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// ZLCustomAlertProtocol.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/6/29.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public enum ZLCustomAlertStyle {
|
||||
case alert
|
||||
case actionSheet
|
||||
}
|
||||
|
||||
public protocol ZLCustomAlertProtocol: AnyObject {
|
||||
/// Should return an instance of ZLCustomAlertProtocol
|
||||
static func alert(title: String?, message: String, style: ZLCustomAlertStyle) -> ZLCustomAlertProtocol
|
||||
|
||||
func addAction(_ action: ZLCustomAlertAction)
|
||||
|
||||
func show(with parentVC: UIViewController?)
|
||||
}
|
||||
|
||||
public class ZLCustomAlertAction: NSObject {
|
||||
public enum Style {
|
||||
case `default`
|
||||
case tint
|
||||
case cancel
|
||||
case destructive
|
||||
}
|
||||
|
||||
public let title: String
|
||||
|
||||
public let style: ZLCustomAlertAction.Style
|
||||
|
||||
public let handler: ((ZLCustomAlertAction) -> Void)?
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLCustomAlertAction deinit")
|
||||
}
|
||||
|
||||
public init(title: String, style: ZLCustomAlertAction.Style, handler: ((ZLCustomAlertAction) -> Void)?) {
|
||||
self.title = title
|
||||
self.style = style
|
||||
self.handler = handler
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
/// internal
|
||||
extension ZLCustomAlertStyle {
|
||||
var toSystemAlertStyle: UIAlertController.Style {
|
||||
switch self {
|
||||
case .alert:
|
||||
return .alert
|
||||
case .actionSheet:
|
||||
return .actionSheet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// internal
|
||||
extension ZLCustomAlertAction.Style {
|
||||
var toSystemAlertActionStyle: UIAlertAction.Style {
|
||||
switch self {
|
||||
case .default, .tint:
|
||||
return .default
|
||||
case .cancel:
|
||||
return .cancel
|
||||
case .destructive:
|
||||
return .destructive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// internal
|
||||
extension ZLCustomAlertAction {
|
||||
func toSystemAlertAction() -> UIAlertAction {
|
||||
return UIAlertAction(title: title, style: style.toSystemAlertActionStyle) { _ in
|
||||
self.handler?(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
360
Pods/ZLPhotoBrowser/Sources/General/ZLEditImageConfiguration.swift
generated
Normal file
@@ -0,0 +1,360 @@
|
||||
//
|
||||
// ZLEditImageConfiguration.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/12/17.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Provide an image sticker container view that conform to this protocol must be a subclass of UIView
|
||||
/// 必须是UIView的子类遵循这个协议
|
||||
@objc public protocol ZLImageStickerContainerDelegate {
|
||||
@objc var selectImageBlock: ((UIImage) -> Void)? { get set }
|
||||
|
||||
@objc var hideBlock: (() -> Void)? { get set }
|
||||
|
||||
@objc func show(in view: UIView)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
public class ZLEditImageConfiguration: NSObject {
|
||||
private var pri_tools: [ZLEditImageConfiguration.EditTool] = ZLEditImageConfiguration.EditTool.allCases
|
||||
/// Edit image tools. (Default order is draw, clip, imageSticker, textSticker, mosaic, filtter)
|
||||
/// Because Objective-C Array can't contain Enum styles, so this property is invalid in Objective-C.
|
||||
/// - warning: If you want to use the image sticker feature, you must provide a view that implements ZLImageStickerContainerDelegate.
|
||||
public var tools: [ZLEditImageConfiguration.EditTool] {
|
||||
get {
|
||||
if pri_tools.isEmpty {
|
||||
return ZLEditImageConfiguration.EditTool.allCases
|
||||
} else {
|
||||
return pri_tools
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_tools = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit image tools. (This property is only for objc).
|
||||
/// - warning: If you want to use the image sticker feature, you must provide a view that implements ZLImageStickerContainerDelegate.
|
||||
public var tools_objc: [Int] = [] {
|
||||
didSet {
|
||||
tools = tools_objc.compactMap { ZLEditImageConfiguration.EditTool(rawValue: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
private static let defaultDrawColors: [UIColor] = [
|
||||
.white,
|
||||
.black,
|
||||
.zl.rgba(249, 80, 81),
|
||||
.zl.rgba(248, 156, 59),
|
||||
.zl.rgba(255, 195, 0),
|
||||
.zl.rgba(145, 211, 0),
|
||||
.zl.rgba(0, 193, 94),
|
||||
.zl.rgba(16, 173, 254),
|
||||
.zl.rgba(16, 132, 236),
|
||||
.zl.rgba(99, 103, 240),
|
||||
.zl.rgba(127, 127, 127)
|
||||
]
|
||||
|
||||
private var pri_drawColors = ZLEditImageConfiguration.defaultDrawColors
|
||||
/// Draw colors for image editor.
|
||||
public var drawColors: [UIColor] {
|
||||
get {
|
||||
if pri_drawColors.isEmpty {
|
||||
return ZLEditImageConfiguration.defaultDrawColors
|
||||
} else {
|
||||
return pri_drawColors
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_drawColors = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The default draw color. If this color not in editImageDrawColors, will pick the first color in editImageDrawColors as the default.
|
||||
public var defaultDrawColor: UIColor = .zl.rgba(249, 80, 81)
|
||||
|
||||
private var pri_clipRatios: [ZLImageClipRatio] = [.custom]
|
||||
/// Edit ratios for image editor.
|
||||
public var clipRatios: [ZLImageClipRatio] {
|
||||
get {
|
||||
if pri_clipRatios.isEmpty {
|
||||
return [.custom]
|
||||
} else {
|
||||
return pri_clipRatios
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_clipRatios = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private static let defaultTextStickerTextColors: [UIColor] = [
|
||||
.white,
|
||||
.black,
|
||||
.zl.rgba(249, 80, 81),
|
||||
.zl.rgba(248, 156, 59),
|
||||
.zl.rgba(255, 195, 0),
|
||||
.zl.rgba(145, 211, 0),
|
||||
.zl.rgba(0, 193, 94),
|
||||
.zl.rgba(16, 173, 254),
|
||||
.zl.rgba(16, 132, 236),
|
||||
.zl.rgba(99, 103, 240),
|
||||
.zl.rgba(127, 127, 127)
|
||||
]
|
||||
|
||||
private var pri_textStickerTextColors: [UIColor] = ZLEditImageConfiguration.defaultTextStickerTextColors
|
||||
/// Text sticker colors for image editor.
|
||||
public var textStickerTextColors: [UIColor] {
|
||||
get {
|
||||
if pri_textStickerTextColors.isEmpty {
|
||||
return ZLEditImageConfiguration.defaultTextStickerTextColors
|
||||
} else {
|
||||
return pri_textStickerTextColors
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_textStickerTextColors = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The default text sticker color. If this color not in textStickerTextColors, will pick the first color in textStickerTextColors as the default.
|
||||
public var textStickerDefaultTextColor = UIColor.white
|
||||
|
||||
private var pri_filters: [ZLFilter] = ZLFilter.all
|
||||
/// Filters for image editor.
|
||||
public var filters: [ZLFilter] {
|
||||
get {
|
||||
if pri_filters.isEmpty {
|
||||
return ZLFilter.all
|
||||
} else {
|
||||
return pri_filters
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_filters = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var imageStickerContainerView: (UIView & ZLImageStickerContainerDelegate)?
|
||||
|
||||
private var pri_adjustTools: [ZLEditImageConfiguration.AdjustTool] = ZLEditImageConfiguration.AdjustTool.allCases
|
||||
/// Adjust image tools. (Default order is brightness, contrast, saturation)
|
||||
/// Valid when the tools contain EditTool.adjust
|
||||
/// Because Objective-C Array can't contain Enum styles, so this property is invalid in Objective-C.
|
||||
public var adjustTools: [ZLEditImageConfiguration.AdjustTool] {
|
||||
get {
|
||||
if pri_adjustTools.isEmpty {
|
||||
return ZLEditImageConfiguration.AdjustTool.allCases
|
||||
} else {
|
||||
return pri_adjustTools
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_adjustTools = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust image tools. (This property is only for objc).
|
||||
/// Valid when the tools contain EditTool.adjust
|
||||
public var adjustTools_objc: [Int] = [] {
|
||||
didSet {
|
||||
adjustTools = adjustTools_objc.compactMap { ZLEditImageConfiguration.AdjustTool(rawValue: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Give an impact feedback when the adjust slider value is zero. Defaults to true.
|
||||
public var impactFeedbackWhenAdjustSliderValueIsZero = true
|
||||
|
||||
/// Impact feedback style. Defaults to .medium
|
||||
public var impactFeedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle = .medium
|
||||
|
||||
/// Whether to support redo in graffiti and mosaic tools. Defaults to false
|
||||
public var canRedo = false
|
||||
}
|
||||
|
||||
public extension ZLEditImageConfiguration {
|
||||
@objc enum EditTool: Int, CaseIterable {
|
||||
case draw
|
||||
case clip
|
||||
case imageSticker
|
||||
case textSticker
|
||||
case mosaic
|
||||
case filter
|
||||
case adjust
|
||||
}
|
||||
|
||||
@objc enum AdjustTool: Int, CaseIterable {
|
||||
case brightness
|
||||
case contrast
|
||||
case saturation
|
||||
|
||||
var key: String {
|
||||
switch self {
|
||||
case .brightness:
|
||||
return kCIInputBrightnessKey
|
||||
case .contrast:
|
||||
return kCIInputContrastKey
|
||||
case .saturation:
|
||||
return kCIInputSaturationKey
|
||||
}
|
||||
}
|
||||
|
||||
func filterValue(_ value: Float) -> Float {
|
||||
switch self {
|
||||
case .brightness:
|
||||
// 亮度范围-1---1,默认0,这里除以3,取 -0.33---0.33
|
||||
return value / 3
|
||||
case .contrast:
|
||||
// 对比度范围0---4,默认1,这里计算下取0.5---2.5
|
||||
let v: Float
|
||||
if value < 0 {
|
||||
v = 1 + value * (1 / 2)
|
||||
} else {
|
||||
v = 1 + value * (3 / 2)
|
||||
}
|
||||
return v
|
||||
case .saturation:
|
||||
// 饱和度范围0---2,默认1
|
||||
return value + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: chaining
|
||||
|
||||
public extension ZLEditImageConfiguration {
|
||||
@discardableResult
|
||||
func tools(_ tools: [ZLEditImageConfiguration.EditTool]) -> ZLEditImageConfiguration {
|
||||
self.tools = tools
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func drawColors(_ colors: [UIColor]) -> ZLEditImageConfiguration {
|
||||
drawColors = colors
|
||||
return self
|
||||
}
|
||||
|
||||
func defaultDrawColor(_ color: UIColor) -> ZLEditImageConfiguration {
|
||||
defaultDrawColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func clipRatios(_ ratios: [ZLImageClipRatio]) -> ZLEditImageConfiguration {
|
||||
clipRatios = ratios
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func textStickerTextColors(_ colors: [UIColor]) -> ZLEditImageConfiguration {
|
||||
textStickerTextColors = colors
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func textStickerDefaultTextColor(_ color: UIColor) -> ZLEditImageConfiguration {
|
||||
textStickerDefaultTextColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func filters(_ filters: [ZLFilter]) -> ZLEditImageConfiguration {
|
||||
self.filters = filters
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func imageStickerContainerView(_ view: (UIView & ZLImageStickerContainerDelegate)?) -> ZLEditImageConfiguration {
|
||||
imageStickerContainerView = view
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func adjustTools(_ tools: [ZLEditImageConfiguration.AdjustTool]) -> ZLEditImageConfiguration {
|
||||
adjustTools = tools
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func impactFeedbackWhenAdjustSliderValueIsZero(_ value: Bool) -> ZLEditImageConfiguration {
|
||||
impactFeedbackWhenAdjustSliderValueIsZero = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func impactFeedbackStyle(_ style: UIImpactFeedbackGenerator.FeedbackStyle) -> ZLEditImageConfiguration {
|
||||
impactFeedbackStyle = style
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func canRedo(_ value: Bool) -> ZLEditImageConfiguration {
|
||||
canRedo = value
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: 裁剪比例
|
||||
|
||||
public class ZLImageClipRatio: NSObject {
|
||||
@objc public var title: String
|
||||
|
||||
@objc public let whRatio: CGFloat
|
||||
|
||||
@objc public let isCircle: Bool
|
||||
|
||||
@objc public init(title: String, whRatio: CGFloat, isCircle: Bool = false) {
|
||||
self.title = title
|
||||
self.whRatio = isCircle ? 1 : whRatio
|
||||
self.isCircle = isCircle
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLImageClipRatio {
|
||||
static func == (lhs: ZLImageClipRatio, rhs: ZLImageClipRatio) -> Bool {
|
||||
return lhs.whRatio == rhs.whRatio && lhs.title == rhs.title
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLImageClipRatio {
|
||||
@objc static let custom = ZLImageClipRatio(title: "custom", whRatio: 0)
|
||||
|
||||
@objc static let circle = ZLImageClipRatio(title: "circle", whRatio: 1, isCircle: true)
|
||||
|
||||
@objc static let wh1x1 = ZLImageClipRatio(title: "1 : 1", whRatio: 1)
|
||||
|
||||
@objc static let wh3x4 = ZLImageClipRatio(title: "3 : 4", whRatio: 3.0 / 4.0)
|
||||
|
||||
@objc static let wh4x3 = ZLImageClipRatio(title: "4 : 3", whRatio: 4.0 / 3.0)
|
||||
|
||||
@objc static let wh2x3 = ZLImageClipRatio(title: "2 : 3", whRatio: 2.0 / 3.0)
|
||||
|
||||
@objc static let wh3x2 = ZLImageClipRatio(title: "3 : 2", whRatio: 3.0 / 2.0)
|
||||
|
||||
@objc static let wh9x16 = ZLImageClipRatio(title: "9 : 16", whRatio: 9.0 / 16.0)
|
||||
|
||||
@objc static let wh16x9 = ZLImageClipRatio(title: "16 : 9", whRatio: 16.0 / 9.0)
|
||||
}
|
||||
232
Pods/ZLPhotoBrowser/Sources/General/ZLEmbedAlbumListView.swift
generated
Normal file
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// ZLEmbedAlbumListView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/9/7.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
class ZLEmbedAlbumListView: UIView {
|
||||
static let rowH: CGFloat = 60
|
||||
|
||||
private var selectedAlbum: ZLAlbumListModel
|
||||
|
||||
private lazy var tableBgView = UIView()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let view = UITableView(frame: .zero, style: .plain)
|
||||
view.backgroundColor = .zl.albumListBgColor
|
||||
view.tableFooterView = UIView()
|
||||
view.rowHeight = ZLEmbedAlbumListView.rowH
|
||||
view.separatorInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0)
|
||||
view.separatorColor = .zl.separatorLineColor
|
||||
view.delegate = self
|
||||
view.dataSource = self
|
||||
ZLAlbumListCell.zl.register(view)
|
||||
return view
|
||||
}()
|
||||
|
||||
private var arrDataSource: [ZLAlbumListModel] = []
|
||||
|
||||
var selectAlbumBlock: ((ZLAlbumListModel) -> Void)?
|
||||
|
||||
var hideBlock: (() -> Void)?
|
||||
|
||||
private var orientation: UIInterfaceOrientation = UIApplication.shared.statusBarOrientation
|
||||
|
||||
init(selectedAlbum: ZLAlbumListModel) {
|
||||
self.selectedAlbum = selectedAlbum
|
||||
super.init(frame: .zero)
|
||||
setupUI()
|
||||
loadAlbumList()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let currOri = UIApplication.shared.statusBarOrientation
|
||||
|
||||
guard currOri != orientation else {
|
||||
return
|
||||
}
|
||||
orientation = currOri
|
||||
|
||||
guard !isHidden else {
|
||||
return
|
||||
}
|
||||
|
||||
let bgFrame = calculateBgViewBounds()
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: frame.width, height: bgFrame.height), byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 8, height: 8))
|
||||
tableBgView.layer.mask = nil
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.path = path.cgPath
|
||||
tableBgView.layer.mask = maskLayer
|
||||
|
||||
tableBgView.frame = bgFrame
|
||||
tableView.frame = tableBgView.bounds
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
clipsToBounds = true
|
||||
|
||||
backgroundColor = .zl.embedAlbumListTranslucentColor
|
||||
|
||||
addSubview(tableBgView)
|
||||
tableBgView.addSubview(tableView)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
|
||||
tap.delegate = self
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
private func loadAlbumList(completion: (() -> Void)? = nil) {
|
||||
DispatchQueue.global().async {
|
||||
ZLPhotoManager.getPhotoAlbumList(ascending: ZLPhotoConfiguration.default().sortAscending, allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage, allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo) { [weak self] albumList in
|
||||
self?.arrDataSource.removeAll()
|
||||
self?.arrDataSource.append(contentsOf: albumList)
|
||||
|
||||
ZLMainAsync {
|
||||
completion?()
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateBgViewBounds() -> CGRect {
|
||||
let contentH = CGFloat(arrDataSource.count) * ZLEmbedAlbumListView.rowH
|
||||
|
||||
let maxH: CGFloat
|
||||
if UIApplication.shared.statusBarOrientation.isPortrait {
|
||||
maxH = min(frame.height * 0.7, contentH)
|
||||
} else {
|
||||
maxH = min(frame.height * 0.8, contentH)
|
||||
}
|
||||
|
||||
return CGRect(x: 0, y: 0, width: frame.width, height: maxH)
|
||||
}
|
||||
|
||||
@objc private func tapAction(_ tap: UITapGestureRecognizer) {
|
||||
hide()
|
||||
hideBlock?()
|
||||
}
|
||||
|
||||
/// 这里不采用监听相册发生变化的方式,是因为每次变化,系统都会回调多次,造成重复获取相册列表
|
||||
func show(reloadAlbumList: Bool) {
|
||||
guard reloadAlbumList else {
|
||||
animateShow()
|
||||
return
|
||||
}
|
||||
|
||||
if #available(iOS 14.0, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited {
|
||||
loadAlbumList { [weak self] in
|
||||
self?.animateShow()
|
||||
}
|
||||
} else {
|
||||
loadAlbumList()
|
||||
animateShow()
|
||||
}
|
||||
}
|
||||
|
||||
func hide() {
|
||||
var toFrame = tableBgView.frame
|
||||
toFrame.origin.y = -toFrame.height
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.alpha = 0
|
||||
self.tableBgView.frame = toFrame
|
||||
}) { _ in
|
||||
self.isHidden = true
|
||||
self.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
private func animateShow() {
|
||||
let toFrame = calculateBgViewBounds()
|
||||
|
||||
isHidden = false
|
||||
alpha = 0
|
||||
var newFrame = toFrame
|
||||
newFrame.origin.y -= newFrame.height
|
||||
|
||||
if newFrame != tableBgView.frame {
|
||||
let path = UIBezierPath(
|
||||
roundedRect: CGRect(x: 0, y: 0, width: newFrame.width, height: newFrame.height),
|
||||
byRoundingCorners: [.bottomLeft, .bottomRight],
|
||||
cornerRadii: CGSize(width: 8, height: 8)
|
||||
)
|
||||
tableBgView.layer.mask = nil
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.path = path.cgPath
|
||||
tableBgView.layer.mask = maskLayer
|
||||
}
|
||||
|
||||
tableBgView.frame = newFrame
|
||||
tableView.frame = tableBgView.bounds
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.alpha = 1
|
||||
self.tableBgView.frame = toFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLEmbedAlbumListView: UIGestureRecognizerDelegate {
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let point = gestureRecognizer.location(in: self)
|
||||
return !tableBgView.frame.contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLEmbedAlbumListView: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return arrDataSource.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ZLAlbumListCell.zl.identifier, for: indexPath) as! ZLAlbumListCell
|
||||
|
||||
let m = arrDataSource[indexPath.row]
|
||||
|
||||
cell.configureCell(model: m, style: .embedAlbumList)
|
||||
|
||||
cell.selectBtn.isSelected = m == selectedAlbum
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let m = arrDataSource[indexPath.row]
|
||||
selectedAlbum = m
|
||||
selectAlbumBlock?(m)
|
||||
hide()
|
||||
if let indexPaths = tableView.indexPathsForVisibleRows {
|
||||
tableView.reloadRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Pods/ZLPhotoBrowser/Sources/General/ZLEnlargeButton.swift
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// ZLEnlargeButton.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/4/24.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
public class ZLEnlargeButton: UIButton {
|
||||
|
||||
/// 扩大点击区域
|
||||
public var enlargeInsets: UIEdgeInsets = .zero
|
||||
|
||||
/// 上下左右均扩大该值的点击范围
|
||||
public var enlargeInset: CGFloat = 0 {
|
||||
didSet {
|
||||
let inset = max(0, enlargeInset)
|
||||
enlargeInsets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
|
||||
}
|
||||
}
|
||||
|
||||
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
guard !isHidden, alpha != 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
let rect = enlargeRect()
|
||||
if rect.equalTo(bounds) {
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
return rect.contains(point) ? true : false
|
||||
}
|
||||
|
||||
private func enlargeRect() -> CGRect {
|
||||
guard enlargeInsets != .zero else {
|
||||
return bounds
|
||||
}
|
||||
|
||||
let rect = CGRect(
|
||||
x: bounds.minX - enlargeInsets.left,
|
||||
y: bounds.minY - enlargeInsets.top,
|
||||
width: bounds.width + enlargeInsets.left + enlargeInsets.right,
|
||||
height: bounds.height + enlargeInsets.top + enlargeInsets.bottom
|
||||
)
|
||||
return rect
|
||||
}
|
||||
|
||||
}
|
||||
181
Pods/ZLPhotoBrowser/Sources/General/ZLFetchImageOperation.swift
generated
Normal file
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// ZLFetchImageOperation.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/18.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
class ZLFetchImageOperation: Operation {
|
||||
private let model: ZLPhotoModel
|
||||
|
||||
private let isOriginal: Bool
|
||||
|
||||
private let progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)?
|
||||
|
||||
private let completion: (UIImage?, PHAsset?) -> Void
|
||||
|
||||
private var pri_isExecuting = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
}
|
||||
|
||||
override var isExecuting: Bool {
|
||||
return pri_isExecuting
|
||||
}
|
||||
|
||||
private var pri_isFinished = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isFinished")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isFinished")
|
||||
}
|
||||
}
|
||||
|
||||
override var isFinished: Bool {
|
||||
return pri_isFinished
|
||||
}
|
||||
|
||||
private var pri_isCancelled = false {
|
||||
willSet {
|
||||
willChangeValue(forKey: "isCancelled")
|
||||
}
|
||||
didSet {
|
||||
didChangeValue(forKey: "isCancelled")
|
||||
}
|
||||
}
|
||||
|
||||
private var requestImageID: PHImageRequestID = PHInvalidImageRequestID
|
||||
|
||||
override var isCancelled: Bool {
|
||||
return pri_isCancelled
|
||||
}
|
||||
|
||||
init(
|
||||
model: ZLPhotoModel,
|
||||
isOriginal: Bool,
|
||||
progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil,
|
||||
completion: @escaping ((UIImage?, PHAsset?) -> Void)
|
||||
) {
|
||||
self.model = model
|
||||
self.isOriginal = isOriginal
|
||||
self.progress = progress
|
||||
self.completion = completion
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func start() {
|
||||
if isCancelled {
|
||||
fetchFinish()
|
||||
return
|
||||
}
|
||||
zl_debugPrint("---- start fetch")
|
||||
pri_isExecuting = true
|
||||
|
||||
// 存在编辑的图片
|
||||
if let editImage = model.editImage {
|
||||
if ZLPhotoConfiguration.default().saveNewImageAfterEdit {
|
||||
ZLPhotoManager.saveImageToAlbum(image: editImage) { [weak self] _, asset in
|
||||
self?.completion(editImage, asset)
|
||||
self?.fetchFinish()
|
||||
}
|
||||
} else {
|
||||
ZLMainAsync {
|
||||
self.completion(editImage, nil)
|
||||
self.fetchFinish()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ZLPhotoConfiguration.default().allowSelectGif, model.type == .gif {
|
||||
requestImageID = ZLPhotoManager.fetchOriginalImageData(for: model.asset) { [weak self] data, _, isDegraded in
|
||||
if !isDegraded {
|
||||
let image = UIImage.zl.animateGifImage(data: data)
|
||||
self?.completion(image, nil)
|
||||
self?.fetchFinish()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isOriginal {
|
||||
requestImageID = ZLPhotoManager.fetchOriginalImage(for: model.asset, progress: progress) { [weak self] image, isDegraded in
|
||||
if !isDegraded {
|
||||
zl_debugPrint("---- 原图加载完成 \(String(describing: self?.isCancelled))")
|
||||
self?.completion(image?.zl.fixOrientation(), nil)
|
||||
self?.fetchFinish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestImageID = ZLPhotoManager.fetchImage(for: model.asset, size: model.previewSize, progress: progress) { [weak self] image, isDegraded in
|
||||
if !isDegraded {
|
||||
zl_debugPrint("---- 加载完成 isCancelled: \(String(describing: self?.isCancelled))")
|
||||
self?.completion(self?.scaleImage(image?.zl.fixOrientation()), nil)
|
||||
self?.fetchFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
zl_debugPrint("---- cancel \(isExecuting) \(requestImageID)")
|
||||
PHImageManager.default().cancelImageRequest(requestImageID)
|
||||
pri_isCancelled = true
|
||||
if isExecuting {
|
||||
fetchFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private func scaleImage(_ image: UIImage?) -> UIImage? {
|
||||
guard let i = image else {
|
||||
return nil
|
||||
}
|
||||
guard let data = i.jpegData(compressionQuality: 1) else {
|
||||
return i
|
||||
}
|
||||
let mUnit: CGFloat = 1024 * 1024
|
||||
|
||||
if data.count < Int(0.2 * mUnit) {
|
||||
return i
|
||||
}
|
||||
let scale: CGFloat = (data.count > Int(mUnit) ? 0.6 : 0.8)
|
||||
|
||||
guard let d = i.jpegData(compressionQuality: scale) else {
|
||||
return i
|
||||
}
|
||||
return UIImage(data: d)
|
||||
}
|
||||
|
||||
private func fetchFinish() {
|
||||
pri_isExecuting = false
|
||||
pri_isFinished = true
|
||||
}
|
||||
}
|
||||
254
Pods/ZLPhotoBrowser/Sources/General/ZLGeneralDefine.swift
generated
Normal file
@@ -0,0 +1,254 @@
|
||||
//
|
||||
// ZLGeneralDefine.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/11.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
let ZLMaxImageWidth: CGFloat = 500
|
||||
|
||||
enum ZLLayout {
|
||||
static let navTitleFont: UIFont = .zl.font(ofSize: 17)
|
||||
|
||||
static let bottomToolViewH: CGFloat = 55
|
||||
|
||||
static let bottomToolBtnH: CGFloat = 34
|
||||
|
||||
static let bottomToolBtnY: CGFloat = 10
|
||||
|
||||
static let bottomToolTitleFont: UIFont = .zl.font(ofSize: 17)
|
||||
|
||||
static let bottomToolBtnCornerRadius: CGFloat = 5
|
||||
}
|
||||
|
||||
func markSelected(source: inout [ZLPhotoModel], selected: inout [ZLPhotoModel]) {
|
||||
guard selected.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
var selIds: [String: Bool] = [:]
|
||||
var selEditImage: [String: UIImage] = [:]
|
||||
var selEditModel: [String: ZLEditImageModel] = [:]
|
||||
var selIdAndIndex: [String: Int] = [:]
|
||||
|
||||
for (index, m) in selected.enumerated() {
|
||||
selIds[m.ident] = true
|
||||
selEditImage[m.ident] = m.editImage
|
||||
selEditModel[m.ident] = m.editImageModel
|
||||
selIdAndIndex[m.ident] = index
|
||||
}
|
||||
|
||||
source.forEach { m in
|
||||
if selIds[m.ident] == true {
|
||||
m.isSelected = true
|
||||
m.editImage = selEditImage[m.ident]
|
||||
m.editImageModel = selEditModel[m.ident]
|
||||
selected[selIdAndIndex[m.ident]!] = m
|
||||
} else {
|
||||
m.isSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAppName() -> String {
|
||||
if let name = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String {
|
||||
return name
|
||||
}
|
||||
if let name = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String {
|
||||
return name
|
||||
}
|
||||
if let name = Bundle.main.infoDictionary?["CFBundleName"] as? String {
|
||||
return name
|
||||
}
|
||||
return "App"
|
||||
}
|
||||
|
||||
func deviceIsiPhone() -> Bool {
|
||||
return UIDevice.current.userInterfaceIdiom == .phone
|
||||
}
|
||||
|
||||
func deviceIsiPad() -> Bool {
|
||||
return UIDevice.current.userInterfaceIdiom == .pad
|
||||
}
|
||||
|
||||
func deviceSafeAreaInsets() -> UIEdgeInsets {
|
||||
var insets: UIEdgeInsets = .zero
|
||||
|
||||
if #available(iOS 11, *) {
|
||||
insets = UIApplication.shared.keyWindow?.safeAreaInsets ?? .zero
|
||||
}
|
||||
|
||||
return insets
|
||||
}
|
||||
|
||||
func deviceIsFringeScreen() -> Bool {
|
||||
return deviceSafeAreaInsets().top > 0
|
||||
}
|
||||
|
||||
func isSmallScreen() -> Bool {
|
||||
return UIScreen.main.bounds.height <= 812
|
||||
}
|
||||
|
||||
func isRTL() -> Bool {
|
||||
return UIView.userInterfaceLayoutDirection(for: UIView.appearance().semanticContentAttribute) == .rightToLeft
|
||||
}
|
||||
|
||||
func showAlertView(_ message: String, _ sender: UIViewController?) {
|
||||
ZLMainAsync {
|
||||
let action = ZLCustomAlertAction(title: localLanguageTextValue(.ok), style: .default, handler: nil)
|
||||
showAlertController(title: nil, message: message, style: .alert, actions: [action], sender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
func showAlertController(title: String?, message: String?, style: ZLCustomAlertStyle, actions: [ZLCustomAlertAction], sender: UIViewController?) {
|
||||
if let alertClass = ZLPhotoUIConfiguration.default().customAlertClass {
|
||||
let alert = alertClass.alert(title: title, message: message ?? "", style: style)
|
||||
actions.forEach { alert.addAction($0) }
|
||||
alert.show(with: sender)
|
||||
return
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: style.toSystemAlertStyle)
|
||||
actions
|
||||
.map { $0.toSystemAlertAction() }
|
||||
.forEach { alert.addAction($0) }
|
||||
if deviceIsiPad() {
|
||||
alert.popoverPresentationController?.sourceView = sender?.view
|
||||
}
|
||||
(sender ?? UIApplication.shared.keyWindow?.rootViewController)?.zl.showAlertController(alert)
|
||||
}
|
||||
|
||||
func canAddModel(_ model: ZLPhotoModel, currentSelectCount: Int, sender: UIViewController?, showAlert: Bool = true) -> Bool {
|
||||
let config = ZLPhotoConfiguration.default()
|
||||
|
||||
guard config.canSelectAsset?(model.asset) ?? true else {
|
||||
return false
|
||||
}
|
||||
|
||||
if currentSelectCount >= config.maxSelectCount {
|
||||
if showAlert {
|
||||
let message = String(format: localLanguageTextValue(.exceededMaxSelectCount), config.maxSelectCount)
|
||||
showAlertView(message, sender)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if currentSelectCount > 0,
|
||||
!config.allowMixSelect,
|
||||
model.type == .video{
|
||||
return false
|
||||
}
|
||||
|
||||
guard model.type == .video else {
|
||||
return true
|
||||
}
|
||||
|
||||
if model.second > config.maxSelectVideoDuration {
|
||||
if showAlert {
|
||||
let message = String(format: localLanguageTextValue(.longerThanMaxVideoDuration), config.maxSelectVideoDuration)
|
||||
showAlertView(message, sender)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if model.second < config.minSelectVideoDuration {
|
||||
if showAlert {
|
||||
let message = String(format: localLanguageTextValue(.shorterThanMinVideoDuration), config.minSelectVideoDuration)
|
||||
showAlertView(message, sender)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
guard (config.minSelectVideoDataSize > 0 || config.maxSelectVideoDataSize != .greatestFiniteMagnitude),
|
||||
let size = model.dataSize else {
|
||||
return true
|
||||
}
|
||||
|
||||
if size > config.maxSelectVideoDataSize {
|
||||
if showAlert {
|
||||
let value = Int(round(config.maxSelectVideoDataSize / 1024))
|
||||
let message = String(format: localLanguageTextValue(.largerThanMaxVideoDataSize), String(value))
|
||||
showAlertView(message, sender)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if size < config.minSelectVideoDataSize {
|
||||
if showAlert {
|
||||
let value = Int(round(config.minSelectVideoDataSize / 1024))
|
||||
let message = String(format: localLanguageTextValue(.smallerThanMinVideoDataSize), String(value))
|
||||
showAlertView(message, sender)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/// Check if the video duration and size meet the requirements
|
||||
func videoIsMeetRequirements(model: ZLPhotoModel) -> Bool {
|
||||
guard model.type == .video else {
|
||||
return true
|
||||
}
|
||||
|
||||
let config = ZLPhotoConfiguration.default()
|
||||
|
||||
guard config.minSelectVideoDuration...config.maxSelectVideoDuration ~= model.second else {
|
||||
return false
|
||||
}
|
||||
|
||||
if (config.minSelectVideoDataSize > 0 || config.maxSelectVideoDataSize != .greatestFiniteMagnitude),
|
||||
let dataSize = model.dataSize,
|
||||
!(config.minSelectVideoDataSize...config.maxSelectVideoDataSize ~= dataSize) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ZLMainAsync(after: TimeInterval = 0, handler: @escaping (() -> Void)) {
|
||||
if after > 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + after) {
|
||||
handler()
|
||||
}
|
||||
} else {
|
||||
if Thread.isMainThread {
|
||||
handler()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func zl_debugPrint(_ message: Any...) {
|
||||
// message.forEach { debugPrint($0) }
|
||||
}
|
||||
|
||||
func zlLoggerInDebug(_ lastMessage: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) {
|
||||
#if DEBUG
|
||||
print("\(file):\(line): \(lastMessage())")
|
||||
#endif
|
||||
}
|
||||
71
Pods/ZLPhotoBrowser/Sources/General/ZLImageNavController.swift
generated
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// ZLImageNavController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/18.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
class ZLImageNavController: UINavigationController {
|
||||
|
||||
var isSelectedOriginal: Bool = false
|
||||
|
||||
var arrSelectedModels: [ZLPhotoModel] = []
|
||||
|
||||
var selectImageBlock: (() -> Void)?
|
||||
|
||||
var cancelBlock: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLImageNavController deinit")
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return ZLPhotoUIConfiguration.default().statusBarStyle
|
||||
}
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
override init(rootViewController: UIViewController) {
|
||||
super.init(rootViewController: rootViewController)
|
||||
navigationBar.barStyle = .black
|
||||
navigationBar.isTranslucent = true
|
||||
modalPresentationStyle = .fullScreen
|
||||
isNavigationBarHidden = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
}
|
||||
582
Pods/ZLPhotoBrowser/Sources/General/ZLImagePreviewController.swift
generated
Normal file
@@ -0,0 +1,582 @@
|
||||
//
|
||||
// ZLImagePreviewController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/22.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
@objc public enum ZLURLType: Int {
|
||||
case image
|
||||
case video
|
||||
}
|
||||
|
||||
public typealias ZLImageLoaderBlock = (_ url: URL, _ imageView: UIImageView, _ progress: @escaping (CGFloat) -> Void, _ complete: @escaping () -> Void) -> Void
|
||||
|
||||
public class ZLImagePreviewController: UIViewController {
|
||||
static let colItemSpacing: CGFloat = 40
|
||||
|
||||
static let selPhotoPreviewH: CGFloat = 100
|
||||
|
||||
private let datas: [Any]
|
||||
|
||||
private var selectStatus: [Bool]
|
||||
|
||||
private let urlType: ((URL) -> ZLURLType)?
|
||||
|
||||
private let urlImageLoader: ZLImageLoaderBlock?
|
||||
|
||||
private let showSelectBtn: Bool
|
||||
|
||||
private let showBottomView: Bool
|
||||
|
||||
private var currentIndex: Int
|
||||
|
||||
private var indexBeforOrientationChanged: Int
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = ZLCollectionViewFlowLayout()
|
||||
layout.scrollDirection = .horizontal
|
||||
|
||||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
view.backgroundColor = .clear
|
||||
view.dataSource = self
|
||||
view.delegate = self
|
||||
view.isPagingEnabled = true
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
|
||||
ZLPhotoPreviewCell.zl.register(view)
|
||||
ZLGifPreviewCell.zl.register(view)
|
||||
ZLLivePhotoPreviewCell.zl.register(view)
|
||||
ZLVideoPreviewCell.zl.register(view)
|
||||
ZLLocalImagePreviewCell.zl.register(view)
|
||||
ZLNetImagePreviewCell.zl.register(view)
|
||||
ZLNetVideoPreviewCell.zl.register(view)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var navView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.navBarColorOfPreviewVC
|
||||
return view
|
||||
}()
|
||||
|
||||
private var navBlurView: UIVisualEffectView?
|
||||
|
||||
private lazy var backBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
var image = UIImage.zl.getImage("zl_navBack")
|
||||
if isRTL() {
|
||||
image = image?.imageFlippedForRightToLeftLayoutDirection()
|
||||
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -10)
|
||||
} else {
|
||||
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0)
|
||||
}
|
||||
btn.setImage(image, for: .normal)
|
||||
btn.addTarget(self, action: #selector(backBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var indexLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .zl.indexLabelTextColor
|
||||
label.font = ZLLayout.navTitleFont
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var selectBtn: ZLEnlargeButton = {
|
||||
let btn = ZLEnlargeButton(type: .custom)
|
||||
btn.setImage(.zl.getImage("zl_btn_circle"), for: .normal)
|
||||
btn.setImage(.zl.getImage("zl_btn_selected"), for: .selected)
|
||||
btn.enlargeInset = 10
|
||||
btn.addTarget(self, action: #selector(selectBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var bottomView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.bottomToolViewBgColorOfPreviewVC
|
||||
return view
|
||||
}()
|
||||
|
||||
private var bottomBlurView: UIVisualEffectView?
|
||||
|
||||
private lazy var doneBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.setTitle(title, for: .normal)
|
||||
btn.setTitleColor(.zl.bottomToolViewDoneBtnNormalTitleColorOfPreviewVC, for: .normal)
|
||||
btn.setTitleColor(.zl.bottomToolViewDoneBtnDisableTitleColorOfPreviewVC, for: .disabled)
|
||||
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
|
||||
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColorOfPreviewVC
|
||||
btn.layer.masksToBounds = true
|
||||
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
|
||||
return btn
|
||||
}()
|
||||
|
||||
private var isFirstAppear = true
|
||||
|
||||
private var hideNavView = false
|
||||
|
||||
private var orientation: UIInterfaceOrientation = .unknown
|
||||
|
||||
@objc public var longPressBlock: ((ZLImagePreviewController?, UIImage?, Int) -> Void)?
|
||||
|
||||
@objc public var doneBlock: (([Any]) -> Void)?
|
||||
|
||||
@objc public var videoHttpHeader: [String: Any]?
|
||||
|
||||
override public var prefersStatusBarHidden: Bool {
|
||||
return !ZLPhotoUIConfiguration.default().showStatusBarInPreviewInterface
|
||||
}
|
||||
|
||||
override public var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return ZLPhotoUIConfiguration.default().statusBarStyle
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLImagePreviewController deinit")
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - datas: Must be one of PHAsset, UIImage and URL, will filter others in init function.
|
||||
/// - showBottomView: If showSelectBtn is true, showBottomView is always true.
|
||||
/// - index: Index for first display.
|
||||
/// - urlType: Tell me the url is image or video.
|
||||
/// - urlImageLoader: Called when cell will display, cell will layout after callback when image load finish. The first block is progress callback, second is load finish callback.
|
||||
@objc public init(
|
||||
datas: [Any],
|
||||
index: Int = 0,
|
||||
showSelectBtn: Bool = true,
|
||||
showBottomView: Bool = true,
|
||||
urlType: ((URL) -> ZLURLType)? = nil,
|
||||
urlImageLoader: ZLImageLoaderBlock? = nil
|
||||
) {
|
||||
let filterDatas = datas.filter { $0 is PHAsset || $0 is UIImage || $0 is URL }
|
||||
self.datas = filterDatas
|
||||
selectStatus = Array(repeating: true, count: filterDatas.count)
|
||||
currentIndex = min(index, filterDatas.count - 1)
|
||||
indexBeforOrientationChanged = currentIndex
|
||||
self.showSelectBtn = showSelectBtn
|
||||
self.showBottomView = showSelectBtn ? true : showBottomView
|
||||
self.urlType = urlType
|
||||
self.urlImageLoader = urlImageLoader
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupUI()
|
||||
resetSubViewStatus()
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.navigationBar.isHidden = true
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
guard isFirstAppear else {
|
||||
return
|
||||
}
|
||||
isFirstAppear = false
|
||||
|
||||
reloadCurrentCell()
|
||||
}
|
||||
|
||||
override public func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
|
||||
if #available(iOS 11.0, *) {
|
||||
insets = view.safeAreaInsets
|
||||
}
|
||||
insets.top = max(20, insets.top)
|
||||
|
||||
collectionView.frame = CGRect(
|
||||
x: -ZLPhotoPreviewController.colItemSpacing / 2,
|
||||
y: 0,
|
||||
width: view.zl.width + ZLPhotoPreviewController.colItemSpacing,
|
||||
height: view.zl.height
|
||||
)
|
||||
|
||||
let navH = insets.top + 44
|
||||
navView.frame = CGRect(x: 0, y: 0, width: view.zl.width, height: navH)
|
||||
navBlurView?.frame = navView.bounds
|
||||
|
||||
indexLabel.frame = CGRect(x: (view.zl.width - 80) / 2, y: insets.top, width: 80, height: 44)
|
||||
|
||||
if isRTL() {
|
||||
backBtn.frame = CGRect(x: view.zl.width - insets.right - 60, y: insets.top, width: 60, height: 44)
|
||||
selectBtn.frame = CGRect(x: insets.left + 15, y: insets.top + (44 - 25) / 2, width: 25, height: 25)
|
||||
} else {
|
||||
backBtn.frame = CGRect(x: insets.left, y: insets.top, width: 60, height: 44)
|
||||
selectBtn.frame = CGRect(x: view.zl.width - 40 - insets.right, y: insets.top + (44 - 25) / 2, width: 25, height: 25)
|
||||
}
|
||||
|
||||
let bottomViewH = ZLLayout.bottomToolViewH
|
||||
|
||||
bottomView.frame = CGRect(x: 0, y: view.zl.height - insets.bottom - bottomViewH, width: view.zl.width, height: bottomViewH + insets.bottom)
|
||||
bottomBlurView?.frame = bottomView.bounds
|
||||
|
||||
resetBottomViewFrame()
|
||||
|
||||
let ori = UIApplication.shared.statusBarOrientation
|
||||
if ori != orientation {
|
||||
orientation = ori
|
||||
collectionView.setContentOffset(
|
||||
CGPoint(
|
||||
x: (view.zl.width + ZLPhotoPreviewController.colItemSpacing) * CGFloat(indexBeforOrientationChanged),
|
||||
y: 0
|
||||
),
|
||||
animated: false
|
||||
)
|
||||
collectionView.performBatchUpdates({
|
||||
self.collectionView.setContentOffset(
|
||||
CGPoint(
|
||||
x: (self.view.frame.width + ZLPhotoPreviewController.colItemSpacing) * CGFloat(self.indexBeforOrientationChanged),
|
||||
y: 0
|
||||
),
|
||||
animated: false
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadCurrentCell() {
|
||||
guard let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let cell = cell as? ZLGifPreviewCell {
|
||||
cell.loadGifWhenCellDisplaying()
|
||||
} else if let cell = cell as? ZLLivePhotoPreviewCell {
|
||||
cell.loadLivePhotoData()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .zl.previewVCBgColor
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
|
||||
view.addSubview(navView)
|
||||
|
||||
if let effect = ZLPhotoUIConfiguration.default().navViewBlurEffectOfPreview {
|
||||
navBlurView = UIVisualEffectView(effect: effect)
|
||||
navView.addSubview(navBlurView!)
|
||||
}
|
||||
|
||||
navView.addSubview(backBtn)
|
||||
navView.addSubview(indexLabel)
|
||||
navView.addSubview(selectBtn)
|
||||
view.addSubview(collectionView)
|
||||
view.addSubview(bottomView)
|
||||
|
||||
if let effect = ZLPhotoUIConfiguration.default().bottomViewBlurEffectOfPreview {
|
||||
bottomBlurView = UIVisualEffectView(effect: effect)
|
||||
bottomView.addSubview(bottomBlurView!)
|
||||
}
|
||||
|
||||
bottomView.addSubview(doneBtn)
|
||||
view.bringSubviewToFront(navView)
|
||||
}
|
||||
|
||||
private func resetSubViewStatus() {
|
||||
indexLabel.text = String(currentIndex + 1) + " / " + String(datas.count)
|
||||
|
||||
if showSelectBtn {
|
||||
selectBtn.isSelected = selectStatus[currentIndex]
|
||||
} else {
|
||||
selectBtn.isHidden = true
|
||||
}
|
||||
|
||||
resetBottomViewFrame()
|
||||
}
|
||||
|
||||
private func resetBottomViewFrame() {
|
||||
guard showBottomView else {
|
||||
bottomView.isHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
let btnY = ZLLayout.bottomToolBtnY
|
||||
|
||||
var doneTitle = localLanguageTextValue(.done)
|
||||
let selCount = selectStatus.filter { $0 }.count
|
||||
if showSelectBtn,
|
||||
ZLPhotoConfiguration.default().showSelectCountOnDoneBtn,
|
||||
selCount > 0 {
|
||||
doneTitle += "(" + String(selCount) + ")"
|
||||
}
|
||||
let doneBtnW = doneTitle.zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)).width + 20
|
||||
doneBtn.frame = CGRect(x: bottomView.bounds.width - doneBtnW - 15, y: btnY, width: doneBtnW, height: ZLLayout.bottomToolBtnH)
|
||||
doneBtn.setTitle(doneTitle, for: .normal)
|
||||
}
|
||||
|
||||
private func dismiss() {
|
||||
if let nav = navigationController {
|
||||
let vc = nav.popViewController(animated: true)
|
||||
if vc == nil {
|
||||
nav.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: btn actions
|
||||
|
||||
@objc private func backBtnClick() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@objc private func selectBtnClick() {
|
||||
var isSelected = selectStatus[currentIndex]
|
||||
selectBtn.layer.removeAllAnimations()
|
||||
if isSelected {
|
||||
isSelected = false
|
||||
} else {
|
||||
if ZLPhotoConfiguration.default().animateSelectBtnWhenSelect {
|
||||
selectBtn.layer.add(ZLAnimationUtils.springAnimation(), forKey: nil)
|
||||
}
|
||||
isSelected = true
|
||||
}
|
||||
|
||||
selectStatus[currentIndex] = isSelected
|
||||
resetSubViewStatus()
|
||||
}
|
||||
|
||||
@objc private func doneBtnClick() {
|
||||
if showSelectBtn {
|
||||
let res = datas.enumerated()
|
||||
.filter { self.selectStatus[$0.offset] }
|
||||
.map { $0.element }
|
||||
|
||||
doneBlock?(res)
|
||||
} else {
|
||||
doneBlock?(datas)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func tapPreviewCell() {
|
||||
hideNavView.toggle()
|
||||
|
||||
let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0))
|
||||
if let cell = cell as? ZLVideoPreviewCell, cell.isPlaying {
|
||||
hideNavView = true
|
||||
}
|
||||
navView.isHidden = hideNavView
|
||||
if showBottomView {
|
||||
bottomView.isHidden = hideNavView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll view delegate
|
||||
public extension ZLImagePreviewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard scrollView == collectionView else {
|
||||
return
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: ZLPhotoPreviewController.previewVCScrollNotification, object: nil)
|
||||
let offset = scrollView.contentOffset
|
||||
var page = Int(round(offset.x / (view.bounds.width + ZLPhotoPreviewController.colItemSpacing)))
|
||||
page = max(0, min(page, datas.count - 1))
|
||||
if page == currentIndex {
|
||||
return
|
||||
}
|
||||
|
||||
currentIndex = page
|
||||
resetSubViewStatus()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
indexBeforOrientationChanged = currentIndex
|
||||
let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0))
|
||||
if let cell = cell as? ZLGifPreviewCell {
|
||||
cell.loadGifWhenCellDisplaying()
|
||||
} else if let cell = cell as? ZLLivePhotoPreviewCell {
|
||||
cell.loadLivePhotoData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLImagePreviewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return ZLImagePreviewController.colItemSpacing
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return ZLImagePreviewController.colItemSpacing
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0, left: ZLImagePreviewController.colItemSpacing / 2, bottom: 0, right: ZLImagePreviewController.colItemSpacing / 2)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
return CGSize(width: view.zl.width, height: view.zl.height)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return datas.count
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let config = ZLPhotoConfiguration.default()
|
||||
let obj = datas[indexPath.row]
|
||||
|
||||
let baseCell: ZLPreviewBaseCell
|
||||
|
||||
if let asset = obj as? PHAsset {
|
||||
let model = ZLPhotoModel(asset: asset)
|
||||
|
||||
if config.allowSelectGif, model.type == .gif {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLGifPreviewCell.zl.identifier, for: indexPath) as! ZLGifPreviewCell
|
||||
|
||||
cell.singleTapBlock = { [weak self] in
|
||||
self?.tapPreviewCell()
|
||||
}
|
||||
|
||||
cell.model = model
|
||||
baseCell = cell
|
||||
} else if config.allowSelectLivePhoto, model.type == .livePhoto {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLLivePhotoPreviewCell.zl.identifier, for: indexPath) as! ZLLivePhotoPreviewCell
|
||||
|
||||
cell.model = model
|
||||
|
||||
baseCell = cell
|
||||
} else if config.allowSelectVideo, model.type == .video {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLVideoPreviewCell.zl.identifier, for: indexPath) as! ZLVideoPreviewCell
|
||||
|
||||
cell.model = model
|
||||
|
||||
baseCell = cell
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLPhotoPreviewCell.zl.identifier, for: indexPath) as! ZLPhotoPreviewCell
|
||||
|
||||
cell.singleTapBlock = { [weak self] in
|
||||
self?.tapPreviewCell()
|
||||
}
|
||||
|
||||
cell.model = model
|
||||
|
||||
baseCell = cell
|
||||
}
|
||||
|
||||
return baseCell
|
||||
} else if let image = obj as? UIImage {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLLocalImagePreviewCell.zl.identifier, for: indexPath) as! ZLLocalImagePreviewCell
|
||||
|
||||
cell.image = image
|
||||
|
||||
baseCell = cell
|
||||
} else if let url = obj as? URL {
|
||||
let type: ZLURLType = urlType?(url) ?? .image
|
||||
if type == .image {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLNetImagePreviewCell.zl.identifier, for: indexPath) as! ZLNetImagePreviewCell
|
||||
cell.image = nil
|
||||
|
||||
urlImageLoader?(url, cell.preview.imageView, { [weak cell] progress in
|
||||
ZLMainAsync {
|
||||
cell?.progress = progress
|
||||
}
|
||||
}, { [weak cell] in
|
||||
ZLMainAsync {
|
||||
cell?.preview.resetSubViewSize()
|
||||
}
|
||||
})
|
||||
|
||||
baseCell = cell
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLNetVideoPreviewCell.zl.identifier, for: indexPath) as! ZLNetVideoPreviewCell
|
||||
|
||||
cell.configureCell(videoUrl: url, httpHeader: videoHttpHeader)
|
||||
|
||||
baseCell = cell
|
||||
}
|
||||
} else {
|
||||
#if DEBUG
|
||||
fatalError("Preview obj must one of PHAsset, UIImage, URL")
|
||||
#else
|
||||
return UICollectionViewCell()
|
||||
#endif
|
||||
}
|
||||
|
||||
baseCell.singleTapBlock = { [weak self] in
|
||||
self?.tapPreviewCell()
|
||||
}
|
||||
|
||||
(baseCell as? ZLLocalImagePreviewCell)?.longPressBlock = { [weak self, weak baseCell] in
|
||||
if let callback = self?.longPressBlock {
|
||||
callback(self, baseCell?.currentImage, indexPath.row)
|
||||
} else {
|
||||
self?.showSaveImageAlert()
|
||||
}
|
||||
}
|
||||
|
||||
return baseCell
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
if let cell = cell as? ZLPreviewBaseCell {
|
||||
cell.resetSubViewStatusWhenCellEndDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private func showSaveImageAlert() {
|
||||
func saveImage() {
|
||||
guard let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) as? ZLLocalImagePreviewCell, let image = cell.currentImage else {
|
||||
return
|
||||
}
|
||||
|
||||
let hud = ZLProgressHUD.show()
|
||||
ZLPhotoManager.saveImageToAlbum(image: image) { [weak self] suc, _ in
|
||||
hud.hide()
|
||||
if !suc {
|
||||
showAlertView(localLanguageTextValue(.saveImageError), self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let saveAction = ZLCustomAlertAction(title: localLanguageTextValue(.save), style: .default) { _ in
|
||||
saveImage()
|
||||
}
|
||||
let cancelAction = ZLCustomAlertAction(title: localLanguageTextValue(.cancel), style: .cancel, handler: nil)
|
||||
showAlertController(title: nil, message: "", style: .actionSheet, actions: [saveAction, cancelAction], sender: self)
|
||||
}
|
||||
}
|
||||
322
Pods/ZLPhotoBrowser/Sources/General/ZLLanguageDefine.swift
generated
Normal file
@@ -0,0 +1,322 @@
|
||||
//
|
||||
// ZLLanguageDefine.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/17.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc public enum ZLLanguageType: Int {
|
||||
case system
|
||||
case chineseSimplified
|
||||
case chineseTraditional
|
||||
case english
|
||||
case japanese
|
||||
case french
|
||||
case german
|
||||
case russian
|
||||
case vietnamese
|
||||
case korean
|
||||
case malay
|
||||
case italian
|
||||
case indonesian
|
||||
case portuguese
|
||||
case spanish
|
||||
case turkish
|
||||
case arabic
|
||||
|
||||
var key: String {
|
||||
var key = "en"
|
||||
|
||||
switch self {
|
||||
case .system:
|
||||
key = Locale.preferredLanguages.first ?? "en"
|
||||
|
||||
if key.hasPrefix("zh") {
|
||||
if key.range(of: "Hans") != nil {
|
||||
key = "zh-Hans"
|
||||
} else {
|
||||
key = "zh-Hant"
|
||||
}
|
||||
} else if key.hasPrefix("ja") {
|
||||
key = "ja-US"
|
||||
} else if key.hasPrefix("fr") {
|
||||
key = "fr"
|
||||
} else if key.hasPrefix("de") {
|
||||
key = "de"
|
||||
} else if key.hasPrefix("ru") {
|
||||
key = "ru"
|
||||
} else if key.hasPrefix("vi") {
|
||||
key = "vi"
|
||||
} else if key.hasPrefix("ko") {
|
||||
key = "ko"
|
||||
} else if key.hasPrefix("ms") {
|
||||
key = "ms"
|
||||
} else if key.hasPrefix("it") {
|
||||
key = "it"
|
||||
} else if key.hasPrefix("id") {
|
||||
key = "id"
|
||||
} else if key.hasPrefix("pt") {
|
||||
key = "pt-BR"
|
||||
} else if key.hasPrefix("es") {
|
||||
key = "es-419"
|
||||
} else if key.hasPrefix("tr") {
|
||||
key = "tr"
|
||||
} else if key.hasPrefix("ar") {
|
||||
key = "ar"
|
||||
} else {
|
||||
key = "en"
|
||||
}
|
||||
case .chineseSimplified:
|
||||
key = "zh-Hans"
|
||||
case .chineseTraditional:
|
||||
key = "zh-Hant"
|
||||
case .english:
|
||||
key = "en"
|
||||
case .japanese:
|
||||
key = "ja-US"
|
||||
case .french:
|
||||
key = "fr"
|
||||
case .german:
|
||||
key = "de"
|
||||
case .russian:
|
||||
key = "ru"
|
||||
case .vietnamese:
|
||||
key = "vi"
|
||||
case .korean:
|
||||
key = "ko"
|
||||
case .malay:
|
||||
key = "ms"
|
||||
case .italian:
|
||||
key = "it"
|
||||
case .indonesian:
|
||||
key = "id"
|
||||
case .portuguese:
|
||||
key = "pt-BR"
|
||||
case .spanish:
|
||||
key = "es-419"
|
||||
case .turkish:
|
||||
key = "tr"
|
||||
case .arabic:
|
||||
key = "ar"
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
public struct ZLLocalLanguageKey: Hashable {
|
||||
public let rawValue: String
|
||||
|
||||
public init(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// Camera (拍照)
|
||||
public static let previewCamera = ZLLocalLanguageKey(rawValue: "previewCamera")
|
||||
|
||||
/// Record (拍摄)
|
||||
public static let previewCameraRecord = ZLLocalLanguageKey(rawValue: "previewCameraRecord")
|
||||
|
||||
/// Album (相册)
|
||||
public static let previewAlbum = ZLLocalLanguageKey(rawValue: "previewAlbum")
|
||||
|
||||
/// Cancel (取消)
|
||||
public static let cancel = ZLLocalLanguageKey(rawValue: "cancel")
|
||||
|
||||
/// No Photo (无照片)
|
||||
public static let noPhotoTips = ZLLocalLanguageKey(rawValue: "noPhotoTips")
|
||||
|
||||
/// waiting... (正在处理...)
|
||||
public static let hudLoading = ZLLocalLanguageKey(rawValue: "hudLoading")
|
||||
|
||||
/// Done (确定)
|
||||
public static let done = ZLLocalLanguageKey(rawValue: "done")
|
||||
|
||||
/// OK (确定)
|
||||
public static let ok = ZLLocalLanguageKey(rawValue: "ok")
|
||||
|
||||
/// Request timed out (请求超时)
|
||||
public static let timeout = ZLLocalLanguageKey(rawValue: "timeout")
|
||||
|
||||
/// Please Allow %@ to access your album in \"Settings\"->\"Privacy\"->\"Photos\"
|
||||
/// (请在iPhone的\"设置-隐私-照片\"选项中,允许%@访问你的照片)
|
||||
public static let noPhotoLibratyAuthority = ZLLocalLanguageKey(rawValue: "noPhotoLibratyAuthority")
|
||||
|
||||
/// Please allow %@ to access your device's camera in \"Settings\"->\"Privacy\"->\"Camera\"
|
||||
/// (请在iPhone的\"设置-隐私-相机\"选项中,允许%@访问你的相机)
|
||||
public static let noCameraAuthority = ZLLocalLanguageKey(rawValue: "noCameraAuthority")
|
||||
|
||||
/// Unable to record audio. Go to \"Settings\" > \"%@\" and enable microphone access.
|
||||
/// (无法录制声音,前往\"设置 > %@\"中打开麦克风权限)
|
||||
public static let noMicrophoneAuthority = ZLLocalLanguageKey(rawValue: "noMicrophoneAuthority")
|
||||
|
||||
/// Camera is unavailable (相机不可用)
|
||||
public static let cameraUnavailable = ZLLocalLanguageKey(rawValue: "cameraUnavailable")
|
||||
|
||||
/// Keep Recording (继续拍摄)
|
||||
public static let keepRecording = ZLLocalLanguageKey(rawValue: "keepRecording")
|
||||
|
||||
/// Go to Settings (前往设置)
|
||||
public static let gotoSettings = ZLLocalLanguageKey(rawValue: "gotoSettings")
|
||||
|
||||
/// Photos (照片)
|
||||
public static let photo = ZLLocalLanguageKey(rawValue: "photo")
|
||||
|
||||
/// Full Image (原图)
|
||||
public static let originalPhoto = ZLLocalLanguageKey(rawValue: "originalPhoto")
|
||||
|
||||
/// Back (返回)
|
||||
public static let back = ZLLocalLanguageKey(rawValue: "back")
|
||||
|
||||
/// Edit (编辑)
|
||||
public static let edit = ZLLocalLanguageKey(rawValue: "edit")
|
||||
|
||||
/// Done (完成)
|
||||
public static let editFinish = ZLLocalLanguageKey(rawValue: "editFinish")
|
||||
|
||||
/// Undo (还原)
|
||||
public static let revert = ZLLocalLanguageKey(rawValue: "revert")
|
||||
|
||||
/// Brightness (亮度)
|
||||
public static let brightness = ZLLocalLanguageKey(rawValue: "brightness")
|
||||
|
||||
/// Contrast (对比度)
|
||||
public static let contrast = ZLLocalLanguageKey(rawValue: "contrast")
|
||||
|
||||
/// Saturation (饱和度)
|
||||
public static let saturation = ZLLocalLanguageKey(rawValue: "saturation")
|
||||
|
||||
/// Preview (预览)
|
||||
public static let preview = ZLLocalLanguageKey(rawValue: "preview")
|
||||
|
||||
/// Save (保存)
|
||||
public static let save = ZLLocalLanguageKey(rawValue: "save")
|
||||
|
||||
/// Failed to save the image (图片保存失败)
|
||||
public static let saveImageError = ZLLocalLanguageKey(rawValue: "saveImageError")
|
||||
|
||||
/// Failed to save the video (视频保存失败)
|
||||
public static let saveVideoError = ZLLocalLanguageKey(rawValue: "saveVideoError")
|
||||
|
||||
/// Max select count: %ld (最多只能选择%ld张图片)
|
||||
public static let exceededMaxSelectCount = ZLLocalLanguageKey(rawValue: "exceededMaxSelectCount")
|
||||
|
||||
/// Max count for video selection: %ld (最多只能选择%ld个视频)
|
||||
public static let exceededMaxVideoSelectCount = ZLLocalLanguageKey(rawValue: "exceededMaxVideoSelectCount")
|
||||
|
||||
/// Min count for video selection: %ld (最少选择%ld个视频)
|
||||
public static let lessThanMinVideoSelectCount = ZLLocalLanguageKey(rawValue: "lessThanMinVideoSelectCount")
|
||||
|
||||
/// Can't select videos longer than %lds
|
||||
/// (不能选择超过%ld秒的视频)
|
||||
public static let longerThanMaxVideoDuration = ZLLocalLanguageKey(rawValue: "longerThanMaxVideoDuration")
|
||||
|
||||
/// Can't select videos shorter than %lds
|
||||
/// (不能选择低于%ld秒的视频)
|
||||
public static let shorterThanMinVideoDuration = ZLLocalLanguageKey(rawValue: "shorterThanMinVideoDuration")
|
||||
|
||||
/// Can't select videos larger than %@MB
|
||||
/// (不能选择大于%@MB的视频)
|
||||
public static let largerThanMaxVideoDataSize = ZLLocalLanguageKey(rawValue: "largerThanMaxVideoDataSize")
|
||||
|
||||
/// Can't select videos smaller than %@MB
|
||||
/// (不能选择小于%@MB的视频)
|
||||
public static let smallerThanMinVideoDataSize = ZLLocalLanguageKey(rawValue: "smallerThanMinVideoDataSize")
|
||||
|
||||
/// Unable to sync from iCloud (iCloud无法同步)
|
||||
public static let iCloudVideoLoadFaild = ZLLocalLanguageKey(rawValue: "iCloudVideoLoadFaild")
|
||||
|
||||
/// loading failed (图片加载失败)
|
||||
public static let imageLoadFailed = ZLLocalLanguageKey(rawValue: "imageLoadFailed")
|
||||
|
||||
/// Tap to take photo and hold to record video (轻触拍照,按住摄像)
|
||||
public static let customCameraTips = ZLLocalLanguageKey(rawValue: "customCameraTips")
|
||||
|
||||
/// Tap to take photo (轻触拍照)
|
||||
public static let customCameraTakePhotoTips = ZLLocalLanguageKey(rawValue: "customCameraTakePhotoTips")
|
||||
|
||||
/// hold to record video (按住摄像)
|
||||
public static let customCameraRecordVideoTips = ZLLocalLanguageKey(rawValue: "customCameraRecordVideoTips")
|
||||
|
||||
/// Record at least %lds (至少录制%ld秒)
|
||||
public static let minRecordTimeTips = ZLLocalLanguageKey(rawValue: "minRecordTimeTips")
|
||||
|
||||
/// Recents (所有照片)
|
||||
public static let cameraRoll = ZLLocalLanguageKey(rawValue: "cameraRoll")
|
||||
|
||||
/// Panoramas (全景照片)
|
||||
public static let panoramas = ZLLocalLanguageKey(rawValue: "panoramas")
|
||||
|
||||
/// Videos (视频)
|
||||
public static let videos = ZLLocalLanguageKey(rawValue: "videos")
|
||||
|
||||
/// Favorites (个人收藏)
|
||||
public static let favorites = ZLLocalLanguageKey(rawValue: "favorites")
|
||||
|
||||
/// Time-Lapse (延时摄影)
|
||||
public static let timelapses = ZLLocalLanguageKey(rawValue: "timelapses")
|
||||
|
||||
/// Recently Added (最近添加)
|
||||
public static let recentlyAdded = ZLLocalLanguageKey(rawValue: "recentlyAdded")
|
||||
|
||||
/// Bursts (连拍快照)
|
||||
public static let bursts = ZLLocalLanguageKey(rawValue: "bursts")
|
||||
|
||||
/// Slo-mo (慢动作)
|
||||
public static let slomoVideos = ZLLocalLanguageKey(rawValue: "slomoVideos")
|
||||
|
||||
/// Selfies (自拍)
|
||||
public static let selfPortraits = ZLLocalLanguageKey(rawValue: "selfPortraits")
|
||||
|
||||
/// Screenshots (屏幕快照)
|
||||
public static let screenshots = ZLLocalLanguageKey(rawValue: "screenshots")
|
||||
|
||||
/// Portrait (人像)
|
||||
public static let depthEffect = ZLLocalLanguageKey(rawValue: "depthEffect")
|
||||
|
||||
/// Live Photo
|
||||
public static let livePhotos = ZLLocalLanguageKey(rawValue: "livePhotos")
|
||||
|
||||
/// Animated (动图)
|
||||
public static let animated = ZLLocalLanguageKey(rawValue: "animated")
|
||||
|
||||
/// My Photo Stream (我的照片流)
|
||||
public static let myPhotoStream = ZLLocalLanguageKey(rawValue: "myPhotoStream")
|
||||
|
||||
/// All Photos (所有照片)
|
||||
public static let noTitleAlbumListPlaceholder = ZLLocalLanguageKey(rawValue: "noTitleAlbumListPlaceholder")
|
||||
|
||||
/// Unable to access all photos, go to settings (无法访问所有照片,前往设置)
|
||||
public static let unableToAccessAllPhotos = ZLLocalLanguageKey(rawValue: "unableToAccessAllPhotos")
|
||||
|
||||
/// Drag here to remove (拖到此处删除)
|
||||
public static let textStickerRemoveTips = ZLLocalLanguageKey(rawValue: "textStickerRemoveTips")
|
||||
}
|
||||
|
||||
func localLanguageTextValue(_ key: ZLLocalLanguageKey) -> String {
|
||||
if let value = ZLCustomLanguageDeploy.deploy[key] {
|
||||
return value
|
||||
}
|
||||
return Bundle.zlLocalizedString(key.rawValue)
|
||||
}
|
||||
75
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoBrowser.swift
generated
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// ZLPhotoBrowser.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/9/2.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
import Photos
|
||||
|
||||
let version = "4.4.3.2"
|
||||
|
||||
public struct ZLPhotoBrowserWrapper<Base> {
|
||||
public let base: Base
|
||||
|
||||
public init(_ base: Base) {
|
||||
self.base = base
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ZLPhotoBrowserCompatible: AnyObject { }
|
||||
|
||||
public protocol ZLPhotoBrowserCompatibleValue { }
|
||||
|
||||
extension ZLPhotoBrowserCompatible {
|
||||
public var zl: ZLPhotoBrowserWrapper<Self> {
|
||||
get { ZLPhotoBrowserWrapper(self) }
|
||||
set { }
|
||||
}
|
||||
|
||||
public static var zl: ZLPhotoBrowserWrapper<Self>.Type {
|
||||
get { ZLPhotoBrowserWrapper<Self>.self }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLPhotoBrowserCompatibleValue {
|
||||
public var zl: ZLPhotoBrowserWrapper<Self> {
|
||||
get { ZLPhotoBrowserWrapper(self) }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController: ZLPhotoBrowserCompatible { }
|
||||
extension UIColor: ZLPhotoBrowserCompatible { }
|
||||
extension UIImage: ZLPhotoBrowserCompatible { }
|
||||
extension CIImage: ZLPhotoBrowserCompatible { }
|
||||
extension PHAsset: ZLPhotoBrowserCompatible { }
|
||||
extension UIFont: ZLPhotoBrowserCompatible { }
|
||||
extension UIView: ZLPhotoBrowserCompatible { }
|
||||
|
||||
extension Array: ZLPhotoBrowserCompatibleValue { }
|
||||
extension String: ZLPhotoBrowserCompatibleValue { }
|
||||
extension CGFloat: ZLPhotoBrowserCompatibleValue { }
|
||||
extension Bool: ZLPhotoBrowserCompatibleValue { }
|
||||
366
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoConfiguration+Chaining.swift
generated
Normal file
@@ -0,0 +1,366 @@
|
||||
//
|
||||
// ZLPhotoConfiguration+Chaining.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/11/1.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
public extension ZLPhotoConfiguration {
|
||||
@discardableResult
|
||||
func sortAscending(_ ascending: Bool) -> ZLPhotoConfiguration {
|
||||
sortAscending = ascending
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxSelectCount(_ count: Int) -> ZLPhotoConfiguration {
|
||||
maxSelectCount = count
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxVideoSelectCount(_ count: Int) -> ZLPhotoConfiguration {
|
||||
maxVideoSelectCount = count
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func minVideoSelectCount(_ count: Int) -> ZLPhotoConfiguration {
|
||||
minVideoSelectCount = count
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowMixSelect(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowMixSelect = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxPreviewCount(_ count: Int) -> ZLPhotoConfiguration {
|
||||
maxPreviewCount = count
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowSelectImage(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowSelectImage = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@objc func allowSelectVideo(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowSelectVideo = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowSelectGif(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowSelectGif = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowSelectLivePhoto(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowSelectLivePhoto = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowTakePhotoInLibrary(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowTakePhotoInLibrary = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func callbackDirectlyAfterTakingPhoto(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
callbackDirectlyAfterTakingPhoto = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowEditImage(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowEditImage = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowEditVideo(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowEditVideo = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func animateSelectBtnWhenSelect(_ animate: Bool) -> ZLPhotoConfiguration {
|
||||
animateSelectBtnWhenSelect = animate
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectBtnAnimationDuration(_ duration: CFTimeInterval) -> ZLPhotoConfiguration {
|
||||
selectBtnAnimationDuration = duration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func editAfterSelectThumbnailImage(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
editAfterSelectThumbnailImage = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cropVideoAfterSelectThumbnail(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
cropVideoAfterSelectThumbnail = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showClipDirectlyIfOnlyHasClipTool(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showClipDirectlyIfOnlyHasClipTool = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func saveNewImageAfterEdit(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
saveNewImageAfterEdit = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowSlideSelect(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowSlideSelect = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func autoScrollWhenSlideSelectIsActive(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
autoScrollWhenSlideSelectIsActive = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func autoScrollMaxSpeed(_ speed: CGFloat) -> ZLPhotoConfiguration {
|
||||
autoScrollMaxSpeed = speed
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowDragSelect(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowDragSelect = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowSelectOriginal(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowSelectOriginal = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func alwaysRequestOriginal(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
alwaysRequestOriginal = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func allowPreviewPhotos(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
allowPreviewPhotos = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showPreviewButtonInAlbum(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showPreviewButtonInAlbum = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showSelectCountOnDoneBtn(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showSelectCountOnDoneBtn = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxEditVideoTime(_ second: Second) -> ZLPhotoConfiguration {
|
||||
maxEditVideoTime = second
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxSelectVideoDuration(_ duration: Second) -> ZLPhotoConfiguration {
|
||||
maxSelectVideoDuration = duration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func minSelectVideoDuration(_ duration: Second) -> ZLPhotoConfiguration {
|
||||
minSelectVideoDuration = duration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxSelectVideoDataSize(_ size: ZLPhotoConfiguration.KBUnit) -> ZLPhotoConfiguration {
|
||||
maxSelectVideoDataSize = size
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func minSelectVideoDataSize(_ size: ZLPhotoConfiguration.KBUnit) -> ZLPhotoConfiguration {
|
||||
minSelectVideoDataSize = size
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func editImageConfiguration(_ configuration: ZLEditImageConfiguration) -> ZLPhotoConfiguration {
|
||||
editImageConfiguration = configuration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showCaptureImageOnTakePhotoBtn(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showCaptureImageOnTakePhotoBtn = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showSelectBtnWhenSingleSelect(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showSelectBtnWhenSingleSelect = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showSelectedMask(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showSelectedMask = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showSelectedBorder(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showSelectedBorder = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showInvalidMask(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showInvalidMask = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showSelectedIndex(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showSelectedIndex = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showSelectedPhotoPreview(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showSelectedPhotoPreview = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func timeout(_ timeout: TimeInterval) -> ZLPhotoConfiguration {
|
||||
self.timeout = timeout
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func useCustomCamera(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
useCustomCamera = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cameraConfiguration(_ configuration: ZLCameraConfiguration) -> ZLPhotoConfiguration {
|
||||
cameraConfiguration = configuration
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func canSelectAsset(_ block: ((PHAsset) -> Bool)?) -> ZLPhotoConfiguration {
|
||||
canSelectAsset = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func didSelectAsset(_ block: ((PHAsset) -> Void)?) -> ZLPhotoConfiguration {
|
||||
didSelectAsset = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func didDeselectAsset(_ block: ((PHAsset) -> Void)?) -> ZLPhotoConfiguration {
|
||||
didDeselectAsset = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showAddPhotoButton(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showAddPhotoButton = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showEnterSettingTips(_ value: Bool) -> ZLPhotoConfiguration {
|
||||
showEnterSettingTips = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func maxFrameCountForGIF(_ frameCount: Int) -> ZLPhotoConfiguration {
|
||||
maxFrameCountForGIF = frameCount
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func gifPlayBlock(_ block: ((UIImageView, Data, [AnyHashable: Any]?) -> Void)?) -> ZLPhotoConfiguration {
|
||||
gifPlayBlock = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pauseGIFBlock(_ block: ((UIImageView) -> Void)?) -> ZLPhotoConfiguration {
|
||||
pauseGIFBlock = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func resumeGIFBlock(_ block: ((UIImageView) -> Void)?) -> ZLPhotoConfiguration {
|
||||
resumeGIFBlock = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func noAuthorityCallback(_ callback: ((ZLNoAuthorityType) -> Void)?) -> ZLPhotoConfiguration {
|
||||
noAuthorityCallback = callback
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func operateBeforeDoneAction(_ block: ((UIViewController, @escaping () -> Void) -> Void)?) -> ZLPhotoConfiguration {
|
||||
operateBeforeDoneAction = block
|
||||
return self
|
||||
}
|
||||
}
|
||||
286
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoConfiguration.swift
generated
Normal file
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// ZLPhotoConfiguration.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/11.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
@objcMembers
|
||||
public class ZLPhotoConfiguration: NSObject {
|
||||
public typealias Second = Int
|
||||
|
||||
public typealias KBUnit = CGFloat
|
||||
|
||||
private static var single = ZLPhotoConfiguration()
|
||||
|
||||
public class func `default`() -> ZLPhotoConfiguration {
|
||||
ZLPhotoConfiguration.single
|
||||
}
|
||||
|
||||
public class func resetConfiguration() {
|
||||
ZLPhotoConfiguration.single = ZLPhotoConfiguration()
|
||||
}
|
||||
|
||||
/// Photo sorting method, the preview interface is not affected by this parameter. Defaults to true.
|
||||
public var sortAscending = true
|
||||
|
||||
private var pri_maxSelectCount = 9
|
||||
/// Anything superior than 1 will enable the multiple selection feature. Defaults to 9.
|
||||
public var maxSelectCount: Int {
|
||||
get {
|
||||
pri_maxSelectCount
|
||||
}
|
||||
set {
|
||||
pri_maxSelectCount = max(1, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private var pri_maxVideoSelectCount = 0
|
||||
/// A count for video max selection. Defaults to 0.
|
||||
/// - warning: Only valid in mix selection mode. (i.e. allowMixSelect = true)
|
||||
public var maxVideoSelectCount: Int {
|
||||
get {
|
||||
if pri_maxVideoSelectCount <= 0 {
|
||||
return maxSelectCount
|
||||
} else {
|
||||
return max(minVideoSelectCount, min(pri_maxVideoSelectCount, maxSelectCount))
|
||||
}
|
||||
}
|
||||
set {
|
||||
pri_maxVideoSelectCount = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var pri_minVideoSelectCount = 0
|
||||
/// A count for video min selection. Defaults to 0.
|
||||
/// - warning: Only valid in mix selection mode. (i.e. allowMixSelect = true)
|
||||
public var minVideoSelectCount: Int {
|
||||
get {
|
||||
min(maxSelectCount, max(pri_minVideoSelectCount, 0))
|
||||
}
|
||||
set {
|
||||
pri_minVideoSelectCount = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether photos and videos can be selected together. Defaults to true.
|
||||
/// If set to false, only one video can be selected. Defaults to true.
|
||||
public var allowMixSelect = true
|
||||
|
||||
/// Preview selection max preview count, if the value is zero, only show `Camera`, `Album`, `Cancel` buttons. Defaults to 20.
|
||||
public var maxPreviewCount = 20
|
||||
|
||||
/// If set to false, gif and livephoto cannot be selected either. Defaults to true.
|
||||
public var allowSelectImage = true
|
||||
|
||||
public var allowSelectVideo = true
|
||||
|
||||
/// Allow select Gif, it only controls whether it is displayed in Gif form.
|
||||
/// If value is false, the Gif logo is not displayed. Defaults to true.
|
||||
public var allowSelectGif = true
|
||||
|
||||
/// Allow select LivePhoto, it only controls whether it is displayed in LivePhoto form.
|
||||
/// If value is false, the LivePhoto logo is not displayed. Defaults to false.
|
||||
public var allowSelectLivePhoto = false
|
||||
|
||||
private var pri_allowTakePhotoInLibrary = true
|
||||
/// Allow take photos in the album. Defaults to true.
|
||||
/// - warning: If allowTakePhoto and allowRecordVideo are both false, it will not be displayed.
|
||||
public var allowTakePhotoInLibrary: Bool {
|
||||
get {
|
||||
pri_allowTakePhotoInLibrary && (cameraConfiguration.allowTakePhoto || cameraConfiguration.allowRecordVideo)
|
||||
}
|
||||
set {
|
||||
pri_allowTakePhotoInLibrary = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to callback directly after taking a photo. Defaults to false.
|
||||
public var callbackDirectlyAfterTakingPhoto = false
|
||||
|
||||
private var pri_allowEditImage = true
|
||||
public var allowEditImage: Bool {
|
||||
get {
|
||||
pri_allowEditImage
|
||||
}
|
||||
set {
|
||||
pri_allowEditImage = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// - warning: The video can only be edited when no photos are selected, or only one video is selected, and the selection callback is executed immediately after editing is completed.
|
||||
private var pri_allowEditVideo = false
|
||||
public var allowEditVideo: Bool {
|
||||
get {
|
||||
pri_allowEditVideo
|
||||
}
|
||||
set {
|
||||
pri_allowEditVideo = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Control whether to display the selection button animation when selecting. Defaults to true.
|
||||
public var animateSelectBtnWhenSelect = true
|
||||
|
||||
/// Animation duration for select button
|
||||
public var selectBtnAnimationDuration: CFTimeInterval = 0.4
|
||||
|
||||
/// After selecting a image/video in the thumbnail interface, enter the editing interface directly. Defaults to false.
|
||||
/// - discussion: Editing image is only valid when allowEditImage is true and maxSelectCount is 1.
|
||||
/// Editing video is only valid when allowEditVideo is true and maxSelectCount is 1.
|
||||
public var editAfterSelectThumbnailImage = false
|
||||
|
||||
/// Only valid when allowMixSelect is false and allowEditVideo is true. Defaults to true.
|
||||
/// Just like the Wechat-Timeline selection style. If you want to crop the video after select thumbnail under allowMixSelect = true, please use **editAfterSelectThumbnailImage**.
|
||||
public var cropVideoAfterSelectThumbnail = true
|
||||
|
||||
/// If image edit tools only has clip and this property is true. When you click edit, the cropping interface (i.e. ZLClipImageViewController) will be displayed. Defaults to false.
|
||||
public var showClipDirectlyIfOnlyHasClipTool = false
|
||||
|
||||
/// Save the edited image to the album after editing. Defaults to true.
|
||||
public var saveNewImageAfterEdit = true
|
||||
|
||||
/// If true, you can slide select photos in album. Defaults to true.
|
||||
public var allowSlideSelect = true
|
||||
|
||||
/// When slide select is active, will auto scroll to top or bottom when your finger at the top or bottom. Defaults to true.
|
||||
public var autoScrollWhenSlideSelectIsActive = true
|
||||
|
||||
/// The max speed (pt/s) of auto scroll. Defaults to 600.
|
||||
public var autoScrollMaxSpeed: CGFloat = 600
|
||||
|
||||
/// If true, you can drag select photo when preview selection style. Defaults to false.
|
||||
public var allowDragSelect = false
|
||||
|
||||
/// Allow select full image. Defaults to true.
|
||||
public var allowSelectOriginal = true
|
||||
|
||||
/// Always return the original photo.
|
||||
/// - warning: Only valid when `allowSelectOriginal = false`, Defaults to false.
|
||||
public var alwaysRequestOriginal = false
|
||||
|
||||
/// Allow access to the preview large image interface (That is, whether to allow access to the large image interface after clicking the thumbnail image). Defaults to true.
|
||||
public var allowPreviewPhotos = true
|
||||
|
||||
/// Whether to show the preview button (i.e. the preview button in the lower left corner of the thumbnail interface). Defaults to true.
|
||||
public var showPreviewButtonInAlbum = true
|
||||
|
||||
/// Whether to display the selected count on the button. Defaults to true.
|
||||
public var showSelectCountOnDoneBtn = true
|
||||
|
||||
/// Maximum cropping time when editing video, unit: second. Defaults to 10.
|
||||
public var maxEditVideoTime: ZLPhotoConfiguration.Second = 10
|
||||
|
||||
/// Allow to choose the maximum duration of the video. Defaults to 120.
|
||||
public var maxSelectVideoDuration: ZLPhotoConfiguration.Second = 120
|
||||
|
||||
/// Allow to choose the minimum duration of the video. Defaults to 0.
|
||||
public var minSelectVideoDuration: ZLPhotoConfiguration.Second = 0
|
||||
|
||||
/// Allow to choose the maximum data size of the video. Defaults to infinite.
|
||||
public var maxSelectVideoDataSize: ZLPhotoConfiguration.KBUnit = .greatestFiniteMagnitude
|
||||
|
||||
/// Allow to choose the minimum data size of the video. Defaults to 0 KB.
|
||||
public var minSelectVideoDataSize: ZLPhotoConfiguration.KBUnit = 0
|
||||
|
||||
/// Image editor configuration.
|
||||
public var editImageConfiguration = ZLEditImageConfiguration()
|
||||
|
||||
/// Show the image captured by the camera is displayed on the camera button inside the album. Defaults to false.
|
||||
public var showCaptureImageOnTakePhotoBtn = false
|
||||
|
||||
/// In single selection mode, whether to display the selection button. Defaults to false.
|
||||
public var showSelectBtnWhenSingleSelect = false
|
||||
|
||||
/// Overlay a mask layer on top of the selected photos. Defaults to true.
|
||||
public var showSelectedMask = true
|
||||
|
||||
/// Display a border on the selected photos cell. Defaults to false.
|
||||
public var showSelectedBorder = false
|
||||
|
||||
/// Overlay a mask layer above the cells that cannot be selected. Defaults to true.
|
||||
public var showInvalidMask = true
|
||||
|
||||
/// Display the index of the selected photos. Defaults to true.
|
||||
public var showSelectedIndex = true
|
||||
|
||||
/// Display the selected photos at the bottom of the preview large photos interface. Defaults to true.
|
||||
public var showSelectedPhotoPreview = true
|
||||
|
||||
/// Timeout for image parsing. Defaults to 20.
|
||||
public var timeout: TimeInterval = 20
|
||||
|
||||
/// Whether to use custom camera. Defaults to true.
|
||||
public var useCustomCamera = true
|
||||
|
||||
/// The configuration for camera.
|
||||
public var cameraConfiguration = ZLCameraConfiguration()
|
||||
|
||||
/// This block will be called before selecting an image, the developer can first determine whether the asset is allowed to be selected.
|
||||
/// Only control whether it is allowed to be selected, and will not affect the selection logic in the framework.
|
||||
/// - Tips: If the choice is not allowed, the developer can toast prompt the user for relevant information.
|
||||
public var canSelectAsset: ((PHAsset) -> Bool)?
|
||||
|
||||
/// This block will be called when selecting an asset.
|
||||
public var didSelectAsset: ((PHAsset) -> Void)?
|
||||
|
||||
/// This block will be called when cancel selecting an asset.
|
||||
public var didDeselectAsset: ((PHAsset) -> Void)?
|
||||
|
||||
/// If user choose limited Photo mode, a button with '+' will be added to the ZLThumbnailViewController. It will call PHPhotoLibrary.shared().presentLimitedLibraryPicker(from:) to add photo. Defaults to true.
|
||||
/// E.g., Sina Weibo's ImagePicker
|
||||
public var showAddPhotoButton = true
|
||||
|
||||
/// iOS14 limited Photo mode, will show collection footer view in ZLThumbnailViewController.
|
||||
/// Will go to system setting if clicked. Defaults to true.
|
||||
public var showEnterSettingTips = true
|
||||
|
||||
/// The maximum number of frames for GIF images. To avoid crashes due to memory spikes caused by loading GIF images with too many frames, it is recommended that this value is not too large. Defaults to 50.
|
||||
public var maxFrameCountForGIF = 50
|
||||
|
||||
/// You can use this block to customize the playback of GIF images to achieve better results. For example, use FLAnimatedImage to play GIFs. Defaults to nil.
|
||||
public var gifPlayBlock: ((UIImageView, Data, [AnyHashable: Any]?) -> Void)?
|
||||
|
||||
/// Pause GIF image playback, used together with gifPlayBlock. Defaults to nil.
|
||||
public var pauseGIFBlock: ((UIImageView) -> Void)?
|
||||
|
||||
/// Resume GIF image playback, used together with gifPlayBlock. Defaults to nil.
|
||||
public var resumeGIFBlock: ((UIImageView) -> Void)?
|
||||
|
||||
/// Callback after the no authority alert dismiss.
|
||||
public var noAuthorityCallback: ((ZLNoAuthorityType) -> Void)?
|
||||
|
||||
/// Allow user to do something before select photo result callback.
|
||||
/// And you must call the second parameter of this block to continue the photos selection.
|
||||
/// The first parameter is the current controller.
|
||||
/// The second parameter is the block that needs to be called after the user completes the operation.
|
||||
public var operateBeforeDoneAction: ((UIViewController, @escaping () -> Void) -> Void)?
|
||||
}
|
||||
|
||||
@objc public enum ZLNoAuthorityType: Int {
|
||||
case library
|
||||
case camera
|
||||
case microphone
|
||||
}
|
||||
462
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoManager.swift
generated
Normal file
@@ -0,0 +1,462 @@
|
||||
//
|
||||
// ZLPhotoManager.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/11.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
@objcMembers
|
||||
public class ZLPhotoManager: NSObject {
|
||||
/// Save image to album.
|
||||
public class func saveImageToAlbum(image: UIImage, completion: ((Bool, PHAsset?) -> Void)?) {
|
||||
let status = PHPhotoLibrary.authorizationStatus()
|
||||
|
||||
if status == .denied || status == .restricted {
|
||||
completion?(false, nil)
|
||||
return
|
||||
}
|
||||
var placeholderAsset: PHObjectPlaceholder?
|
||||
let completionHandler: ((Bool, Error?) -> Void) = { suc, _ in
|
||||
ZLMainAsync {
|
||||
if suc {
|
||||
let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
|
||||
completion?(suc, asset)
|
||||
} else {
|
||||
completion?(false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if image.zl.hasAlphaChannel(), let data = image.pngData() {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let newAssetRequest = PHAssetCreationRequest.forAsset()
|
||||
newAssetRequest.addResource(with: .photo, data: data, options: nil)
|
||||
placeholderAsset = newAssetRequest.placeholderForCreatedAsset
|
||||
}, completionHandler: completionHandler)
|
||||
} else {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||||
placeholderAsset = newAssetRequest.placeholderForCreatedAsset
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Save video to album.
|
||||
public class func saveVideoToAlbum(url: URL, completion: ((Bool, PHAsset?) -> Void)?) {
|
||||
let status = PHPhotoLibrary.authorizationStatus()
|
||||
|
||||
if status == .denied || status == .restricted {
|
||||
completion?(false, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var placeholderAsset: PHObjectPlaceholder?
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let newAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
|
||||
placeholderAsset = newAssetRequest?.placeholderForCreatedAsset
|
||||
}) { suc, _ in
|
||||
ZLMainAsync {
|
||||
if suc {
|
||||
let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
|
||||
completion?(suc, asset)
|
||||
} else {
|
||||
completion?(false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func getAsset(from localIdentifier: String?) -> PHAsset? {
|
||||
guard let id = localIdentifier else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil)
|
||||
return result.firstObject
|
||||
}
|
||||
|
||||
/// Fetch photos from result.
|
||||
public class func fetchPhoto(in result: PHFetchResult<PHAsset>, ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, limitCount: Int = .max) -> [ZLPhotoModel] {
|
||||
var models: [ZLPhotoModel] = []
|
||||
let option: NSEnumerationOptions = ascending ? .init(rawValue: 0) : .reverse
|
||||
var count = 1
|
||||
|
||||
result.enumerateObjects(options: option) { asset, _, stop in
|
||||
let m = ZLPhotoModel(asset: asset)
|
||||
|
||||
if m.type == .image, !allowSelectImage {
|
||||
return
|
||||
}
|
||||
if m.type == .video, !allowSelectVideo {
|
||||
return
|
||||
}
|
||||
if count == limitCount {
|
||||
stop.pointee = true
|
||||
}
|
||||
|
||||
models.append(m)
|
||||
count += 1
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
/// Fetch all album list.
|
||||
public class func getPhotoAlbumList(ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, completion: ([ZLAlbumListModel]) -> Void) {
|
||||
let option = PHFetchOptions()
|
||||
if !allowSelectImage {
|
||||
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
|
||||
}
|
||||
if !allowSelectVideo {
|
||||
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
|
||||
}
|
||||
|
||||
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
|
||||
let albums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
|
||||
let streamAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumMyPhotoStream, options: nil) as! PHFetchResult<PHCollection>
|
||||
let syncedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumSyncedAlbum, options: nil) as! PHFetchResult<PHCollection>
|
||||
let sharedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumCloudShared, options: nil) as! PHFetchResult<PHCollection>
|
||||
let arr = [smartAlbums, albums, streamAlbums, syncedAlbums, sharedAlbums]
|
||||
|
||||
var albumList: [ZLAlbumListModel] = []
|
||||
arr.forEach { album in
|
||||
album.enumerateObjects { collection, _, _ in
|
||||
guard let collection = collection as? PHAssetCollection else { return }
|
||||
if collection.assetCollectionSubtype == .smartAlbumAllHidden {
|
||||
return
|
||||
}
|
||||
if #available(iOS 11.0, *), collection.assetCollectionSubtype.rawValue > PHAssetCollectionSubtype.smartAlbumLongExposures.rawValue {
|
||||
return
|
||||
}
|
||||
let result = PHAsset.fetchAssets(in: collection, options: option)
|
||||
if result.count == 0 {
|
||||
return
|
||||
}
|
||||
let title = self.getCollectionTitle(collection)
|
||||
|
||||
if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
|
||||
// Album of all photos.
|
||||
let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: true)
|
||||
albumList.insert(m, at: 0)
|
||||
} else {
|
||||
let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: false)
|
||||
albumList.append(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion(albumList)
|
||||
}
|
||||
|
||||
/// Fetch camera roll album.
|
||||
public class func getCameraRollAlbum(allowSelectImage: Bool, allowSelectVideo: Bool, completion: @escaping (ZLAlbumListModel) -> Void) {
|
||||
DispatchQueue.global().async {
|
||||
let option = PHFetchOptions()
|
||||
if !allowSelectImage {
|
||||
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
|
||||
}
|
||||
if !allowSelectVideo {
|
||||
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
|
||||
}
|
||||
|
||||
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
|
||||
smartAlbums.enumerateObjects { collection, _, stop in
|
||||
if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
|
||||
stop.pointee = true
|
||||
|
||||
let result = PHAsset.fetchAssets(in: collection, options: option)
|
||||
let albumModel = ZLAlbumListModel(title: self.getCollectionTitle(collection), result: result, collection: collection, option: option, isCameraRoll: true)
|
||||
ZLMainAsync {
|
||||
completion(albumModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversion collection title.
|
||||
private class func getCollectionTitle(_ collection: PHAssetCollection) -> String {
|
||||
if collection.assetCollectionType == .album {
|
||||
// Albums created by user.
|
||||
var title: String?
|
||||
if ZLCustomLanguageDeploy.language == .system {
|
||||
title = collection.localizedTitle
|
||||
} else {
|
||||
switch collection.assetCollectionSubtype {
|
||||
case .albumMyPhotoStream:
|
||||
title = localLanguageTextValue(.myPhotoStream)
|
||||
default:
|
||||
title = collection.localizedTitle
|
||||
}
|
||||
}
|
||||
return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
|
||||
}
|
||||
|
||||
var title: String?
|
||||
if ZLCustomLanguageDeploy.language == .system {
|
||||
title = collection.localizedTitle
|
||||
} else {
|
||||
switch collection.assetCollectionSubtype {
|
||||
case .smartAlbumUserLibrary:
|
||||
title = localLanguageTextValue(.cameraRoll)
|
||||
case .smartAlbumPanoramas:
|
||||
title = localLanguageTextValue(.panoramas)
|
||||
case .smartAlbumVideos:
|
||||
title = localLanguageTextValue(.videos)
|
||||
case .smartAlbumFavorites:
|
||||
title = localLanguageTextValue(.favorites)
|
||||
case .smartAlbumTimelapses:
|
||||
title = localLanguageTextValue(.timelapses)
|
||||
case .smartAlbumRecentlyAdded:
|
||||
title = localLanguageTextValue(.recentlyAdded)
|
||||
case .smartAlbumBursts:
|
||||
title = localLanguageTextValue(.bursts)
|
||||
case .smartAlbumSlomoVideos:
|
||||
title = localLanguageTextValue(.slomoVideos)
|
||||
case .smartAlbumSelfPortraits:
|
||||
title = localLanguageTextValue(.selfPortraits)
|
||||
case .smartAlbumScreenshots:
|
||||
title = localLanguageTextValue(.screenshots)
|
||||
case .smartAlbumDepthEffect:
|
||||
title = localLanguageTextValue(.depthEffect)
|
||||
case .smartAlbumLivePhotos:
|
||||
title = localLanguageTextValue(.livePhotos)
|
||||
default:
|
||||
title = collection.localizedTitle
|
||||
}
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
if collection.assetCollectionSubtype == PHAssetCollectionSubtype.smartAlbumAnimated {
|
||||
title = localLanguageTextValue(.animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public class func fetchImage(for asset: PHAsset, size: CGSize, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
|
||||
return fetchImage(for: asset, size: size, resizeMode: .fast, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public class func fetchOriginalImage(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
|
||||
return fetchImage(for: asset, size: PHImageManagerMaximumSize, resizeMode: .fast, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
/// Fetch asset data.
|
||||
@discardableResult
|
||||
public class func fetchOriginalImageData(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (Data, [AnyHashable: Any]?, Bool) -> Void) -> PHImageRequestID {
|
||||
let option = PHImageRequestOptions()
|
||||
if asset.zl.isGif {
|
||||
option.version = .original
|
||||
}
|
||||
option.isNetworkAccessAllowed = true
|
||||
option.resizeMode = .fast
|
||||
option.deliveryMode = .highQualityFormat
|
||||
option.progressHandler = { pro, error, stop, info in
|
||||
ZLMainAsync {
|
||||
progress?(CGFloat(pro), error, stop, info)
|
||||
}
|
||||
}
|
||||
|
||||
return PHImageManager.default().requestImageData(for: asset, options: option) { data, _, _, info in
|
||||
let cancel = info?[PHImageCancelledKey] as? Bool ?? false
|
||||
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
|
||||
if !cancel, let data = data {
|
||||
completion(data, info, isDegraded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch image for asset.
|
||||
private class func fetchImage(for asset: PHAsset, size: CGSize, resizeMode: PHImageRequestOptionsResizeMode, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
|
||||
let option = PHImageRequestOptions()
|
||||
option.resizeMode = resizeMode
|
||||
option.isNetworkAccessAllowed = true
|
||||
option.progressHandler = { pro, error, stop, info in
|
||||
ZLMainAsync {
|
||||
progress?(CGFloat(pro), error, stop, info)
|
||||
}
|
||||
}
|
||||
|
||||
return PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: option) { image, info in
|
||||
var downloadFinished = false
|
||||
if let info = info {
|
||||
downloadFinished = !(info[PHImageCancelledKey] as? Bool ?? false) && (info[PHImageErrorKey] == nil)
|
||||
}
|
||||
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
|
||||
if downloadFinished {
|
||||
ZLMainAsync {
|
||||
completion(image, isDegraded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class func fetchLivePhoto(for asset: PHAsset, completion: @escaping (PHLivePhoto?, [AnyHashable: Any]?, Bool) -> Void) -> PHImageRequestID {
|
||||
let option = PHLivePhotoRequestOptions()
|
||||
option.version = .current
|
||||
option.deliveryMode = .opportunistic
|
||||
option.isNetworkAccessAllowed = true
|
||||
|
||||
return PHImageManager.default().requestLivePhoto(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { livePhoto, info in
|
||||
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
|
||||
completion(livePhoto, info, isDegraded)
|
||||
}
|
||||
}
|
||||
|
||||
public class func fetchVideo(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (AVPlayerItem?, [AnyHashable: Any]?, Bool) -> Void) -> PHImageRequestID {
|
||||
let option = PHVideoRequestOptions()
|
||||
option.isNetworkAccessAllowed = true
|
||||
option.progressHandler = { pro, error, stop, info in
|
||||
ZLMainAsync {
|
||||
progress?(CGFloat(pro), error, stop, info)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/longitachi/ZLPhotoBrowser/issues/369#issuecomment-728679135
|
||||
if asset.zl.isInCloud {
|
||||
return PHImageManager.default().requestExportSession(forVideo: asset, options: option, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: { session, info in
|
||||
// iOS11 and earlier, callback is not on the main thread.
|
||||
ZLMainAsync {
|
||||
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
|
||||
if let avAsset = session?.asset {
|
||||
let item = AVPlayerItem(asset: avAsset)
|
||||
completion(item, info, isDegraded)
|
||||
} else {
|
||||
completion(nil, nil, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return PHImageManager.default().requestPlayerItem(forVideo: asset, options: option) { item, info in
|
||||
// iOS11 and earlier, callback is not on the main thread.
|
||||
ZLMainAsync {
|
||||
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
|
||||
completion(item, info, isDegraded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class func isFetchImageError(_ error: Error?) -> Bool {
|
||||
guard let e = error as NSError? else {
|
||||
return false
|
||||
}
|
||||
if e.domain == "CKErrorDomain" || e.domain == "CloudPhotoLibraryErrorDomain" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public class func fetchAVAsset(forVideo asset: PHAsset, completion: @escaping (AVAsset?, [AnyHashable: Any]?) -> Void) -> PHImageRequestID {
|
||||
let options = PHVideoRequestOptions()
|
||||
options.deliveryMode = .automatic
|
||||
options.isNetworkAccessAllowed = true
|
||||
|
||||
if asset.zl.isInCloud {
|
||||
return PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { session, info in
|
||||
// iOS11 and earlier, callback is not on the main thread.
|
||||
ZLMainAsync {
|
||||
if let avAsset = session?.asset {
|
||||
completion(avAsset, info)
|
||||
} else {
|
||||
completion(nil, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, info in
|
||||
ZLMainAsync {
|
||||
completion(avAsset, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the size of asset. Unit is KB.
|
||||
public class func fetchAssetSize(for asset: PHAsset) -> ZLPhotoConfiguration.KBUnit? {
|
||||
guard let resource = PHAssetResource.assetResources(for: asset).first,
|
||||
let size = resource.value(forKey: "fileSize") as? CGFloat else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return size / 1024
|
||||
}
|
||||
|
||||
/// Fetch asset local file path.
|
||||
/// - Note: Asynchronously to fetch the file path. calls completionHandler block on the main queue.
|
||||
public class func fetchAssetFilePath(for asset: PHAsset, completion: @escaping (String?) -> Void) {
|
||||
asset.requestContentEditingInput(with: nil) { input, _ in
|
||||
var path = input?.fullSizeImageURL?.absoluteString
|
||||
if path == nil,
|
||||
let dir = asset.value(forKey: "directory") as? String,
|
||||
let name = asset.zl.filename {
|
||||
path = String(format: "file:///var/mobile/Media/%@/%@", dir, name)
|
||||
}
|
||||
completion(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Save asset original data to file url. Support save image and video.
|
||||
/// - Note: Asynchronously write to a local file. Calls completionHandler block on the main queue.
|
||||
public class func saveAsset(_ asset: PHAsset, toFile fileUrl: URL, completion: @escaping ((Error?) -> Void)) {
|
||||
guard let resource = asset.zl.resource else {
|
||||
completion(NSError.assetSaveError)
|
||||
return
|
||||
}
|
||||
|
||||
PHAssetResourceManager.default().writeData(for: resource, toFile: fileUrl, options: nil) { error in
|
||||
ZLMainAsync {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authority related.
|
||||
public extension ZLPhotoManager {
|
||||
class func hasPhotoLibratyAuthority() -> Bool {
|
||||
return PHPhotoLibrary.authorizationStatus() == .authorized
|
||||
}
|
||||
|
||||
class func hasCameraAuthority() -> Bool {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
if status == .restricted || status == .denied {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class func hasMicrophoneAuthority() -> Bool {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||
if status == .restricted || status == .denied {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
165
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoModel.swift
generated
Normal file
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// ZLPhotoModel.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/11.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
public extension ZLPhotoModel {
|
||||
|
||||
enum MediaType: Int {
|
||||
case unknown = 0
|
||||
case image
|
||||
case gif
|
||||
case livePhoto
|
||||
case video
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ZLPhotoModel: NSObject {
|
||||
|
||||
public let ident: String
|
||||
|
||||
public let asset: PHAsset
|
||||
|
||||
public var type: ZLPhotoModel.MediaType = .unknown
|
||||
|
||||
public var duration: String = ""
|
||||
|
||||
public var isSelected: Bool = false
|
||||
|
||||
private var pri_dataSize: ZLPhotoConfiguration.KBUnit?
|
||||
|
||||
public var dataSize: ZLPhotoConfiguration.KBUnit? {
|
||||
if let pri_dataSize = pri_dataSize {
|
||||
return pri_dataSize
|
||||
}
|
||||
|
||||
let size = ZLPhotoManager.fetchAssetSize(for: asset)
|
||||
pri_dataSize = size
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
private var pri_editImage: UIImage?
|
||||
|
||||
public var editImage: UIImage? {
|
||||
set {
|
||||
pri_editImage = newValue
|
||||
}
|
||||
get {
|
||||
if let _ = editImageModel {
|
||||
return pri_editImage
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var second: ZLPhotoConfiguration.Second {
|
||||
guard type == .video else {
|
||||
return 0
|
||||
}
|
||||
return Int(round(asset.duration))
|
||||
}
|
||||
|
||||
public var whRatio: CGFloat {
|
||||
return CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight)
|
||||
}
|
||||
|
||||
public var previewSize: CGSize {
|
||||
let scale: CGFloat = UIScreen.main.scale
|
||||
if whRatio > 1 {
|
||||
let h = min(UIScreen.main.bounds.height, ZLMaxImageWidth) * scale
|
||||
let w = h * whRatio
|
||||
return CGSize(width: w, height: h)
|
||||
} else {
|
||||
let w = min(UIScreen.main.bounds.width, ZLMaxImageWidth) * scale
|
||||
let h = w / whRatio
|
||||
return CGSize(width: w, height: h)
|
||||
}
|
||||
}
|
||||
|
||||
// Content of the last edit.
|
||||
public var editImageModel: ZLEditImageModel?
|
||||
|
||||
public init(asset: PHAsset) {
|
||||
ident = asset.localIdentifier
|
||||
self.asset = asset
|
||||
super.init()
|
||||
|
||||
type = transformAssetType(for: asset)
|
||||
if type == .video {
|
||||
duration = transformDuration(for: asset)
|
||||
}
|
||||
}
|
||||
|
||||
public func transformAssetType(for asset: PHAsset) -> ZLPhotoModel.MediaType {
|
||||
switch asset.mediaType {
|
||||
case .video:
|
||||
return .video
|
||||
case .image:
|
||||
if asset.zl.isGif {
|
||||
return .gif
|
||||
}
|
||||
if asset.mediaSubtypes.contains(.photoLive) {
|
||||
return .livePhoto
|
||||
}
|
||||
return .image
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public func transformDuration(for asset: PHAsset) -> String {
|
||||
let dur = Int(round(asset.duration))
|
||||
|
||||
switch dur {
|
||||
case 0..<60:
|
||||
return String(format: "00:%02d", dur)
|
||||
case 60..<3600:
|
||||
let m = dur / 60
|
||||
let s = dur % 60
|
||||
return String(format: "%02d:%02d", m, s)
|
||||
case 3600...:
|
||||
let h = dur / 3600
|
||||
let m = (dur % 3600) / 60
|
||||
let s = dur % 60
|
||||
return String(format: "%02d:%02d:%02d", h, m, s)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension ZLPhotoModel {
|
||||
|
||||
static func ==(lhs: ZLPhotoModel, rhs: ZLPhotoModel) -> Bool {
|
||||
return lhs.ident == rhs.ident
|
||||
}
|
||||
|
||||
}
|
||||
1189
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoPreviewCell.swift
generated
Normal file
1172
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoPreviewController.swift
generated
Normal file
1139
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoPreviewSheet.swift
generated
Normal file
434
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoUIConfiguration+Chaining.swift
generated
Normal file
@@ -0,0 +1,434 @@
|
||||
//
|
||||
// ZLPhotoUIConfiguration+Chaining.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/4/19.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: chaining
|
||||
|
||||
public extension ZLPhotoUIConfiguration {
|
||||
@discardableResult
|
||||
func style(_ style: ZLPhotoBrowserStyle) -> ZLPhotoUIConfiguration {
|
||||
self.style = style
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func statusBarStyle(_ statusBarStyle: UIStatusBarStyle) -> ZLPhotoUIConfiguration {
|
||||
self.statusBarStyle = statusBarStyle
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navCancelButtonStyle(_ style: ZLPhotoUIConfiguration.CancelButtonStyle) -> ZLPhotoUIConfiguration {
|
||||
navCancelButtonStyle = style
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showStatusBarInPreviewInterface(_ value: Bool) -> ZLPhotoUIConfiguration {
|
||||
showStatusBarInPreviewInterface = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func hudStyle(_ style: ZLProgressHUD.HUDStyle) -> ZLPhotoUIConfiguration {
|
||||
hudStyle = style
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func adjustSliderType(_ type: ZLAdjustSliderType) -> ZLPhotoUIConfiguration {
|
||||
adjustSliderType = type
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cellCornerRadio(_ cornerRadio: CGFloat) -> ZLPhotoUIConfiguration {
|
||||
cellCornerRadio = cornerRadio
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func customAlertClass(_ alertClass: ZLCustomAlertProtocol.Type?) -> ZLPhotoUIConfiguration {
|
||||
customAlertClass = alertClass
|
||||
return self
|
||||
}
|
||||
|
||||
/// - Note: This property is ignored when using columnCountBlock.
|
||||
@discardableResult
|
||||
func columnCount(_ count: Int) -> ZLPhotoUIConfiguration {
|
||||
columnCount = count
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func columnCountBlock(_ block: ((_ collectionViewWidth: CGFloat) -> Int)?) -> ZLPhotoUIConfiguration {
|
||||
columnCountBlock = block
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func minimumInteritemSpacing(_ value: CGFloat) -> ZLPhotoUIConfiguration {
|
||||
minimumInteritemSpacing = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func minimumLineSpacing(_ value: CGFloat) -> ZLPhotoUIConfiguration {
|
||||
minimumLineSpacing = value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navViewBlurEffectOfAlbumList(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
|
||||
navViewBlurEffectOfAlbumList = effect
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navViewBlurEffectOfPreview(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
|
||||
navViewBlurEffectOfPreview = effect
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomViewBlurEffectOfAlbumList(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
|
||||
bottomViewBlurEffectOfAlbumList = effect
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomViewBlurEffectOfPreview(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
|
||||
bottomViewBlurEffectOfPreview = effect
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func customImageNames(_ names: [String]) -> ZLPhotoUIConfiguration {
|
||||
customImageNames = names
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func customImageForKey(_ map: [String: UIImage?]) -> ZLPhotoUIConfiguration {
|
||||
customImageForKey = map
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func languageType(_ type: ZLLanguageType) -> ZLPhotoUIConfiguration {
|
||||
languageType = type
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func customLanguageKeyValue(_ map: [ZLLocalLanguageKey: String]) -> ZLPhotoUIConfiguration {
|
||||
customLanguageKeyValue = map
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func themeFontName(_ name: String) -> ZLPhotoUIConfiguration {
|
||||
themeFontName = name
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func themeColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
themeColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sheetTranslucentColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
sheetTranslucentColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sheetBtnBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
sheetBtnBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sheetBtnTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
sheetBtnTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sheetBtnTitleTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
sheetBtnTitleTintColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navBarColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
navBarColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navBarColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
navBarColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
navTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
navTitleColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func navEmbedTitleViewBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
navEmbedTitleViewBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func albumListBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
albumListBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func embedAlbumListTranslucentColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
embedAlbumListTranslucentColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func albumListTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
albumListTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func albumListCountColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
albumListCountColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func separatorColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
separatorColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func thumbnailBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
thumbnailBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func previewVCBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
previewVCBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBgColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBgColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnNormalTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnNormalTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewDoneBtnNormalTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewDoneBtnNormalTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnNormalTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnNormalTitleColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewDoneBtnNormalTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewDoneBtnNormalTitleColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnDisableTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnDisableTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewDoneBtnDisableTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewDoneBtnDisableTitleColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnDisableTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnDisableTitleColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewDoneBtnDisableTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewDoneBtnDisableTitleColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnNormalBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnNormalBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnNormalBgColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnNormalBgColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnDisableBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnDisableBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func bottomToolViewBtnDisableBgColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
bottomToolViewBtnDisableBgColorOfPreviewVC = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func limitedAuthorityTipsColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
limitedAuthorityTipsColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cameraRecodeProgressColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
cameraRecodeProgressColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectedMaskColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
selectedMaskColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectedBorderColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
selectedBorderColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func invalidMaskColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
invalidMaskColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func indexLabelTextColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
indexLabelTextColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func indexLabelBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
indexLabelBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cameraCellBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
cameraCellBgColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func adjustSliderNormalColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
adjustSliderNormalColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func adjustSliderTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
adjustSliderTintColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func imageEditorToolTitleNormalColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
imageEditorToolTitleNormalColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func imageEditorToolTitleTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
imageEditorToolTitleTintColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func imageEditorToolIconTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
imageEditorToolIconTintColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func trashCanBackgroundNormalColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
trashCanBackgroundNormalColor = color
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func trashCanBackgroundTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
|
||||
trashCanBackgroundTintColor = color
|
||||
return self
|
||||
}
|
||||
}
|
||||
460
Pods/ZLPhotoBrowser/Sources/General/ZLPhotoUIConfiguration.swift
generated
Normal file
@@ -0,0 +1,460 @@
|
||||
//
|
||||
// ZLPhotoUIConfiguration.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/4/18.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Custom UI configuration (include colors, images, text, font)
|
||||
@objcMembers
|
||||
public class ZLPhotoUIConfiguration: NSObject {
|
||||
@objc public enum CancelButtonStyle: Int {
|
||||
case text
|
||||
case image
|
||||
}
|
||||
|
||||
private static var single = ZLPhotoUIConfiguration()
|
||||
|
||||
public class func `default`() -> ZLPhotoUIConfiguration {
|
||||
return ZLPhotoUIConfiguration.single
|
||||
}
|
||||
|
||||
public class func resetConfiguration() {
|
||||
ZLPhotoUIConfiguration.single = ZLPhotoUIConfiguration()
|
||||
}
|
||||
|
||||
// MARK: Framework style.
|
||||
|
||||
public var style: ZLPhotoBrowserStyle = .embedAlbumList
|
||||
|
||||
public var statusBarStyle: UIStatusBarStyle = .lightContent
|
||||
|
||||
/// text: Cancel. image: 'x'. Defaults to image.
|
||||
public var navCancelButtonStyle: ZLPhotoUIConfiguration.CancelButtonStyle = .image
|
||||
|
||||
/// Whether to show the status bar when previewing photos. Defaults to false.
|
||||
public var showStatusBarInPreviewInterface = false
|
||||
|
||||
/// HUD style. Defaults to dark.
|
||||
public var hudStyle: ZLProgressHUD.HUDStyle = .dark
|
||||
|
||||
/// Adjust Slider Type
|
||||
public var adjustSliderType: ZLAdjustSliderType = .vertical
|
||||
|
||||
public var cellCornerRadio: CGFloat = 0
|
||||
|
||||
/// Custom alert class. Defaults to nil.
|
||||
public var customAlertClass: ZLCustomAlertProtocol.Type?
|
||||
|
||||
private var pri_columnCount = 4
|
||||
/// The column count when iPhone is in portait mode. Minimum is 2, maximum is 6. Defaults to 4.
|
||||
/// ```
|
||||
/// iPhone landscape mode: columnCount += 2.
|
||||
/// iPad portait mode: columnCount += 2.
|
||||
/// iPad landscape mode: columnCount += 4.
|
||||
/// ```
|
||||
///
|
||||
/// - Note: This property is ignored when using columnCountBlock.
|
||||
public var columnCount: Int {
|
||||
get {
|
||||
pri_columnCount
|
||||
}
|
||||
set {
|
||||
pri_columnCount = min(6, max(newValue, 2))
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this property to customize the column count for `ZLThumbnailViewController`.
|
||||
/// This property is recommended.
|
||||
public var columnCountBlock: ((_ collectionViewWidth: CGFloat) -> Int)?
|
||||
|
||||
/// The minimum spacing to use between items in the same row for `ZLThumbnailViewController`.
|
||||
public var minimumInteritemSpacing: CGFloat = 2
|
||||
|
||||
/// The minimum spacing to use between lines of items in the grid for `ZLThumbnailViewController`.
|
||||
public var minimumLineSpacing: CGFloat = 2
|
||||
|
||||
// MARK: Navigation and bottom tool bar
|
||||
|
||||
/// The blur effect of the navigation bar in the album list
|
||||
public var navViewBlurEffectOfAlbumList: UIBlurEffect? = UIBlurEffect(style: .dark)
|
||||
|
||||
/// The blur effect of the navigation bar in the preview interface
|
||||
public var navViewBlurEffectOfPreview: UIBlurEffect? = UIBlurEffect(style: .dark)
|
||||
|
||||
/// The blur effect of the bottom tool bar in the album list
|
||||
public var bottomViewBlurEffectOfAlbumList: UIBlurEffect? = UIBlurEffect(style: .dark)
|
||||
|
||||
/// The blur effect of the bottom tool bar in the preview interface
|
||||
public var bottomViewBlurEffectOfPreview: UIBlurEffect? = UIBlurEffect(style: .dark)
|
||||
|
||||
// MARK: Image properties
|
||||
|
||||
/// Developers can customize images, but the name of the custom image resource must be consistent with the image name in the replaced bundle.
|
||||
/// - example: Developers need to replace the selected and unselected image resources, and the array that needs to be passed in is
|
||||
/// ["zl_btn_selected", "zl_btn_unselected"].
|
||||
public var customImageNames: [String] = [] {
|
||||
didSet {
|
||||
ZLCustomImageDeploy.imageNames = customImageNames
|
||||
}
|
||||
}
|
||||
|
||||
/// Developers can customize images, but the name of the custom image resource must be consistent with the image name in the replaced bundle.
|
||||
/// - example: Developers need to replace the selected and unselected image resources, and the array that needs to be passed in is
|
||||
/// ["zl_btn_selected": selectedImage, "zl_btn_unselected": unselectedImage].
|
||||
public var customImageForKey: [String: UIImage?] = [:] {
|
||||
didSet {
|
||||
customImageForKey.forEach { ZLCustomImageDeploy.imageForKey[$0.key] = $0.value }
|
||||
}
|
||||
}
|
||||
|
||||
/// Developers can customize images, but the name of the custom image resource must be consistent with the image name in the replaced bundle.
|
||||
/// - example: Developers need to replace the selected and unselected image resources, and the array that needs to be passed in is
|
||||
/// ["zl_btn_selected": selectedImage, "zl_btn_unselected": unselectedImage].
|
||||
public var customImageForKey_objc: [String: UIImage] = [:] {
|
||||
didSet {
|
||||
ZLCustomImageDeploy.imageForKey = customImageForKey_objc
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Language properties
|
||||
|
||||
/// Language for framework.
|
||||
public var languageType: ZLLanguageType = .system {
|
||||
didSet {
|
||||
ZLCustomLanguageDeploy.language = languageType
|
||||
Bundle.resetLanguage()
|
||||
}
|
||||
}
|
||||
|
||||
/// Developers can customize languages.
|
||||
/// - example: If you needs to replace
|
||||
/// key: .hudLoading, value: "loading, waiting please" language,
|
||||
/// The dictionary that needs to be passed in is [.hudLoading: "text to be replaced"].
|
||||
/// - warning: Please pay attention to the placeholders contained in languages when changing, such as %ld, %@.
|
||||
public var customLanguageKeyValue: [ZLLocalLanguageKey: String] = [:] {
|
||||
didSet {
|
||||
ZLCustomLanguageDeploy.deploy = customLanguageKeyValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Developers can customize languages (This property is only for objc).
|
||||
/// - example: If you needs to replace
|
||||
/// key: @"loading", value: @"loading, waiting please" language,
|
||||
/// The dictionary that needs to be passed in is @[@"hudLoading": @"text to be replaced"].
|
||||
/// - warning: Please pay attention to the placeholders contained in languages when changing, such as %ld, %@.
|
||||
public var customLanguageKeyValue_objc: [String: String] = [:] {
|
||||
didSet {
|
||||
var swiftParams: [ZLLocalLanguageKey: String] = [:]
|
||||
customLanguageKeyValue_objc.forEach { key, value in
|
||||
swiftParams[ZLLocalLanguageKey(rawValue: key)] = value
|
||||
}
|
||||
customLanguageKeyValue = swiftParams
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Font
|
||||
|
||||
/// Font name.
|
||||
public var themeFontName: String? {
|
||||
didSet {
|
||||
ZLCustomFontDeploy.fontName = themeFontName
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Color properties
|
||||
|
||||
/// The theme color of framework.
|
||||
/// 框架主题色
|
||||
public var themeColor: UIColor = .zl.rgba(7, 213, 101)
|
||||
|
||||
/// Preview selection mode, translucent background color above.
|
||||
/// 预览快速选择模式下,上方透明区域背景色
|
||||
public var sheetTranslucentColor: UIColor = .black.withAlphaComponent(0.1)
|
||||
|
||||
/// Preview selection mode, a background color for `Camera`, `Album`, `Cancel` buttons.
|
||||
/// 预览快速选择模式下,按钮背景颜色
|
||||
public var sheetBtnBgColor: UIColor = .white
|
||||
|
||||
/// Preview selection mode, a text color for `Camera`, `Album`, `Cancel` buttons.
|
||||
/// 预览快速选择模式下,按钮标题颜色
|
||||
public var sheetBtnTitleColor: UIColor = .black
|
||||
|
||||
private var pri_sheetBtnTitleTintColor: UIColor?
|
||||
/// Preview selection mode, cancel button title color when the selection amount is superior than 0.
|
||||
/// 预览快速选择模式下,按钮标题高亮颜色
|
||||
public var sheetBtnTitleTintColor: UIColor {
|
||||
get {
|
||||
pri_sheetBtnTitleTintColor ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_sheetBtnTitleTintColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// A color for navigation bar.
|
||||
/// 相册列表及小图界面导航条背景色
|
||||
public var navBarColor: UIColor = .zl.rgba(160, 160, 160, 0.65)
|
||||
|
||||
/// A color for navigation bar in preview interface.
|
||||
/// 预览大图界面的导航条背景色
|
||||
public var navBarColorOfPreviewVC: UIColor = .zl.rgba(160, 160, 160, 0.65)
|
||||
|
||||
/// A color for Navigation bar text.
|
||||
/// 相册列表及小图界面导航栏标题颜色
|
||||
public var navTitleColor: UIColor = .white
|
||||
|
||||
/// A color for Navigation bar text of preview vc.
|
||||
/// 预览大图界面导航栏标题颜色
|
||||
public var navTitleColorOfPreviewVC: UIColor = .white
|
||||
|
||||
/// The background color of the title view when the frame style is embedAlbumList.
|
||||
/// 下拉选择相册列表模式下,选择区域的背景色
|
||||
public var navEmbedTitleViewBgColor: UIColor = .zl.rgba(80, 80, 80)
|
||||
|
||||
/// A color for background in album list.
|
||||
/// 相册列表背景色
|
||||
public var albumListBgColor: UIColor = .zl.rgba(45, 45, 45)
|
||||
|
||||
/// A color of the translucent area below the embed album list.
|
||||
/// 嵌入式相册列表下方透明区域颜色
|
||||
public var embedAlbumListTranslucentColor: UIColor = .black.withAlphaComponent(0.8)
|
||||
|
||||
/// A color for album list title label.
|
||||
/// 相册列表标题颜色
|
||||
public var albumListTitleColor: UIColor = .white
|
||||
|
||||
/// A color for album list count label.
|
||||
/// 相册列表数量label的颜色
|
||||
public var albumListCountColor: UIColor = .zl.rgba(180, 180, 180)
|
||||
|
||||
/// A color for album list separator.
|
||||
/// 相册列表分割线颜色
|
||||
public var separatorColor: UIColor = .zl.rgba(60, 60, 60)
|
||||
|
||||
/// A color for background in thumbnail interface.
|
||||
/// 相册小图界面背景色
|
||||
public var thumbnailBgColor: UIColor = .zl.rgba(50, 50, 50)
|
||||
|
||||
/// A color for background in preview interface..
|
||||
/// 预览大图界面背景色
|
||||
public var previewVCBgColor: UIColor = .black
|
||||
|
||||
/// A color for background in bottom tool view.
|
||||
/// 相册小图界面底部工具条背景色
|
||||
public var bottomToolViewBgColor: UIColor = .zl.rgba(35, 35, 35, 0.3)
|
||||
|
||||
/// A color for background in bottom tool view in preview interface.
|
||||
/// 预览大图界面底部工具条背景色
|
||||
public var bottomToolViewBgColorOfPreviewVC: UIColor = .zl.rgba(35, 35, 35, 0.3)
|
||||
|
||||
/// The normal state title color of bottom tool view buttons. Without done button.
|
||||
/// 相册小图界面底部按钮可交互状态下标题颜色,不包括 `完成` 按钮
|
||||
public var bottomToolViewBtnNormalTitleColor: UIColor = .white
|
||||
|
||||
/// The normal state title color of bottom tool view done button.
|
||||
/// 相册小图界面底部 `完成` 按钮可交互状态下标题颜色
|
||||
public var bottomToolViewDoneBtnNormalTitleColor: UIColor = .white
|
||||
|
||||
/// The normal state title color of bottom tool view buttons in preview interface. Without done button.
|
||||
/// 预览大图界面底部按钮可交互状态下标题颜色,不包括 `完成` 按钮
|
||||
public var bottomToolViewBtnNormalTitleColorOfPreviewVC: UIColor = .white
|
||||
|
||||
/// The normal state title color of bottom tool view done button.
|
||||
/// 预览大图界面底部 `完成` 按钮可交互状态下标题颜色
|
||||
public var bottomToolViewDoneBtnNormalTitleColorOfPreviewVC: UIColor = .white
|
||||
|
||||
/// The disable state title color of bottom tool view buttons. Without done button.
|
||||
/// 相册小图界面底部按钮不可交互状态下标题颜色,不包括 `完成` 按钮
|
||||
public var bottomToolViewBtnDisableTitleColor: UIColor = .zl.rgba(168, 168, 168)
|
||||
|
||||
/// The disable state title color of bottom tool view done button.
|
||||
/// 相册小图界面底部 `完成` 按钮不可交互状态下标题颜色
|
||||
public var bottomToolViewDoneBtnDisableTitleColor: UIColor = .zl.rgba(168, 168, 168)
|
||||
|
||||
/// The disable state title color of bottom tool view buttons in preview interface. Without done button.
|
||||
/// 预览大图界面底部按钮不可交互状态下标题颜色,不包括 `完成` 按钮
|
||||
public var bottomToolViewBtnDisableTitleColorOfPreviewVC: UIColor = .zl.rgba(168, 168, 168)
|
||||
|
||||
/// The disable state title color of bottom tool view done button in preview interface.
|
||||
/// 预览大图界面底部 `完成` 按钮不可交互状态下标题颜色
|
||||
public var bottomToolViewDoneBtnDisableTitleColorOfPreviewVC: UIColor = .zl.rgba(168, 168, 168)
|
||||
|
||||
private var pri_bottomToolViewBtnNormalBgColor: UIColor?
|
||||
/// The normal state background color of bottom tool view buttons.
|
||||
/// 相册小图界面底部按钮可交互状态下背景色
|
||||
public var bottomToolViewBtnNormalBgColor: UIColor {
|
||||
get {
|
||||
pri_bottomToolViewBtnNormalBgColor ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_bottomToolViewBtnNormalBgColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var pri_bottomToolViewBtnNormalBgColorOfPreviewVC: UIColor?
|
||||
/// The normal state background color of bottom tool view buttons in preview interface.
|
||||
/// 预览大图界面底部按钮可交互状态下背景色
|
||||
public var bottomToolViewBtnNormalBgColorOfPreviewVC: UIColor {
|
||||
get {
|
||||
pri_bottomToolViewBtnNormalBgColorOfPreviewVC ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_bottomToolViewBtnNormalBgColorOfPreviewVC = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The disable state background color of bottom tool view buttons.
|
||||
/// 相册小图界面底部按钮不可交互状态下背景色
|
||||
public var bottomToolViewBtnDisableBgColor: UIColor = .zl.rgba(50, 50, 50)
|
||||
|
||||
/// The disable state background color of bottom tool view buttons in preview interface.
|
||||
/// 预览大图界面底部按钮不可交互状态下背景色
|
||||
public var bottomToolViewBtnDisableBgColorOfPreviewVC: UIColor = .zl.rgba(50, 50, 50)
|
||||
|
||||
/// With iOS14 limited authority, a color for select more photos at the bottom of the thumbnail interface.
|
||||
/// iOS14 limited权限下,下方提示选择更多图片信息文字的颜色
|
||||
public var limitedAuthorityTipsColor: UIColor = .white
|
||||
|
||||
private var pri_cameraRecodeProgressColor: UIColor?
|
||||
/// The record progress color of custom camera.
|
||||
/// 自定义相机录制视频时进度条颜色
|
||||
public var cameraRecodeProgressColor: UIColor {
|
||||
get {
|
||||
pri_cameraRecodeProgressColor ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_cameraRecodeProgressColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Mask layer color of selected cell.
|
||||
/// 已选择照片上方遮罩阴影颜色
|
||||
public var selectedMaskColor: UIColor = .black.withAlphaComponent(0.2)
|
||||
|
||||
private var pri_selectedBorderColor: UIColor?
|
||||
/// Border color of selected cell.
|
||||
/// 已选择照片border颜色
|
||||
public var selectedBorderColor: UIColor {
|
||||
get {
|
||||
pri_selectedBorderColor ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_selectedBorderColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Mask layer color of the cell that cannot be selected.
|
||||
/// 不可选的照片上方遮罩阴影颜色
|
||||
public var invalidMaskColor: UIColor = .white.withAlphaComponent(0.5)
|
||||
|
||||
/// The text color of selected cell index label.
|
||||
/// 已选照片右上角序号label背景色
|
||||
public var indexLabelTextColor: UIColor = .white
|
||||
|
||||
private var pri_indexLabelBgColor: UIColor?
|
||||
/// The background color of selected cell index label.
|
||||
/// 已选照片右上角序号label背景色
|
||||
public var indexLabelBgColor: UIColor {
|
||||
get {
|
||||
pri_indexLabelBgColor ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_indexLabelBgColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The background color of camera cell inside album.
|
||||
/// 相册小图界面拍照按钮背景色
|
||||
public var cameraCellBgColor: UIColor = .zl.rgba(76, 76, 76)
|
||||
|
||||
/// The normal color of adjust slider.
|
||||
/// 编辑图片,调整饱和度、对比度、亮度时,右侧slider背景色
|
||||
public var adjustSliderNormalColor: UIColor = .white
|
||||
|
||||
private var pri_adjustSliderTintColor: UIColor?
|
||||
/// The tint color of adjust slider.
|
||||
/// 编辑图片,调整饱和度、对比度、亮度时,右侧slider背景高亮色
|
||||
public var adjustSliderTintColor: UIColor {
|
||||
get {
|
||||
pri_adjustSliderTintColor ?? themeColor
|
||||
}
|
||||
set {
|
||||
pri_adjustSliderTintColor = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The normal color of the title below the various tools in the image editor.
|
||||
/// 图片编辑器中各种工具下方标题普通状态下的颜色
|
||||
public var imageEditorToolTitleNormalColor: UIColor = .zl.rgba(160, 160, 160)
|
||||
|
||||
/// The tint color of the title below the various tools in the image editor.
|
||||
/// 图片编辑器中各种工具下方标题高亮状态下的颜色
|
||||
public var imageEditorToolTitleTintColor: UIColor = .white
|
||||
|
||||
/// The tint color of the image editor tool icons.
|
||||
/// 图片编辑器中各种工具图标高亮状态下的颜色
|
||||
public var imageEditorToolIconTintColor: UIColor?
|
||||
|
||||
/// Background color of trash can in image editor.
|
||||
/// 编辑器中垃圾箱普通状态下的颜色
|
||||
public var trashCanBackgroundNormalColor: UIColor = .zl.rgba(40, 40, 40, 0.8)
|
||||
|
||||
/// Background tint color of trash can in image editor.
|
||||
/// 编辑器中垃圾箱高亮状态下的颜色
|
||||
public var trashCanBackgroundTintColor: UIColor = .zl.rgba(241, 79, 79, 0.98)
|
||||
}
|
||||
|
||||
/// Font deploy
|
||||
enum ZLCustomFontDeploy {
|
||||
static var fontName: String?
|
||||
}
|
||||
|
||||
/// Image source deploy
|
||||
enum ZLCustomImageDeploy {
|
||||
static var imageNames: [String] = []
|
||||
|
||||
static var imageForKey: [String: UIImage] = [:]
|
||||
}
|
||||
|
||||
@objc public enum ZLPhotoBrowserStyle: Int {
|
||||
/// The album list is embedded in the navigation of the thumbnail interface, click the drop-down display.
|
||||
case embedAlbumList
|
||||
|
||||
/// The display relationship between the album list and the thumbnail interface is push.
|
||||
case externalAlbumList
|
||||
}
|
||||
|
||||
/// Language deploy
|
||||
enum ZLCustomLanguageDeploy {
|
||||
static var language: ZLLanguageType = .system
|
||||
|
||||
static var deploy: [ZLLocalLanguageKey: String] = [:]
|
||||
}
|
||||
|
||||
/// Adjust slider type
|
||||
@objc public enum ZLAdjustSliderType: Int {
|
||||
case vertical
|
||||
case horizontal
|
||||
}
|
||||
185
Pods/ZLPhotoBrowser/Sources/General/ZLProgressHUD.swift
generated
Normal file
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// ZLProgressHUD.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/17.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
public class ZLProgressHUD: UIView {
|
||||
@objc public enum HUDStyle: Int {
|
||||
case light
|
||||
case lightBlur
|
||||
case dark
|
||||
case darkBlur
|
||||
|
||||
var bgColor: UIColor {
|
||||
switch self {
|
||||
case .light:
|
||||
return .white
|
||||
case .dark:
|
||||
return .darkGray
|
||||
case .lightBlur:
|
||||
return UIColor.white.withAlphaComponent(0.8)
|
||||
case .darkBlur:
|
||||
return UIColor.darkGray.withAlphaComponent(0.8)
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
switch self {
|
||||
case .light, .lightBlur:
|
||||
return .zl.getImage("zl_loading_dark")
|
||||
case .dark, .darkBlur:
|
||||
return .zl.getImage("zl_loading_light")
|
||||
}
|
||||
}
|
||||
|
||||
var textColor: UIColor {
|
||||
switch self {
|
||||
case .light, .lightBlur:
|
||||
return .black
|
||||
case .dark, .darkBlur:
|
||||
return .white
|
||||
}
|
||||
}
|
||||
|
||||
var blurEffectStyle: UIBlurEffect.Style? {
|
||||
switch self {
|
||||
case .light, .dark:
|
||||
return nil
|
||||
case .lightBlur:
|
||||
return .extraLight
|
||||
case .darkBlur:
|
||||
return .dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let style: ZLProgressHUD.HUDStyle
|
||||
|
||||
private lazy var loadingView = UIImageView(image: style.icon)
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
public var timeoutBlock: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLProgressHUD deinit")
|
||||
cleanTimer()
|
||||
}
|
||||
|
||||
public init(style: ZLProgressHUD.HUDStyle) {
|
||||
self.style = style
|
||||
super.init(frame: UIScreen.main.bounds)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
let view = UIView(frame: CGRect(x: 0, y: 0, width: 135, height: 135))
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 12
|
||||
view.backgroundColor = style.bgColor
|
||||
view.clipsToBounds = true
|
||||
view.center = center
|
||||
|
||||
if let effectStyle = style.blurEffectStyle {
|
||||
let effect = UIBlurEffect(style: effectStyle)
|
||||
let effectView = UIVisualEffectView(effect: effect)
|
||||
effectView.frame = view.bounds
|
||||
view.addSubview(effectView)
|
||||
}
|
||||
|
||||
loadingView.frame = CGRect(x: 135 / 2 - 20, y: 27, width: 40, height: 40)
|
||||
view.addSubview(loadingView)
|
||||
|
||||
let label = UILabel(frame: CGRect(x: 0, y: 85, width: view.bounds.width, height: 30))
|
||||
label.textAlignment = .center
|
||||
label.textColor = style.textColor
|
||||
label.font = .zl.font(ofSize: 16)
|
||||
label.text = localLanguageTextValue(.hudLoading)
|
||||
view.addSubview(label)
|
||||
|
||||
addSubview(view)
|
||||
}
|
||||
|
||||
private func startAnimation() {
|
||||
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
animation.fromValue = 0
|
||||
animation.toValue = CGFloat.pi * 2
|
||||
animation.duration = 0.8
|
||||
animation.repeatCount = .infinity
|
||||
animation.fillMode = .forwards
|
||||
animation.isRemovedOnCompletion = false
|
||||
loadingView.layer.add(animation, forKey: nil)
|
||||
}
|
||||
|
||||
public func show(
|
||||
in view: UIView? = UIApplication.shared.keyWindow,
|
||||
timeout: TimeInterval = 100
|
||||
) {
|
||||
ZLMainAsync {
|
||||
self.startAnimation()
|
||||
view?.addSubview(self)
|
||||
}
|
||||
if timeout > 0 {
|
||||
cleanTimer()
|
||||
timer = Timer.scheduledTimer(timeInterval: timeout, target: ZLWeakProxy(target: self), selector: #selector(timeout(_:)), userInfo: nil, repeats: false)
|
||||
RunLoop.current.add(timer!, forMode: .default)
|
||||
}
|
||||
}
|
||||
|
||||
public func hide() {
|
||||
cleanTimer()
|
||||
ZLMainAsync {
|
||||
self.loadingView.layer.removeAllAnimations()
|
||||
self.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func timeout(_ timer: Timer) {
|
||||
timeoutBlock?()
|
||||
hide()
|
||||
}
|
||||
|
||||
func cleanTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLProgressHUD {
|
||||
class func show(
|
||||
in view: UIView? = UIApplication.shared.keyWindow,
|
||||
timeout: TimeInterval = 100
|
||||
) -> ZLProgressHUD {
|
||||
let hud = ZLProgressHUD(style: ZLPhotoUIConfiguration.default().hudStyle)
|
||||
hud.show(in: view, timeout: timeout)
|
||||
return hud
|
||||
}
|
||||
}
|
||||
67
Pods/ZLPhotoBrowser/Sources/General/ZLProgressView.swift
generated
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// ZLProgressView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/13.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLProgressView: UIView {
|
||||
|
||||
private lazy var progressLayer: CAShapeLayer = {
|
||||
let layer = CAShapeLayer()
|
||||
layer.fillColor = UIColor.clear.cgColor
|
||||
layer.strokeColor = UIColor.white.cgColor
|
||||
layer.lineCap = .round
|
||||
layer.lineWidth = 4
|
||||
return layer
|
||||
}()
|
||||
|
||||
var progress: CGFloat = 0 {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = .clear
|
||||
layer.addSublayer(progressLayer)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
|
||||
let radius = rect.width / 2
|
||||
let end = -(.pi / 2) + (.pi * 2 * progress)
|
||||
progressLayer.frame = bounds
|
||||
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: -(.pi / 2), endAngle: end, clockwise: true)
|
||||
progressLayer.path = path.cgPath
|
||||
}
|
||||
|
||||
}
|
||||
58
Pods/ZLPhotoBrowser/Sources/General/ZLResultModel.swift
generated
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// ZLResultModel.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2022/9/7.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
public class ZLResultModel: NSObject {
|
||||
@objc public let asset: PHAsset
|
||||
|
||||
@objc public let image: UIImage
|
||||
|
||||
/// Whether the picture has been edited. Always false when `saveNewImageAfterEdit = true`.
|
||||
@objc public let isEdited: Bool
|
||||
|
||||
/// Content of the last edit. Always nil when `saveNewImageAfterEdit = true`.
|
||||
@objc public let editModel: ZLEditImageModel?
|
||||
|
||||
/// The order in which the user selects the models in the album. This index is not necessarily equal to the order of the model's index in the array, as some PHAssets requests may fail.
|
||||
@objc public let index: Int
|
||||
|
||||
@objc public init(asset: PHAsset, image: UIImage, isEdited: Bool, editModel: ZLEditImageModel? = nil, index: Int) {
|
||||
self.asset = asset
|
||||
self.image = image
|
||||
self.isEdited = isEdited
|
||||
self.editModel = editModel
|
||||
self.index = index
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLResultModel {
|
||||
static func ==(lhs: ZLResultModel, rhs: ZLResultModel) -> Bool {
|
||||
return lhs.asset == rhs.asset
|
||||
}
|
||||
}
|
||||
300
Pods/ZLPhotoBrowser/Sources/General/ZLThumbnailPhotoCell.swift
generated
Normal file
@@ -0,0 +1,300 @@
|
||||
//
|
||||
// ZLThumbnailPhotoCell.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/12.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
class ZLThumbnailPhotoCell: UICollectionViewCell {
|
||||
private lazy var containerView = UIView()
|
||||
|
||||
private lazy var bottomShadowView = UIImageView(image: .zl.getImage("zl_shadow"))
|
||||
|
||||
private lazy var videoTag = UIImageView(image: .zl.getImage("zl_video"))
|
||||
|
||||
private lazy var livePhotoTag = UIImageView(image: .zl.getImage("zl_livePhoto"))
|
||||
|
||||
private lazy var editImageTag = UIImageView(image: .zl.getImage("zl_editImage_tag"))
|
||||
|
||||
private lazy var descLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .zl.font(ofSize: 13)
|
||||
label.textAlignment = .right
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var progressView: ZLProgressView = {
|
||||
let view = ZLProgressView()
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private var imageIdentifier: String = ""
|
||||
|
||||
private var smallImageRequestID: PHImageRequestID = PHInvalidImageRequestID
|
||||
|
||||
private var bigImageReqeustID: PHImageRequestID = PHInvalidImageRequestID
|
||||
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var btnSelect: ZLEnlargeButton = {
|
||||
let btn = ZLEnlargeButton(type: .custom)
|
||||
btn.setBackgroundImage(.zl.getImage("zl_btn_unselected"), for: .normal)
|
||||
btn.setBackgroundImage(.zl.getImage("zl_btn_selected"), for: .selected)
|
||||
btn.addTarget(self, action: #selector(btnSelectClick), for: .touchUpInside)
|
||||
btn.enlargeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 10, right: 5)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var coverView: UIView = {
|
||||
let view = UIView()
|
||||
view.isUserInteractionEnabled = false
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var indexLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.layer.cornerRadius = 23.0 / 2
|
||||
label.layer.masksToBounds = true
|
||||
label.textColor = .zl.indexLabelTextColor
|
||||
label.backgroundColor = .zl.indexLabelBgColor
|
||||
label.font = .zl.font(ofSize: 14)
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
var enableSelect = true
|
||||
|
||||
var selectedBlock: ((Bool) -> Void)?
|
||||
|
||||
var model: ZLPhotoModel! {
|
||||
didSet {
|
||||
configureCell()
|
||||
}
|
||||
}
|
||||
|
||||
var index: Int = 0 {
|
||||
didSet {
|
||||
indexLabel.text = String(index)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLThumbnailPhotoCell deinit")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setupUI() {
|
||||
contentView.addSubview(imageView)
|
||||
contentView.addSubview(containerView)
|
||||
containerView.addSubview(coverView)
|
||||
containerView.addSubview(btnSelect)
|
||||
btnSelect.addSubview(indexLabel)
|
||||
containerView.addSubview(bottomShadowView)
|
||||
bottomShadowView.addSubview(videoTag)
|
||||
bottomShadowView.addSubview(livePhotoTag)
|
||||
bottomShadowView.addSubview(editImageTag)
|
||||
bottomShadowView.addSubview(descLabel)
|
||||
containerView.addSubview(progressView)
|
||||
|
||||
if ZLPhotoConfiguration.default().showSelectedBorder {
|
||||
layer.borderColor = UIColor.zl.selectedBorderColor.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
imageView.frame = bounds
|
||||
|
||||
containerView.frame = bounds
|
||||
coverView.frame = bounds
|
||||
btnSelect.frame = CGRect(x: bounds.width - 30, y: 8, width: 23, height: 23)
|
||||
indexLabel.frame = btnSelect.bounds
|
||||
bottomShadowView.frame = CGRect(x: 0, y: bounds.height - 25, width: bounds.width, height: 25)
|
||||
videoTag.frame = CGRect(x: 5, y: 1, width: 20, height: 15)
|
||||
livePhotoTag.frame = CGRect(x: 5, y: -1, width: 20, height: 20)
|
||||
editImageTag.frame = CGRect(x: 5, y: -1, width: 20, height: 20)
|
||||
descLabel.frame = CGRect(x: 30, y: 1, width: bounds.width - 35, height: 17)
|
||||
progressView.frame = CGRect(x: (bounds.width - 20) / 2, y: (bounds.height - 20) / 2, width: 20, height: 20)
|
||||
|
||||
super.layoutSubviews()
|
||||
}
|
||||
|
||||
@objc func btnSelectClick() {
|
||||
btnSelect.layer.removeAllAnimations()
|
||||
if !btnSelect.isSelected, ZLPhotoConfiguration.default().animateSelectBtnWhenSelect {
|
||||
btnSelect.layer.add(ZLAnimationUtils.springAnimation(), forKey: nil)
|
||||
}
|
||||
|
||||
selectedBlock?(btnSelect.isSelected)
|
||||
|
||||
if btnSelect.isSelected {
|
||||
fetchBigImage()
|
||||
} else {
|
||||
progressView.isHidden = true
|
||||
cancelFetchBigImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureCell() {
|
||||
if ZLPhotoUIConfiguration.default().cellCornerRadio > 0 {
|
||||
layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
|
||||
if model.type == .video {
|
||||
bottomShadowView.isHidden = false
|
||||
videoTag.isHidden = false
|
||||
livePhotoTag.isHidden = true
|
||||
editImageTag.isHidden = true
|
||||
descLabel.text = model.duration
|
||||
} else if model.type == .gif {
|
||||
bottomShadowView.isHidden = !ZLPhotoConfiguration.default().allowSelectGif
|
||||
videoTag.isHidden = true
|
||||
livePhotoTag.isHidden = true
|
||||
editImageTag.isHidden = true
|
||||
descLabel.text = "GIF"
|
||||
} else if model.type == .livePhoto {
|
||||
bottomShadowView.isHidden = !ZLPhotoConfiguration.default().allowSelectLivePhoto
|
||||
videoTag.isHidden = true
|
||||
livePhotoTag.isHidden = false
|
||||
editImageTag.isHidden = true
|
||||
descLabel.text = "Live"
|
||||
} else {
|
||||
if let _ = model.editImage {
|
||||
bottomShadowView.isHidden = false
|
||||
videoTag.isHidden = true
|
||||
livePhotoTag.isHidden = true
|
||||
editImageTag.isHidden = false
|
||||
descLabel.text = ""
|
||||
} else {
|
||||
bottomShadowView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
let showSelBtn: Bool
|
||||
if ZLPhotoConfiguration.default().maxSelectCount > 1 {
|
||||
if !ZLPhotoConfiguration.default().allowMixSelect {
|
||||
showSelBtn = model.type.rawValue < ZLPhotoModel.MediaType.video.rawValue
|
||||
} else {
|
||||
showSelBtn = true
|
||||
}
|
||||
} else {
|
||||
showSelBtn = ZLPhotoConfiguration.default().showSelectBtnWhenSingleSelect
|
||||
}
|
||||
|
||||
btnSelect.isHidden = !showSelBtn
|
||||
btnSelect.isUserInteractionEnabled = showSelBtn
|
||||
btnSelect.isSelected = model.isSelected
|
||||
|
||||
if model.isSelected {
|
||||
fetchBigImage()
|
||||
} else {
|
||||
cancelFetchBigImage()
|
||||
}
|
||||
|
||||
if let ei = model.editImage {
|
||||
imageView.image = ei
|
||||
} else {
|
||||
fetchSmallImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchSmallImage() {
|
||||
let size: CGSize
|
||||
let maxSideLength = bounds.width * 2
|
||||
if model.whRatio > 1 {
|
||||
let w = maxSideLength * model.whRatio
|
||||
size = CGSize(width: w, height: maxSideLength)
|
||||
} else {
|
||||
let h = maxSideLength / model.whRatio
|
||||
size = CGSize(width: maxSideLength, height: h)
|
||||
}
|
||||
|
||||
if smallImageRequestID > PHInvalidImageRequestID {
|
||||
PHImageManager.default().cancelImageRequest(smallImageRequestID)
|
||||
}
|
||||
|
||||
imageIdentifier = model.ident
|
||||
imageView.image = nil
|
||||
smallImageRequestID = ZLPhotoManager.fetchImage(for: model.asset, size: size, completion: { [weak self] image, isDegraded in
|
||||
if self?.imageIdentifier == self?.model.ident {
|
||||
self?.imageView.image = image
|
||||
}
|
||||
if !isDegraded {
|
||||
self?.smallImageRequestID = PHInvalidImageRequestID
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func fetchBigImage() {
|
||||
cancelFetchBigImage()
|
||||
|
||||
bigImageReqeustID = ZLPhotoManager.fetchOriginalImageData(for: model.asset, progress: { [weak self] progress, _, _, _ in
|
||||
if self?.model.isSelected == true {
|
||||
self?.progressView.isHidden = false
|
||||
self?.progressView.progress = max(0.1, progress)
|
||||
self?.imageView.alpha = 0.5
|
||||
if progress >= 1 {
|
||||
self?.resetProgressViewStatus()
|
||||
}
|
||||
} else {
|
||||
self?.cancelFetchBigImage()
|
||||
}
|
||||
}, completion: { [weak self] _, _, _ in
|
||||
self?.resetProgressViewStatus()
|
||||
})
|
||||
}
|
||||
|
||||
private func cancelFetchBigImage() {
|
||||
if bigImageReqeustID > PHInvalidImageRequestID {
|
||||
PHImageManager.default().cancelImageRequest(bigImageReqeustID)
|
||||
}
|
||||
resetProgressViewStatus()
|
||||
}
|
||||
|
||||
private func resetProgressViewStatus() {
|
||||
progressView.isHidden = true
|
||||
imageView.alpha = 1
|
||||
}
|
||||
|
||||
}
|
||||
1685
Pods/ZLPhotoBrowser/Sources/General/ZLThumbnailViewController.swift
generated
Normal file
233
Pods/ZLPhotoBrowser/Sources/General/ZLVideoManager.swift
generated
Normal file
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// ZLVideoManager.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/9/23.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import Photos
|
||||
|
||||
public class ZLVideoManager: NSObject {
|
||||
class func getVideoExportFilePath(format: String? = nil) -> String {
|
||||
let format = format ?? ZLPhotoConfiguration.default().cameraConfiguration.videoExportType.format
|
||||
return NSTemporaryDirectory().appendingFormat("%@.%@", UUID().uuidString, format)
|
||||
}
|
||||
|
||||
class func exportEditVideo(for asset: AVAsset, range: CMTimeRange, complete: @escaping ((URL?, Error?) -> Void)) {
|
||||
let type: ZLVideoManager.ExportType = ZLPhotoConfiguration.default().cameraConfiguration.videoExportType == .mov ? .mov : .mp4
|
||||
exportVideo(for: asset, range: range, exportType: type, presetName: AVAssetExportPresetPassthrough) { url, error in
|
||||
if url != nil {
|
||||
complete(url!, error)
|
||||
} else {
|
||||
complete(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 没有针对不同分辨率视频做处理,仅用于处理相机拍照的视频
|
||||
@objc public class func mergeVideos(fileUrls: [URL], completion: @escaping ((URL?, Error?) -> Void)) {
|
||||
let composition = AVMutableComposition()
|
||||
let assets = fileUrls.map { AVURLAsset(url: $0) }
|
||||
|
||||
var insertTime: CMTime = .zero
|
||||
var assetVideoTracks: [AVAssetTrack] = []
|
||||
|
||||
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID())!
|
||||
let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID())!
|
||||
|
||||
for asset in assets {
|
||||
do {
|
||||
let timeRange = CMTimeRangeMake(start: .zero, duration: asset.duration)
|
||||
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
||||
try compositionVideoTrack.insertTimeRange(
|
||||
timeRange,
|
||||
of: videoTrack,
|
||||
at: insertTime
|
||||
)
|
||||
|
||||
assetVideoTracks.append(videoTrack)
|
||||
}
|
||||
|
||||
if let audioTrack = asset.tracks(withMediaType: .audio).first {
|
||||
try compositionAudioTrack.insertTimeRange(
|
||||
timeRange,
|
||||
of: audioTrack,
|
||||
at: insertTime
|
||||
)
|
||||
}
|
||||
|
||||
insertTime = CMTimeAdd(insertTime, asset.duration)
|
||||
} catch {
|
||||
completion(nil, NSError.videoMergeError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard assetVideoTracks.count == assets.count else {
|
||||
completion(nil, NSError.videoMergeError)
|
||||
return
|
||||
}
|
||||
|
||||
let renderSize = getNaturalSize(videoTrack: assetVideoTracks[0])
|
||||
|
||||
let videoComposition = AVMutableVideoComposition()
|
||||
videoComposition.instructions = getInstructions(compositionTrack: compositionVideoTrack, assetVideoTracks: assetVideoTracks, assets: assets)
|
||||
videoComposition.frameDuration = assetVideoTracks[0].minFrameDuration
|
||||
videoComposition.renderSize = renderSize
|
||||
videoComposition.renderScale = 1
|
||||
|
||||
guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPreset1280x720) else {
|
||||
completion(nil, NSError.videoMergeError)
|
||||
return
|
||||
}
|
||||
|
||||
let outputUrl = URL(fileURLWithPath: ZLVideoManager.getVideoExportFilePath())
|
||||
exportSession.outputURL = outputUrl
|
||||
exportSession.shouldOptimizeForNetworkUse = true
|
||||
exportSession.outputFileType = ZLPhotoConfiguration.default().cameraConfiguration.videoExportType.avFileType
|
||||
exportSession.videoComposition = videoComposition
|
||||
exportSession.exportAsynchronously(completionHandler: {
|
||||
let suc = exportSession.status == .completed
|
||||
if exportSession.status == .failed {
|
||||
zl_debugPrint("ZLPhotoBrowser: video merge failed: \(exportSession.error?.localizedDescription ?? "")")
|
||||
}
|
||||
ZLMainAsync {
|
||||
completion(suc ? outputUrl : nil, exportSession.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static func getNaturalSize(videoTrack: AVAssetTrack) -> CGSize {
|
||||
var size = videoTrack.naturalSize
|
||||
if isPortraitVideoTrack(videoTrack) {
|
||||
swap(&size.width, &size.height)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
private static func getInstructions(
|
||||
compositionTrack: AVMutableCompositionTrack,
|
||||
assetVideoTracks: [AVAssetTrack],
|
||||
assets: [AVURLAsset]
|
||||
) -> [AVMutableVideoCompositionInstruction] {
|
||||
var instructions: [AVMutableVideoCompositionInstruction] = []
|
||||
|
||||
var start: CMTime = .zero
|
||||
for (index, videoTrack) in assetVideoTracks.enumerated() {
|
||||
let asset = assets[index]
|
||||
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionTrack)
|
||||
layerInstruction.setTransform(videoTrack.preferredTransform, at: .zero)
|
||||
|
||||
let instruction = AVMutableVideoCompositionInstruction()
|
||||
instruction.timeRange = CMTimeRangeMake(start: start, duration: asset.duration)
|
||||
instruction.layerInstructions = [layerInstruction]
|
||||
instructions.append(instruction)
|
||||
|
||||
start = CMTimeAdd(start, asset.duration)
|
||||
}
|
||||
|
||||
return instructions
|
||||
}
|
||||
|
||||
private static func isPortraitVideoTrack(_ track: AVAssetTrack) -> Bool {
|
||||
let transform = track.preferredTransform
|
||||
let tfA = transform.a
|
||||
let tfB = transform.b
|
||||
let tfC = transform.c
|
||||
let tfD = transform.d
|
||||
|
||||
if (tfA == 0 && tfB == 1 && tfC == -1 && tfD == 0) ||
|
||||
(tfA == 0 && tfB == 1 && tfC == 1 && tfD == 0) ||
|
||||
(tfA == 0 && tfB == -1 && tfC == 1 && tfD == 0) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: export methods
|
||||
|
||||
public extension ZLVideoManager {
|
||||
@objc class func exportVideo(for asset: PHAsset, exportType: ZLVideoManager.ExportType = .mov, presetName: String = AVAssetExportPresetMediumQuality, complete: @escaping ((URL?, Error?) -> Void)) {
|
||||
guard asset.mediaType == .video else {
|
||||
complete(nil, NSError.videoExportTypeError)
|
||||
return
|
||||
}
|
||||
|
||||
_ = ZLPhotoManager.fetchAVAsset(forVideo: asset) { avAsset, _ in
|
||||
if let set = avAsset {
|
||||
self.exportVideo(for: set, exportType: exportType, presetName: presetName, complete: complete)
|
||||
} else {
|
||||
complete(nil, NSError.videoExportError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc class func exportVideo(for asset: AVAsset, range: CMTimeRange = CMTimeRange(start: .zero, duration: .positiveInfinity), exportType: ZLVideoManager.ExportType = .mov, presetName: String = AVAssetExportPresetMediumQuality, complete: @escaping ((URL?, Error?) -> Void)) {
|
||||
let outputUrl = URL(fileURLWithPath: getVideoExportFilePath(format: exportType.format))
|
||||
guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
|
||||
complete(nil, NSError.videoExportError)
|
||||
return
|
||||
}
|
||||
exportSession.outputURL = outputUrl
|
||||
exportSession.outputFileType = exportType.avFileType
|
||||
exportSession.timeRange = range
|
||||
|
||||
exportSession.exportAsynchronously(completionHandler: {
|
||||
let suc = exportSession.status == .completed
|
||||
if exportSession.status == .failed {
|
||||
zl_debugPrint("ZLPhotoBrowser: video export failed: \(exportSession.error?.localizedDescription ?? "")")
|
||||
}
|
||||
ZLMainAsync {
|
||||
complete(suc ? outputUrl : nil, exportSession.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLVideoManager {
|
||||
@objc enum ExportType: Int {
|
||||
var format: String {
|
||||
switch self {
|
||||
case .mov:
|
||||
return "mov"
|
||||
case .mp4:
|
||||
return "mp4"
|
||||
}
|
||||
}
|
||||
|
||||
var avFileType: AVFileType {
|
||||
switch self {
|
||||
case .mov:
|
||||
return .mov
|
||||
case .mp4:
|
||||
return .mp4
|
||||
}
|
||||
}
|
||||
|
||||
case mov
|
||||
case mp4
|
||||
}
|
||||
}
|
||||
50
Pods/ZLPhotoBrowser/Sources/General/ZLWeakProxy.swift
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// ZLWeakProxy.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/3/10.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLWeakProxy: NSObject {
|
||||
|
||||
private weak var target: NSObjectProtocol?
|
||||
|
||||
init(target: NSObjectProtocol) {
|
||||
self.target = target
|
||||
super.init()
|
||||
}
|
||||
|
||||
class func proxy(withTarget target: NSObjectProtocol) -> ZLWeakProxy {
|
||||
return ZLWeakProxy(target: target)
|
||||
}
|
||||
|
||||
override func forwardingTarget(for aSelector: Selector!) -> Any? {
|
||||
return target
|
||||
}
|
||||
|
||||
override func responds(to aSelector: Selector!) -> Bool {
|
||||
return target?.responds(to: aSelector) ?? false
|
||||
}
|
||||
|
||||
}
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/ar.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "كاميرا";
|
||||
"previewCameraRecord" = "تسجيل";
|
||||
"previewAlbum" = "البوم";
|
||||
"cancel" = "إلغاء";
|
||||
|
||||
"originalPhoto" = "الصورة الكاملة";
|
||||
"done" = "تم";
|
||||
"ok" = "حسنا";
|
||||
"editFinish" = "تم";
|
||||
|
||||
"back" = "رجوع";
|
||||
"edit" = "تعديل";
|
||||
"revert" = "أعاد";
|
||||
"brightness" = "السطوع";
|
||||
"contrast" = "التباين";
|
||||
"saturation" = "التشبع";
|
||||
|
||||
"photo" = "الصور";
|
||||
"preview" = "معاينة";
|
||||
|
||||
"noPhotoTips" = "لا توجد صور";
|
||||
|
||||
"hudLoading" = "يرجى الانتظار...";
|
||||
|
||||
"exceededMaxSelectCount" = "أقصى عدد للاختيار: %ld";
|
||||
"longerThanMaxVideoDuration" = "لا يمكن تحديد مقاطع فيديو أطول من %lds";
|
||||
"shorterThanMinVideoDuration" = "لا يمكن تحديد مقاطع فيديو أقصر من %lds";
|
||||
"largerThanMaxVideoDataSize" = "لا يمكن تحديد مقاطع فيديو أكبر من %@ ميغا بايت";
|
||||
"smallerThanMinVideoDataSize" = "لا يمكن تحديد مقاطع فيديو أصغر من %@ ميغا بايت";
|
||||
"exceededMaxVideoSelectCount" = "أقصى عدد لتحديد الفيديو: %ld";
|
||||
"lessThanMinVideoSelectCount" = "الحد الأدنى لتحديد الفيديو: %ld";
|
||||
|
||||
"noCameraAuthority" = "يُرجى السماح لـ %@ بالوصول إلى كاميرا جهازك في الإعدادات > الخصوصية > الكاميرا ";
|
||||
"noPhotoLibratyAuthority" = "يُرجى السماح لـ %@ بالوصول إلى ألبومك في الإعدادات > الخصوصية > الكاميرا ";
|
||||
"noMicrophoneAuthority" = "تعذر تسجيل الصوت. انتقل إلى الإعدادات > %@ وقم بتمكين الوصول إلى الميكروفون";
|
||||
"cameraUnavailable" = "الكاميرا غير متوفرة";
|
||||
"keepRecording" = "استمر في التسجيل";
|
||||
"gotoSettings" = "اذهب للاعدادات";
|
||||
|
||||
"iCloudVideoLoadFaild" = "غير قادر على المزامنة من iCloud";
|
||||
"imageLoadFailed" = "فشل التحميل";
|
||||
|
||||
"save" = "حفظ";
|
||||
"saveImageError" = "فشل حفظ الصورة";
|
||||
"saveVideoError" = "فشل حفظ الفيديو";
|
||||
"timeout" = "الطلب منتهي المدة";
|
||||
|
||||
"customCameraTips" = "انقر لالتقاط صورة مع الاستمرار لتسجيل الفيديو";
|
||||
"customCameraTakePhotoTips" = "انقر لالتقاط صورة";
|
||||
"customCameraRecordVideoTips" = "اضغط مع الاستمرار لتسجيل الفيديو";
|
||||
"minRecordTimeTips" = "سجل على الأقل %lds";
|
||||
|
||||
"cameraRoll" = "حديثا";
|
||||
"panoramas" = "بانوراما";
|
||||
"videos" = "فيديوات";
|
||||
"favorites" = "المفضلة";
|
||||
"timelapses" = "الفاصل الزمني";
|
||||
"recentlyAdded" = "أضيف مؤخرا";
|
||||
"bursts" = "رشقات نارية";
|
||||
"slomoVideos" = "حركة بطيئة";
|
||||
"selfPortraits" = "سيلفي";
|
||||
"screenshots" = "لقطات الشاشة";
|
||||
"depthEffect" = "لَوحَة";
|
||||
"livePhotos" = "صور حية";
|
||||
"animated" = "متحرك";
|
||||
"myPhotoStream" = "دفق الصور الخاص بي";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "جميع الصور";
|
||||
"unableToAccessAllPhotos" = "يتعذر الوصول إلى جميع الصور في الألبوم ، يرجى السماح بالوصول إلى كل الصور في الصور .";
|
||||
"textStickerRemoveTips" = "اسحب هنا للإزالة";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/de.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Kamera";
|
||||
"previewCameraRecord" = "Aufzeichnung";
|
||||
"previewAlbum" = "Album";
|
||||
"cancel" = "Abbrechen";
|
||||
|
||||
"originalPhoto" = "Vollbild";
|
||||
"done" = "Erledigt";
|
||||
"ok" = "in Ordnung";
|
||||
"editFinish" = "Fertig";
|
||||
|
||||
"back" = "Zurück";
|
||||
"edit" = "Bearbeiten";
|
||||
"revert" = "Rückgängig";
|
||||
"brightness" = "Helligkeit";
|
||||
"contrast" = "Kontrast";
|
||||
"saturation" = "Sättigung";
|
||||
|
||||
"photo" = "Fotos";
|
||||
"preview" = "Vorschau";
|
||||
|
||||
"noPhotoTips" = "Keine Fotos";
|
||||
|
||||
"hudLoading" = "Bitte warten...";
|
||||
|
||||
"exceededMaxSelectCount" = "Maximale Auswahlanzahl: %ld";
|
||||
"longerThanMaxVideoDuration" = "Videos, die länger als %lds sind, können nicht ausgewählt werden";
|
||||
"shorterThanMinVideoDuration" = "Videos kürzer als %lds können nicht ausgewählt werden";
|
||||
"largerThanMaxVideoDataSize" = "Videos größer als %@MB können nicht ausgewählt werden";
|
||||
"smallerThanMinVideoDataSize" = "Videos kleiner als %@MB können nicht ausgewählt werden";
|
||||
"exceededMaxVideoSelectCount" = "Video max Auswahlanzahl: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Video min Auswahlanzahl: %ld";
|
||||
|
||||
"noCameraAuthority" = "Bitte erlauben Sie %@, auf die Kamera Ihres Geräts unter \"Einstellungen\" > \"Datenschutz\" > \"Kamera\" zuzugreifen";
|
||||
"noPhotoLibratyAuthority" = "Bitte erlauben Sie %@, auf Ihr Album unter \"Einstellungen\" > \"Datenschutz\" > \"Fotos\" zuzugreifen";
|
||||
"noMicrophoneAuthority" = "Audio kann nicht aufgenommen werden. Gehen Sie zu \"Einstellungen\" > \"%@\" und aktivieren Sie den Mikrofonzugriff";
|
||||
"cameraUnavailable" = "Kamera ist nicht verfügbar";
|
||||
"keepRecording" = "Aufnahme behalten";
|
||||
"gotoSettings" = "Zu Einstellungen wechseln";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Synchronisierung von iCloud nicht möglich";
|
||||
"imageLoadFailed" = "Laden fehlgeschlagen";
|
||||
|
||||
"save" = "Sparen";
|
||||
"saveImageError" = "Das Bild konnte nicht gespeichert werden";
|
||||
"saveVideoError" = "Das Video konnte nicht gespeichert werden";
|
||||
"timeout" = "Zeitüberschreitung der Anforderung";
|
||||
|
||||
"customCameraTips" = "Tippen, um Fotos aufzunehmen und halten, um ein Video aufzunehmen";
|
||||
"customCameraTakePhotoTips" = "Tippen, um ein Foto aufzunehmen";
|
||||
"customCameraRecordVideoTips" = "Halten Sie gedrückt, um ein Video aufzunehmen";
|
||||
"minRecordTimeTips" = "Nehmen Sie mindestens %lds auf";
|
||||
|
||||
"cameraRoll" = "Letzte";
|
||||
"panoramas" = "Panoramen";
|
||||
"videos" = "Videos";
|
||||
"favorites" = "Favoriten";
|
||||
"timelapses" = "Zeitraffer";
|
||||
"recentlyAdded" = "Kürzlich hinzugefügt";
|
||||
"bursts" = "Serien";
|
||||
"slomoVideos" = "Slo-Mo";
|
||||
"selfPortraits" = "Selfies";
|
||||
"screenshots" = "Bildschirmfotos";
|
||||
"depthEffect" = "Porträt";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Animiert";
|
||||
"myPhotoStream" = "Mein Fotostream";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Alle Fotos";
|
||||
"unableToAccessAllPhotos" = "Zugriff auf alle Fotos im Album nicht möglich.\nZugriff auf \"Alle Fotos\" unter \"Fotos\" zulassen.";
|
||||
"textStickerRemoveTips" = "Zum Entfernen hierher ziehen";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/en.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Camera";
|
||||
"previewCameraRecord" = "Record";
|
||||
"previewAlbum" = "Album";
|
||||
"cancel" = "Cancel";
|
||||
|
||||
"originalPhoto" = "Full Image";
|
||||
"done" = "Done";
|
||||
"ok" = "OK";
|
||||
"editFinish" = "Done";
|
||||
|
||||
"back" = "Back";
|
||||
"edit" = "Edit";
|
||||
"revert" = "Undo";
|
||||
"brightness" = "Brightness";
|
||||
"contrast" = "Contrast";
|
||||
"saturation" = "Saturation";
|
||||
|
||||
"photo" = "Photos";
|
||||
"preview" = "Preview";
|
||||
|
||||
"noPhotoTips" = "No Photos";
|
||||
|
||||
"hudLoading" = "waiting...";
|
||||
|
||||
"exceededMaxSelectCount" = "Max count for selection: %ld";
|
||||
"longerThanMaxVideoDuration" = "Can't select videos longer than %lds";
|
||||
"shorterThanMinVideoDuration" = "Can't select videos shorter than %lds";
|
||||
"largerThanMaxVideoDataSize" = "Can't select videos larger than %@MB";
|
||||
"smallerThanMinVideoDataSize" = "Can't select videos smaller than %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Max count for video selection: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Min count for video selection: %ld";
|
||||
|
||||
"noCameraAuthority" = "Please allow %@ to access your device's camera in \"Settings\" > \"Privacy\" > \"Camera\"";
|
||||
"noPhotoLibratyAuthority" = "Please allow %@ to access your album in \"Settings\" > \"Privacy\" > \"Photos\"";
|
||||
"noMicrophoneAuthority" = "Unable to record audio. Go to \"Settings\" > \"%@\" and enable microphone access";
|
||||
"cameraUnavailable" = "Camera is unavailable";
|
||||
"keepRecording" = "Keep Recording";
|
||||
"gotoSettings" = "Go to Settings";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Unable to sync from iCloud";
|
||||
"imageLoadFailed" = "loading failed";
|
||||
|
||||
"save" = "Save";
|
||||
"saveImageError" = "Failed to save the image";
|
||||
"saveVideoError" = "Failed to save the video";
|
||||
"timeout" = "Request timed out";
|
||||
|
||||
"customCameraTips" = "Tap to take photo and hold to record video";
|
||||
"customCameraTakePhotoTips" = "Tap to take photo";
|
||||
"customCameraRecordVideoTips" = "Hold to record video";
|
||||
"minRecordTimeTips" = "Record at least %lds";
|
||||
|
||||
"cameraRoll" = "Recents";
|
||||
"panoramas" = "Panoramas";
|
||||
"videos" = "Videos";
|
||||
"favorites" = "Favorites";
|
||||
"timelapses" = "Time-Lapse";
|
||||
"recentlyAdded" = "Recently Added";
|
||||
"bursts" = "Bursts";
|
||||
"slomoVideos" = "Slo-mo";
|
||||
"selfPortraits" = "Selfies";
|
||||
"screenshots" = "Screenshots";
|
||||
"depthEffect" = "Portrait";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Animated";
|
||||
"myPhotoStream" = "My Photo Stream";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "All Photos";
|
||||
"unableToAccessAllPhotos" = "Unable to access all photos in the album.\nAllow access to \"All Photos\" in \"Photos\".";
|
||||
"textStickerRemoveTips" = "Drag here to remove";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/es-419.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Cámara";
|
||||
"previewCameraRecord" = "Grabar";
|
||||
"previewAlbum" = "Álbum";
|
||||
"cancel" = "Cancelar";
|
||||
|
||||
"originalPhoto" = "Imagen completa";
|
||||
"done" = "Hecho";
|
||||
"ok" = "OK";
|
||||
"editFinish" = "Hecho";
|
||||
|
||||
"back" = "Volver";
|
||||
"edit" = "Editar";
|
||||
"revert" = "Deshacer";
|
||||
"brightness" = "Brillo";
|
||||
"contrast" = "Contraste";
|
||||
"saturation" = "Saturación";
|
||||
|
||||
"photo" = "Fotos";
|
||||
"preview" = "Vista previa";
|
||||
|
||||
"noPhotoTips" = "No hay fotos";
|
||||
|
||||
"hudLoading" = "cargando...";
|
||||
|
||||
"exceededMaxSelectCount" = "Número máximo para la selección: %ld";
|
||||
"longerThanMaxVideoDuration" = "No se puede seleccionar un vídeo con una duración superior a %lds";
|
||||
"shorterThanMinVideoDuration" = "No se puede seleccionar un vídeo con una duración inferior a %lds";
|
||||
"largerThanMaxVideoDataSize" = "No se pueden seleccionar vídeos de más de %@MB";
|
||||
"smallerThanMinVideoDataSize" = "No se pueden seleccionar vídeos de menos de %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Número máximo para la selección de vídeos: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Número mínimo para la selección de vídeos: %ld";
|
||||
|
||||
"noCameraAuthority" = "Permite que %@ acceda a la cámara de tu dispositivo en \"Ajustes\" > \"Privacidad\" > \"Cámara\"";
|
||||
"noPhotoLibratyAuthority" = "Permita que %@ acceda a su álbum en \"Configuración\" > \"Privacidad\" > \"Fotos\"";
|
||||
"noMicrophoneAuthority" = "No se puede grabar audio. Ve a \"Ajustes\" > \"%@\" y activa el acceso al micrófono";
|
||||
"cameraUnavailable" = "La cámara no está disponible";
|
||||
"keepRecording" = "Continuar Grabando";
|
||||
"gotoSettings" = "Ir a Ajustes";
|
||||
|
||||
"iCloudVideoLoadFaild" = "No se puede sincronizar desde iCloud";
|
||||
"imageLoadFailed" = "carga fallida";
|
||||
|
||||
"save" = "Guardar";
|
||||
"saveImageError" = "No se ha podido guardar la imagen";
|
||||
"saveVideoError" = "No se ha podido guardar el vídeo";
|
||||
"timeout" = "La solicitud se ha vencido";
|
||||
|
||||
"customCameraTips" = "Toca para tomar una foto y mantén pulsado para grabar un vídeo";
|
||||
"customCameraTakePhotoTips" = "Toca para tomar una foto";
|
||||
"customCameraRecordVideoTips" = "Mantén pulsado para grabar vídeo";
|
||||
"minRecordTimeTips" = "Grabar al menos %lds";
|
||||
|
||||
"cameraRoll" = "Recientes";
|
||||
"panoramas" = "Panoramas";
|
||||
"videos" = "Vídeos";
|
||||
"favorites" = "Favoritos";
|
||||
"timelapses" = "Lapso de Tiempo";
|
||||
"recentlyAdded" = "Añadido recientemente";
|
||||
"bursts" = "Ráfagas";
|
||||
"slomoVideos" = "Cámara lenta";
|
||||
"selfPortraits" = "Selfies";
|
||||
"screenshots" = "Capturas de pantalla";
|
||||
"depthEffect" = "Retrato";
|
||||
"livePhotos" = "Fotos en vivo";
|
||||
"animated" = "Animado";
|
||||
"myPhotoStream" = "Mi flujo de fotos";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Todas las fotos";
|
||||
"unableToAccessAllPhotos" = "No se puede acceder a todas las fotos del álbum.\nPermite el acceso a \"Todas las fotos\" en \"Fotos\".";
|
||||
"textStickerRemoveTips" = "Arrastra aquí para eliminar";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/fr.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Caméra";
|
||||
"previewCameraRecord" = "Record";
|
||||
"previewAlbum" = "Album";
|
||||
"cancel" = "Annuler";
|
||||
|
||||
"originalPhoto" = "image complète";
|
||||
"done" = "Terminé";
|
||||
"ok" = "D'accord";
|
||||
"editFinish" = "Terminé";
|
||||
|
||||
"back" = "Retour";
|
||||
"edit" = "Modifier";
|
||||
"revert" = "Annuler";
|
||||
"brightness" = "Luminosité";
|
||||
"contrast" = "Contraste";
|
||||
"saturation" = "Saturation";
|
||||
|
||||
"photo" = "Photos";
|
||||
"preview" = "Aperçu";
|
||||
|
||||
"noPhotoTips" = "Pas de photos";
|
||||
|
||||
"hudLoading" = "attendre...";
|
||||
|
||||
"exceededMaxSelectCount" = "Nombre maximal de sélections: %ld";
|
||||
"longerThanMaxVideoDuration" = "Impossible de sélectionner une vidéo d'une durée supérieure à %lds";
|
||||
"shorterThanMinVideoDuration" = "Impossible de sélectionner une vidéo d'une durée inférieure à %lds";
|
||||
"largerThanMaxVideoDataSize" = "Impossible de sélectionner des vidéos de plus de %@Mo";
|
||||
"smallerThanMinVideoDataSize" = "Impossible de sélectionner des vidéos de moins de %@Mo";
|
||||
"exceededMaxVideoSelectCount" = "Nombre maximal de sélections vidéo: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Nombre minimal de sélection de vidéo: %ld";
|
||||
|
||||
"noCameraAuthority" = "Veuillez autoriser %@ à accéder à la caméra de votre appareil dans \"Paramètres\" > \"Confidentialité\" > \"Caméra\"";
|
||||
"noPhotoLibratyAuthority" = "Veuillez autoriser %@ à accéder à votre album dans \"Paramètres\" > \"Confidentialité\" > \"Photos\"";
|
||||
"noMicrophoneAuthority" = "Impossible d'enregistrer le son. Rendez-vous dans « Paramètres >> > << %@ » et activez l'accès au microphone";
|
||||
"cameraUnavailable" = "La caméra n'est pas disponible";
|
||||
"keepRecording" = "Continuer à enregistrer";
|
||||
"gotoSettings" = "Accéder à Paramètres";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Impossible de synchroniser depuis iCloud";
|
||||
"imageLoadFailed" = "chargement échoué";
|
||||
|
||||
"save" = "Enregistrer";
|
||||
"saveImageError" = "Échec de l'enregistrement de l'image";
|
||||
"saveVideoError" = "Échec de l'enregistrement de la vidéo";
|
||||
"timeout" = "La demande a expiré";
|
||||
|
||||
"customCameraTips" = "Maintenez la pression sur pour enregistrer";
|
||||
"customCameraTakePhotoTips" = "Appuyez pour prendre une photo";
|
||||
"customCameraRecordVideoTips" = "Maintenez enfoncé pour enregistrer une vidéo";
|
||||
"minRecordTimeTips" = "Enregistrez au moins %lds";
|
||||
|
||||
"cameraRoll" = "Récents";
|
||||
"panoramas" = "Panoramas";
|
||||
"videos" = "Vidéos";
|
||||
"favorites" = "Favorites";
|
||||
"timelapses" = "Accéléré";
|
||||
"recentlyAdded" = "Récemment ajouté";
|
||||
"bursts" = "Rafales";
|
||||
"slomoVideos" = "Ralentis";
|
||||
"selfPortraits" = "Selfies";
|
||||
"screenshots" = "Captures d'écran";
|
||||
"depthEffect" = "Portrait";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Animations";
|
||||
"myPhotoStream" = "Mon flux de photos";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Toutes les photos";
|
||||
"unableToAccessAllPhotos" = "Impossible d'accéder à toutes les photos de l'album.\nAutorisez l'accès à « Toutes les photos » dans « Photos ».";
|
||||
"textStickerRemoveTips" = "Faites glisser ici pour supprimer";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/id.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Kamera";
|
||||
"previewCameraRecord" = "Merekam";
|
||||
"previewAlbum" = "Album";
|
||||
"cancel" = "Membatalkan";
|
||||
|
||||
"originalPhoto" = "Gambar Penuh";
|
||||
"done" = "Selesai";
|
||||
"ok" = "Oke";
|
||||
"editFinish" = "Selesai";
|
||||
|
||||
"back" = "Kembali";
|
||||
"edit" = "Edit";
|
||||
"revert" = "Batalkan";
|
||||
"brightness" = "Kecerahan";
|
||||
"contrast" = "Kontras";
|
||||
"saturation" = "Saturasi";
|
||||
|
||||
"photo" = "Foto";
|
||||
"preview" = "Pratinjau";
|
||||
|
||||
"noPhotoTips" = "Tidak ada fotos";
|
||||
|
||||
"hudLoading" = "menunggu...";
|
||||
|
||||
"exceededMaxSelectCount" = "Jumlah maksimum untuk seleksi: %ld";
|
||||
"longerThanMaxVideoDuration" = "Tidak dapat memilih video dengan durasi lebih dari %ld detik";
|
||||
"shorterThanMinVideoDuration" = "Tidak dapat memilih video dengan durasi lebih pendek dari %ld detik";
|
||||
"largerThanMaxVideoDataSize" = "Tidak dapat memilih video yang lebih besar dari %@MB";
|
||||
"smallerThanMinVideoDataSize" = "Tidak dapat memilih video yang lebih kecil dari %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Jumlah maksimum untuk pemilihan video: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Jumlah minimum untuk pemilihan video: %ld";
|
||||
|
||||
"noCameraAuthority" = "Izinkan akses kamera di \"Pengaturan\" > \"%@\" iPhone Anda.";
|
||||
"noPhotoLibratyAuthority" = "Izinkan %@ mengakses album Anda di \"Setelan\" > \"Privasi\" > \"Foto\"";
|
||||
"noMicrophoneAuthority" = "Tidak dapat merekam audio. Buka \"Setelan\" > \"%@\" dan aktifkan akses mikrofon";
|
||||
"cameraUnavailable" = "Kamera tidak tersedia";
|
||||
"keepRecording" = "Terus Merekam";
|
||||
"gotoSettings" = "Pergi ke pengaturan";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Tidak dapat menyinkronkan dari iCloud";
|
||||
"imageLoadFailed" = "Gagal Memuat";
|
||||
|
||||
"save" = "Menghemat";
|
||||
"saveImageError" = "Gagal menyimpan gambar";
|
||||
"saveVideoError" = "Gagal menyimpan video";
|
||||
"timeout" = "Waktu permintaan habis";
|
||||
|
||||
"customCameraTips" = "Ketuk untuk mengambil video dan tahan untuk merekam";
|
||||
"customCameraTakePhotoTips" = "Ketuk untuk mengambil foto";
|
||||
"customCameraRecordVideoTips" = "Tahan untuk merekam video";
|
||||
"minRecordTimeTips" = "Merekam setidaknya %ld detik";
|
||||
|
||||
"cameraRoll" = "Terbaru";
|
||||
"panoramas" = "Panorama";
|
||||
"videos" = "Video";
|
||||
"favorites" = "Favorit";
|
||||
"timelapses" = "Selang Waktu";
|
||||
"recentlyAdded" = "Terkini";
|
||||
"bursts" = "Foto Beruntun";
|
||||
"slomoVideos" = "Slo-mo";
|
||||
"selfPortraits" = "Selfie";
|
||||
"screenshots" = "Jepretan Layer";
|
||||
"depthEffect" = "Potret";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Animasi";
|
||||
"myPhotoStream" = "Aliran Foto Saya";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Semua Foto";
|
||||
"unableToAccessAllPhotos" = "Tidak dapat mengakses semua foto dalam album.\nIzinkan akses ke \"Semua Foto\" di \"Foto\".";
|
||||
"textStickerRemoveTips" = "Seret ke sini untuk menghapus";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/it.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Fotocamera";
|
||||
"previewCameraRecord" = "Disco";
|
||||
"previewAlbum" = "Immagini";
|
||||
"cancel" = "Annulla";
|
||||
|
||||
"originalPhoto" = "Immagine completa";
|
||||
"done" = "Fine";
|
||||
"ok" = "OK";
|
||||
"editFinish" = "Fine";
|
||||
|
||||
"back" = "Indietro";
|
||||
"edit" = "Modifica";
|
||||
"revert" = "Annulla";
|
||||
"brightness" = "Luminosità";
|
||||
"contrast" = "Contrasto";
|
||||
"saturation" = "Saturazione";
|
||||
|
||||
"photo" = "Fotografie";
|
||||
"preview" = "Anteprima";
|
||||
|
||||
"noPhotoTips" = "Niente fotos";
|
||||
|
||||
"hudLoading" = "in attesa...";
|
||||
|
||||
"exceededMaxSelectCount" = "Conteggio massimo per la selezione: %ld";
|
||||
"longerThanMaxVideoDuration" = "Impossibile selezionare video con una durata superiore a %lds";
|
||||
"shorterThanMinVideoDuration" = "Impossibile selezionare video con una durata inferiore a %lds";
|
||||
"largerThanMaxVideoDataSize" = "Impossibile selezionare video di dimensioni superiori a %@MB";
|
||||
"smallerThanMinVideoDataSize" = "Impossibile selezionare video di dimensioni inferiori a %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Conteggio massimo per la selezione dei video: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Conteggio minimo per la selezione del video: %ld";
|
||||
|
||||
"noCameraAuthority" = "Consenti a %@ di accedere alla fotocamera del tuo dispositivo in \"Impostazioni\" > \"Privacy\" > \"Fotocamera\"";
|
||||
"noPhotoLibratyAuthority" = "Consenti a %@ di accedere al tuo album in \"Impostazioni\" > \"Privacy\" > \"Foto\"";
|
||||
"noMicrophoneAuthority" = "Impossibile registrare I'audio. Vai a \"Impostazioni\" > \"%@\" e attiva I'accesso al microfono";
|
||||
"cameraUnavailable" = "La fotocamera non è disponibile";
|
||||
"keepRecording" = "Continua a registrare";
|
||||
"gotoSettings" = "Vai a lmpostazioni";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Impossibile sincronizzare da iCloud";
|
||||
"imageLoadFailed" = "Caricamento fallito";
|
||||
|
||||
"save" = "Salva";
|
||||
"saveImageError" = "Impossibile salvare l'immagine";
|
||||
"saveVideoError" = "Impossibile salvare il video";
|
||||
"timeout" = "Tempo scaduto per la richiesta";
|
||||
|
||||
"customCameraTips" = "Toccare per scattare e tiene premuto per registrare";
|
||||
"customCameraTakePhotoTips" = "Tocca per scattare una foto";
|
||||
"customCameraRecordVideoTips" = "Tieni premuto per registrare il video";
|
||||
"minRecordTimeTips" = "Registra almeno %lds";
|
||||
|
||||
"cameraRoll" = "Recenti";
|
||||
"panoramas" = "Panoramiche";
|
||||
"videos" = "Video";
|
||||
"favorites" = "Preferiti";
|
||||
"timelapses" = "Time-lapse";
|
||||
"recentlyAdded" = "Aggiunto recentemente";
|
||||
"bursts" = "Sequenze";
|
||||
"slomoVideos" = "Slow motion";
|
||||
"selfPortraits" = "Selfie";
|
||||
"screenshots" = "Istantanee";
|
||||
"depthEffect" = "Ritratti";
|
||||
"livePhotos" = "Live Photo";
|
||||
"animated" = "Animazioni";
|
||||
"myPhotoStream" = "II mio streaming foto";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Tutte le foto";
|
||||
"unableToAccessAllPhotos" = "Impossibile accedere a tutte le foto nell'album.\nConsenti l'accesso a \"Tutte le foto\" in \"Foto\"";
|
||||
"textStickerRemoveTips" = "Trascina qui per rimuovere";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/ja-US.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "カメラ";
|
||||
"previewCameraRecord" = "撮影";
|
||||
"previewAlbum" = "アルバム";
|
||||
"cancel" = "キャンセル";
|
||||
|
||||
"originalPhoto" = "元の画像";
|
||||
"done" = "確定";
|
||||
"ok" = "確定";
|
||||
"editFinish" = "完了";
|
||||
|
||||
"back" = "戻る";
|
||||
"edit" = "編集";
|
||||
"revert" = "元に戻す";
|
||||
"brightness" = "明度";
|
||||
"contrast" = "コントラスト";
|
||||
"saturation" = "彩度";
|
||||
|
||||
"photo" = "写真";
|
||||
"preview" = "プレビュー";
|
||||
|
||||
"noPhotoTips" = "写真がありません";
|
||||
|
||||
"hudLoading" = "お待ち下さい";
|
||||
|
||||
"exceededMaxSelectCount" = "最大選択数: %ld";
|
||||
"longerThanMaxVideoDuration" = "%ld秒より長い動画は選択できません";
|
||||
"shorterThanMinVideoDuration" = "%ld秒より短い動画は選択できません";
|
||||
"largerThanMaxVideoDataSize" = "%@MBを超える動画は選択できません";
|
||||
"smallerThanMinVideoDataSize" = "%@MB未満の動画は選択できません";
|
||||
"exceededMaxVideoSelectCount" = "動画の最大選択数: %ld";
|
||||
"lessThanMinVideoSelectCount" = "動画の最小選択数: %ld";
|
||||
|
||||
"noCameraAuthority" = "「設定」>「プライパシー」>「カメラ」から、%@があなたのデバイスのカメラにアクセスする許可をしてください";
|
||||
"noPhotoLibratyAuthority" = "「設定」>「プライバシー」>「写真」から、%@があなたのアルバムにアクセスする許可をしてください";
|
||||
"noMicrophoneAuthority" = "音声を録音できません。「設定」 >「%@」に移動し、マイクへのアクセスを有効にしてください";
|
||||
"cameraUnavailable" = "カメラは利用できません";
|
||||
"keepRecording" = "撮影を続ける";
|
||||
"gotoSettings" = "設定に移動";
|
||||
|
||||
"iCloudVideoLoadFaild" = "iCloudから同期できません";
|
||||
"imageLoadFailed" = "ロード失敗";
|
||||
|
||||
"save" = "セーブ";
|
||||
"saveImageError" = "画像の保存に失敗しました";
|
||||
"saveVideoError" = "ビデオの保存に失敗しました";
|
||||
"timeout" = "タイムアウトしました";
|
||||
|
||||
"customCameraTips" = "タップして撮影、長押しして記録できます";
|
||||
"customCameraTakePhotoTips" = "タップして撮影できます";
|
||||
"customCameraRecordVideoTips" = "長押しで記録できます";
|
||||
"minRecordTimeTips" = "最低 %ld秒以上記録してください";
|
||||
|
||||
"cameraRoll" = "最近の項目";
|
||||
"panoramas" = "パノラマ";
|
||||
"videos" = "ビデオ";
|
||||
"favorites" = "お気に入り";
|
||||
"timelapses" = "タイムラプス";
|
||||
"recentlyAdded" = "最後に追加した項目";
|
||||
"bursts" = "バースト";
|
||||
"slomoVideos" = "スローモーション";
|
||||
"selfPortraits" = "セルフイー";
|
||||
"screenshots" = "スクリーンショット";
|
||||
"depthEffect" = "ポートレート";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "アニメーション";
|
||||
"myPhotoStream" = "マイフォトストリーム";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "画像すべて";
|
||||
"unableToAccessAllPhotos" = "アルバム内のすべての写真にアクセスできません。\n「写真」内の「すべての写真」 へのアクセスを許可してください。";
|
||||
"textStickerRemoveTips" = "ここにドラッグして削除します";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/ko.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "카메라";
|
||||
"previewCameraRecord" = "기록";
|
||||
"previewAlbum" = "이미지";
|
||||
"cancel" = "취소";
|
||||
|
||||
"originalPhoto" = "전체 이미지";
|
||||
"done" = "확인";
|
||||
"ok" = "확인";
|
||||
"editFinish" = "완료";
|
||||
|
||||
"back" = "뒤";
|
||||
"edit" = "편집";
|
||||
"revert" = "실행 취소";
|
||||
"brightness" = "밝기";
|
||||
"contrast" = "대비";
|
||||
"saturation" = "채도";
|
||||
|
||||
"photo" = "사진";
|
||||
"preview" = "미리 보기";
|
||||
|
||||
"noPhotoTips" = "사진 없음";
|
||||
|
||||
"hudLoading" = "기다리는 중...";
|
||||
|
||||
"exceededMaxSelectCount" = "최대 선택 수: %ld";
|
||||
"longerThanMaxVideoDuration" = "길이가 %ld 초 보다 긴 동영상을 선택할 수 없습니다";
|
||||
"shorterThanMinVideoDuration" = "기간이 %ld 초 보다 짧은 비디오를 선택할 수 없습니다";
|
||||
"largerThanMaxVideoDataSize" = "%@MB보다 큰 동영상은 선택할 수 없습니다";
|
||||
"smallerThanMinVideoDataSize" = "%@MB 미만의 동영상은 선택할 수 없습니다";
|
||||
"exceededMaxVideoSelectCount" = "동영상 최대 선택 수: %ld";
|
||||
"lessThanMinVideoSelectCount" = "동영상 최소 선택 횟수: %ld";
|
||||
|
||||
"noCameraAuthority" = "%@ 에서 장치의 카메라에 액세스하도록 허용하십시오 에서 \"설정\" > \"개인 정보\" > \"카메라\"";
|
||||
"noPhotoLibratyAuthority" = "%@ 이 \"설정\" > \"개인 정보\" > \"사진\"에서 앨범에 액세스하도록 허용하세요";
|
||||
"noMicrophoneAuthority" = "오디오를 녹음할 수 없습니다. \"설정\" > \"%@\"으로 이동하여 마이크 액세스를 사용으로 설정하십시오";
|
||||
"cameraUnavailable" = "카메라를 사용할 수 없습니다";
|
||||
"keepRecording" = "계속 촬영";
|
||||
"gotoSettings" = "설정으로 이동";
|
||||
|
||||
"iCloudVideoLoadFaild" = "iCloud에서 동기화 할 수 없습니다";
|
||||
"imageLoadFailed" = "로드 실패";
|
||||
|
||||
"save" = "저장";
|
||||
"saveImageError" = "이미지를 저장하지 못했습니다";
|
||||
"saveVideoError" = "비디오를 저장하지 못했습니다";
|
||||
"timeout" = "요청 시간이 초과되었습니다";
|
||||
|
||||
"customCameraTips" = "눌러서 촬영 및 길게 눌러서 기록";
|
||||
"customCameraTakePhotoTips" = "눌러서 촬영";
|
||||
"customCameraRecordVideoTips" = "길게 눌러서 기록";
|
||||
"minRecordTimeTips" = "%ld 초 이상 녹화";
|
||||
|
||||
"cameraRoll" = "최근 항목";
|
||||
"panoramas" = "파노라마";
|
||||
"videos" = "비디오";
|
||||
"favorites" = "즐겨 찾기";
|
||||
"timelapses" = "타임랩스";
|
||||
"recentlyAdded" = "최근에 추가";
|
||||
"bursts" = "고속 연사 촬영";
|
||||
"slomoVideos" = "슬로 모션";
|
||||
"selfPortraits" = "셀카";
|
||||
"screenshots" = "스크린샷";
|
||||
"depthEffect" = "인물 사진";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "움직이는 항목";
|
||||
"myPhotoStream" = "나의 사진 스트림";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "모든 사진";
|
||||
"unableToAccessAllPhotos" = "앨범 사진에 접근할 수 없습니다.\n\"사진\"에서 \"모든 사진\"에 대한 접근을 허용합니다.";
|
||||
"textStickerRemoveTips" = "제거하려면 여기로 드래그하세요";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/ms.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Kamera";
|
||||
"previewCameraRecord" = "Rekod";
|
||||
"previewAlbum" = "Imej";
|
||||
"cancel" = "Batal";
|
||||
|
||||
"originalPhoto" = "Imej Penuh";
|
||||
"done" = "Selesai";
|
||||
"ok" = "Okey";
|
||||
"editFinish" = "Selesai";
|
||||
|
||||
"back" = "Belakang";
|
||||
"edit" = "Edit";
|
||||
"revert" = "Buat asal";
|
||||
"brightness" = "Kecerahan";
|
||||
"contrast" = "Contrast";
|
||||
"saturation" = "Ketepuan";
|
||||
|
||||
"photo" = "Gambar";
|
||||
"preview" = "Pratonton";
|
||||
|
||||
"noPhotoTips" = "Tiada Foto";
|
||||
|
||||
"hudLoading" = "menunggu...";
|
||||
|
||||
"exceededMaxSelectCount" = "Kiraan maksimum untuk pemilihan: %ld";
|
||||
"longerThanMaxVideoDuration" = "Tidak dapat memilih video dengan jangka masa lebih lama daripada %lds";
|
||||
"shorterThanMinVideoDuration" = "Tidak dapat memilih video dengan jangka masa lebih pendek daripada %lds";
|
||||
"largerThanMaxVideoDataSize" = "Tidak boleh memilih video yang lebih besar daripada %@MB";
|
||||
"smallerThanMinVideoDataSize" = "Tidak boleh memilih video yang lebih kecil daripada %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Jumlah maksimum untuk pemilihan video: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Kiraan minimum untuk pemilihan video: %ld";
|
||||
|
||||
"noCameraAuthority" = "Izinkan %@ mengakses kamera peranti anda di \"Tetapan\" > \"Privasi\" > \"Kamera\"";
|
||||
"noPhotoLibratyAuthority" = "Izinkan %@ mengakses album anda di \"Tetapan\" > \"Privasi\" > \"Foto\"";
|
||||
"noMicrophoneAuthority" = "Tidak dapat merakam audio. Pergi Ke \"Tetapan\" > \"%@\" dan dayakan akses mikrofon";
|
||||
"cameraUnavailable" = "Kamera tidak tersedia";
|
||||
"keepRecording" = "Teruskan Perakaman";
|
||||
"gotoSettings" = "Pergi ke Tetapan";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Tidak dapat menyegerakkan dari iCloud";
|
||||
"imageLoadFailed" = "pemuatan gagal";
|
||||
|
||||
"save" = "Berjimat";
|
||||
"saveImageError" = "Gagal menyimpan gambar";
|
||||
"saveVideoError" = "Gagal menyimpan video";
|
||||
"timeout" = "Permintaan tamat";
|
||||
|
||||
"customCameraTips" = "Ketik untuk menangkap dan tahan untuk merakam";
|
||||
"customCameraTakePhotoTips" = "Ketik untuk menangkap";
|
||||
"customCameraRecordVideoTips" = "Tahan untuk merakam";
|
||||
"minRecordTimeTips" = "Rakam sekurang-kurangnya %lds";
|
||||
|
||||
"cameraRoll" = "Terbaru";
|
||||
"panoramas" = "Panorama";
|
||||
"videos" = "Video";
|
||||
"favorites" = "Kegemaran";
|
||||
"timelapses" = "Selang Masa";
|
||||
"recentlyAdded" = "Ditambah Terkini";
|
||||
"bursts" = "Jujukan";
|
||||
"slomoVideos" = "Slo-mo";
|
||||
"selfPortraits" = "Swafoto";
|
||||
"screenshots" = "Gambar Skrin";
|
||||
"depthEffect" = "Potret";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Beranimasi";
|
||||
"myPhotoStream" = "Strim Foto Saya";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Semua Foto";
|
||||
"unableToAccessAllPhotos" = "Tidak dapat mengakses semua foto dalam album.\nBenarkan akses kepada \"Semua Foto\" dalam \"Foto\".";
|
||||
"textStickerRemoveTips" = "Seret ke sini untuk mengalih keluar";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/pt-BR.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Câmara";
|
||||
"previewCameraRecord" = "Recorde";
|
||||
"previewAlbum" = "Álbum";
|
||||
"cancel" = "Cancelar";
|
||||
|
||||
"originalPhoto" = "Imagem completa";
|
||||
"done" = "Feito";
|
||||
"ok" = "OK";
|
||||
"editFinish" = "Feito";
|
||||
|
||||
"back" = "Voltar";
|
||||
"edit" = "Editar";
|
||||
"revert" = "Desfazer";
|
||||
"brightness" = "Brilho";
|
||||
"contrast" = "Contraste";
|
||||
"saturation" = "Saturação";
|
||||
|
||||
"photo" = "Fotos";
|
||||
"preview" = "Pré-visualização";
|
||||
|
||||
"noPhotoTips" = "Sem Fotos";
|
||||
|
||||
"hudLoading" = "à espera...";
|
||||
|
||||
"exceededMaxSelectCount" = "Contagem máxima para seleção: %ld";
|
||||
"longerThanMaxVideoDuration" = "Não é possível selecionar vídeos com mais de %ld segundos";
|
||||
"shorterThanMinVideoDuration" = "Não é possível selecionar vídeos com menos de %ld segundos";
|
||||
"largerThanMaxVideoDataSize" = "Não é possível selecionar vídeos com mais de %@MB";
|
||||
"smallerThanMinVideoDataSize" = "Não é possível selecionar vídeos com menos de %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Contagem máxima para seleção de vídeo: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Contagem mínima para seleção de vídeo: %ld";
|
||||
|
||||
"noCameraAuthority" = "Por favor, permita a %@ aceder à câmara do seu dispositivo em \"Definições\" > \"Privacidade\" > \"Câmara\".";
|
||||
"noPhotoLibratyAuthority" = "Por favor, permita que %@ acesse seu álbum em \"Configurações\" > \"Privacidade\" > \"Fotos\".";
|
||||
"noMicrophoneAuthority" = "Incapaz de gravar áudio. Vá para \"Configurações\" > \"%@\" e habilite o acesso ao microfone";
|
||||
"cameraUnavailable" = "A câmara não está disponível";
|
||||
"keepRecording" = "Continuar Gravando";
|
||||
"gotoSettings" = "Ir para Configurações";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Incapaz de sincronizar a partir do iCloud";
|
||||
"imageLoadFailed" = "carregamento fracassado";
|
||||
|
||||
"save" = "Salvar";
|
||||
"saveImageError" = "Falha em salvar a imagem";
|
||||
"saveVideoError" = "Falha ao salvar o vídeo";
|
||||
"timeout" = "Pedidos com tempo limite";
|
||||
|
||||
"customCameraTips" = "Toque para tirar foto e segure para gravar vídeo";
|
||||
"customCameraTakePhotoTips" = "Toque para tirar foto";
|
||||
"customCameraRecordVideoTips" = "Segure para gravar vídeo";
|
||||
"minRecordTimeTips" = "Registre pelo menos %lds";
|
||||
|
||||
"cameraRoll" = "Recentes";
|
||||
"panoramas" = "Panoramas";
|
||||
"videos" = "Vídeos";
|
||||
"favorites" = "Favoritos";
|
||||
"timelapses" = "Prazo";
|
||||
"recentlyAdded" = "Adicionado recentemente";
|
||||
"bursts" = "Rebentamentos";
|
||||
"slomoVideos" = "Slo-mo";
|
||||
"selfPortraits" = "Selfies";
|
||||
"screenshots" = "Imagens de tela";
|
||||
"depthEffect" = "Retrato";
|
||||
"livePhotos" = "Fotos ao vivo";
|
||||
"animated" = "Animado";
|
||||
"myPhotoStream" = "Meu fluxo de fotos";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Todas as fotos";
|
||||
"unableToAccessAllPhotos" = "Incapaz de aceder a todas as fotografias do álbum.\nPermitir o acesso a \"Todas as fotos\" em \"Fotos\".";
|
||||
"textStickerRemoveTips" = "Arraste aqui para remover";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/ru.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Камера";
|
||||
"previewCameraRecord" = "изображения";
|
||||
"previewAlbum" = "Альбом";
|
||||
"cancel" = "Отмена";
|
||||
|
||||
"originalPhoto" = "Полный формат";
|
||||
"done" = "Готово";
|
||||
"ok" = "в порядке";
|
||||
"editFinish" = "Готово";
|
||||
|
||||
"back" = "Назад";
|
||||
"edit" = "Pед";
|
||||
"revert" = "Отменить";
|
||||
"brightness" = "Яркость";
|
||||
"contrast" = "Контраст";
|
||||
"saturation" = "Насыщенность";
|
||||
|
||||
"photo" = "Фото";
|
||||
"preview" = "Предпросмотр";
|
||||
|
||||
"noPhotoTips" = "Нет фотографии";
|
||||
|
||||
"hudLoading" = "ожидание...";
|
||||
|
||||
"exceededMaxSelectCount" = "Максимальное количество выбранных: %ld";
|
||||
"longerThanMaxVideoDuration" = "Невозможно выбрать видео продолжительностью более %ld сек";
|
||||
"shorterThanMinVideoDuration" = "Невозможно выбрать видео короче %ld сек";
|
||||
"largerThanMaxVideoDataSize" = "Невозможно выбрать видео размером более %@МБ";
|
||||
"smallerThanMinVideoDataSize" = "Невозможно выбрать видео размером менее %@МБ";
|
||||
"exceededMaxVideoSelectCount" = "Максимальное количество выбранных видео: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Количество мин. Выбора видео: %ld";
|
||||
|
||||
"noCameraAuthority" = "Разрешите %@ доступ к камере вашего устройства в \"Настройки\" > \"Конфиденциальность\" > \"Камера\"";
|
||||
"noPhotoLibratyAuthority" = "Разрешите %@ доступ к вашему альбому в \"Настройки\" > \"Конфиденциальность\" > \"Фото\"";
|
||||
"noMicrophoneAuthority" = "Не удалось записать звук. Перейдите в меню \"Настройки\" > \"%@\" и включите доступ к микрофону";
|
||||
"cameraUnavailable" = "Камера недоступна";
|
||||
"keepRecording" = "Продолжить запись";
|
||||
"gotoSettings" = "Перейти в настройки";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Невозможно синхронизировать из iCloud";
|
||||
"imageLoadFailed" = "загрузка не удалась";
|
||||
|
||||
"save" = "Сохранить";
|
||||
"saveImageError" = "Не удалось сохранить изображение";
|
||||
"saveVideoError" = "Не удалось сохранить видео";
|
||||
"timeout" = "Истекло время запроса";
|
||||
|
||||
"customCameraTips" = "Нажмите для съемки, удерживайте для записи";
|
||||
"customCameraTakePhotoTips" = "Нажмите для съeмки";
|
||||
"customCameraRecordVideoTips" = "Удерживайте для записи";
|
||||
"minRecordTimeTips" = "Запишите не менее 2 с";
|
||||
|
||||
"cameraRoll" = "Недавние";
|
||||
"panoramas" = "Панорамы";
|
||||
"videos" = "Видео";
|
||||
"favorites" = "Избранное";
|
||||
"timelapses" = "Tаймлапс";
|
||||
"recentlyAdded" = "Недавно добавленный";
|
||||
"bursts" = "Cepии";
|
||||
"slomoVideos" = "Замедленное";
|
||||
"selfPortraits" = "Селфи";
|
||||
"screenshots" = "Cнимки зкрана";
|
||||
"depthEffect" = "Портреты";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Анимированные";
|
||||
"myPhotoStream" = "Мой фотопоток";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Все фотографии";
|
||||
"unableToAccessAllPhotos" = "Невозможно получить доступ к фотографиям в альбоме.\nРазрешить доступ ко \"Всем фотографиям\" в \"Фото\".";
|
||||
"textStickerRemoveTips" = "Перетащите сюда, чтобы удалить";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/tr.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Kamera";
|
||||
"previewCameraRecord" = "Kayıt Et";
|
||||
"previewAlbum" = "Albüm";
|
||||
"cancel" = "İptal";
|
||||
|
||||
"originalPhoto" = "Orijinal Resim";
|
||||
"done" = "Bitti";
|
||||
"ok" = "OK";
|
||||
"editFinish" = "Bitti";
|
||||
|
||||
"back" = "Geri";
|
||||
"edit" = "Düzenle";
|
||||
"revert" = "Geri Al";
|
||||
"brightness" = "Parlaklık";
|
||||
"contrast" = "Kontrast";
|
||||
"saturation" = "Canlılık";
|
||||
|
||||
"photo" = "Fotoğrafşar";
|
||||
"preview" = "Önizle";
|
||||
|
||||
"noPhotoTips" = "Fotoğraf yok";
|
||||
|
||||
"hudLoading" = "bekleyin...";
|
||||
|
||||
"exceededMaxSelectCount" = "Maksimum seçim adeti: %ld";
|
||||
"longerThanMaxVideoDuration" = "%lds'dan uzun süreli videolar seçilemiyor.";
|
||||
"shorterThanMinVideoDuration" = "%lds'dan kıza süreli videolar seçilemiyor.";
|
||||
"largerThanMaxVideoDataSize" = "%@MB'tan büyük videolar seçilemiyor";
|
||||
"smallerThanMinVideoDataSize" = "%@MB'tan küçük videolar seçilemez";
|
||||
"exceededMaxVideoSelectCount" = "Maksimum video seçim adeti: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Minimum video seçim adeti: %ld";
|
||||
|
||||
"noCameraAuthority" = "Lütfen %@'nin \"Ayarlar\" > \"Gizlilik\" > \"Kamera\" bölümünden cihazınızın kamerasına erişmesine izin verin";
|
||||
"noPhotoLibratyAuthority" = "Lütfen %@'nin \"Ayarlar\" > \"Gizlilik\" > \"Fotoğraflar\" bölümünde albümünüze erişmesine izin verin";
|
||||
"noMicrophoneAuthority" = "Ses kaydedilemiyor. \"Ayarlar\" > \"%@\" seçeneğine gidin ve mikrofon erişimini etkinleştirin";
|
||||
"cameraUnavailable" = "Kamera kullanılamıyor";
|
||||
"keepRecording" = "Kayda Devam Et";
|
||||
"gotoSettings" = "Ayarlara git";
|
||||
|
||||
"iCloudVideoLoadFaild" = "iCloud'dan senkronize edilemiyor";
|
||||
"imageLoadFailed" = "Yüklenemedi!";
|
||||
|
||||
"save" = "Kaydet";
|
||||
"saveImageError" = "Resim kaydedilemedi!";
|
||||
"saveVideoError" = "Video kaydedilemedi!";
|
||||
"timeout" = "İstek zaman aşımına uğradı";
|
||||
|
||||
"customCameraTips" = "Fotoğraf çekmek için dokunun ve video kaydetmek için basılı tutun";
|
||||
"customCameraTakePhotoTips" = "Fotoğraf çekmek için dokunun";
|
||||
"customCameraRecordVideoTips" = "Video çekmek için basılı tutun";
|
||||
"minRecordTimeTips" = "En az %lds kaydedin";
|
||||
|
||||
"cameraRoll" = "Son Çekimler";
|
||||
"panoramas" = "Panoramalar";
|
||||
"videos" = "Videolar";
|
||||
"favorites" = "Favoriler";
|
||||
"timelapses" = "Hızlandırılmışlar";
|
||||
"recentlyAdded" = "Yeni Eklenenler";
|
||||
"bursts" = "Bursts";
|
||||
"slomoVideos" = "Yavaş Çekimler";
|
||||
"selfPortraits" = "Selfie'ler";
|
||||
"screenshots" = "Ekran Görüntüleri";
|
||||
"depthEffect" = "Portreler";
|
||||
"livePhotos" = "Live Photo'lar";
|
||||
"animated" = "Hareketli";
|
||||
"myPhotoStream" = "Fotoğraf Akışım";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Tüm Fotoğraflar";
|
||||
"unableToAccessAllPhotos" = "Albümdeki tüm fotoğraflara erişilemiyor.\n\"Fotoğraflar\"da \"Tüm Fotoğraflar\"a erişime izin verin.";
|
||||
"textStickerRemoveTips" = "Kaldırmak için buraya sürükleyin";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/vi.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "Camera";
|
||||
"previewCameraRecord" = "Ghi lại";
|
||||
"previewAlbum" = "Hình ảnh";
|
||||
"cancel" = "Huỷ";
|
||||
|
||||
"originalPhoto" = "Toàn bộ hình ảnh";
|
||||
"done" = "Xong";
|
||||
"ok" = "đồng ý";
|
||||
"editFinish" = "Xong";
|
||||
|
||||
"back" = "Trở lại";
|
||||
"edit" = "Chỉnh sửa";
|
||||
"revert" = "Hoàn tác";
|
||||
"brightness" = "độ sáng";
|
||||
"contrast" = "Sự tương phản";
|
||||
"saturation" = "Bão hòa";
|
||||
|
||||
"photo" = "Ảnh";
|
||||
"preview" = "Xem trước";
|
||||
|
||||
"noPhotoTips" = "Không có ảnh";
|
||||
|
||||
"hudLoading" = "đang chờ đợi...";
|
||||
|
||||
"exceededMaxSelectCount" = "Số lượng lựa chọn tối đa: %ld";
|
||||
"longerThanMaxVideoDuration" = "Không thể chọn video có thời lượng dài hơn %ld giây";
|
||||
"shorterThanMinVideoDuration" = "Không thể chọn video có thời lượng ngắn hơn %ld giây";
|
||||
"largerThanMaxVideoDataSize" = "Không thể chọn video lớn hơn %@MB";
|
||||
"smallerThanMinVideoDataSize" = "Không thể chọn video nhỏ hơn %@MB";
|
||||
"exceededMaxVideoSelectCount" = "Số lượng lựa chọn tối đa của video: %ld";
|
||||
"lessThanMinVideoSelectCount" = "Số phút chọn tối thiểu của video: %ld";
|
||||
|
||||
"noCameraAuthority" = "Vui lòng cho phép %@ truy cập máy ảnh trên thiết bị của bạn trong \"Cài đặt\" > \"Quyền riêng tư\" > \"Máy ảnh\"";
|
||||
"noPhotoLibratyAuthority" = "Vui lòng cho phép %@ truy cập anbom của bạn trong \"Cài đặt\" > \"Bảo mật\" > \"Ảnh\"";
|
||||
"noMicrophoneAuthority" = "Không thểghi hình. Đi tới \"Cài đặt\" > \"%@\" và bật quyên truy cập mic";
|
||||
"cameraUnavailable" = "Máy ảnh không khả dụng";
|
||||
"keepRecording" = "Tiếp tục ghi hình";
|
||||
"gotoSettings" = "Đi đến Cài đặt";
|
||||
|
||||
"iCloudVideoLoadFaild" = "Không thể đồng bộ hóa từ iCloud";
|
||||
"imageLoadFailed" = "tải không thành công";
|
||||
|
||||
"save" = "Tiết kiệm";
|
||||
"saveImageError" = "Lưu ảnh không thành công";
|
||||
"saveVideoError" = "Lưu video không thành công";
|
||||
"timeout" = "Yêu cầu đã hết thời gian chờ";
|
||||
|
||||
"customCameraTips" = "Nhấn để chụp và giữ để ghi";
|
||||
"customCameraTakePhotoTips" = "Nhấn để chụp";
|
||||
"customCameraRecordVideoTips" = "Giữ để quay video";
|
||||
"minRecordTimeTips" = "Ghi ít nhất %ld giây";
|
||||
|
||||
"cameraRoll" = "Gần đây";
|
||||
"panoramas" = "Ảnh toàn cảnh";
|
||||
"videos" = "Video";
|
||||
"favorites" = "Mục ưa thích";
|
||||
"timelapses" = "Ảnh time-lapse";
|
||||
"recentlyAdded" = "Đã thêm gần đây";
|
||||
"bursts" = "Chụp liên hình";
|
||||
"slomoVideos" = "Quay chậm";
|
||||
"selfPortraits" = "Ảnh selfie";
|
||||
"screenshots" = "Ảnh màn hình";
|
||||
"depthEffect" = "Chân dung";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "Hình động";
|
||||
"myPhotoStream" = "Kho ảnh của tôi";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "Tất cả ảnh";
|
||||
"unableToAccessAllPhotos" = "Không thể truy cập tất cả ảnh trong album.\nCho phép truy cập vào \"Tất cả ảnh\" trong \"Ành\".";
|
||||
"textStickerRemoveTips" = "Kéo vào đây để xóa";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zh-Hans.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "拍照";
|
||||
"previewCameraRecord" = "拍摄";
|
||||
"previewAlbum" = "相册";
|
||||
"cancel" = "取消";
|
||||
|
||||
"originalPhoto" = "原图";
|
||||
"done" = "确定";
|
||||
"ok" = "确定";
|
||||
"editFinish" = "完成";
|
||||
|
||||
"back" = "返回";
|
||||
"edit" = "编辑";
|
||||
"revert" = "还原";
|
||||
"brightness" = "亮度";
|
||||
"contrast" = "对比度";
|
||||
"saturation" = "饱和度";
|
||||
|
||||
"photo" = "照片";
|
||||
"preview" = "预览";
|
||||
|
||||
"noPhotoTips" = "无照片";
|
||||
|
||||
"hudLoading" = "正在处理...";
|
||||
|
||||
"exceededMaxSelectCount" = "最多只能选择%ld张图片";
|
||||
"longerThanMaxVideoDuration" = "不能选择超过%ld秒的视频";
|
||||
"shorterThanMinVideoDuration" = "不能选择低于%ld秒的视频";
|
||||
"largerThanMaxVideoDataSize" = "不能选择大于%@MB的视频";
|
||||
"smallerThanMinVideoDataSize" = "不能选择小于%@MB的视频";
|
||||
"exceededMaxVideoSelectCount" = "最多只能选择%ld个视频";
|
||||
"lessThanMinVideoSelectCount" = "最少选择%ld个视频";
|
||||
|
||||
"noCameraAuthority" = "请在iPhone的\"设置 > 隐私 > 相机\"选项中,允许%@访问你的相机";
|
||||
"noPhotoLibratyAuthority" = "请在iPhone的\"设置 > 隐私 >照片\"选项中,允许%@访问你的照片";
|
||||
"noMicrophoneAuthority" = "无法录制声音,前往\"设置 > %@\"中打开麦克风权限";
|
||||
"cameraUnavailable" = "相机不可用";
|
||||
"keepRecording" = "继续拍摄";
|
||||
"gotoSettings" = "前往设置";
|
||||
|
||||
"iCloudVideoLoadFaild" = "iCloud无法同步";
|
||||
"imageLoadFailed" = "图片加载失败";
|
||||
|
||||
"save" = "保存";
|
||||
"saveImageError" = "图片保存失败";
|
||||
"saveVideoError" = "视频保存失败";
|
||||
"timeout" = "请求超时";
|
||||
|
||||
"customCameraTips" = "轻触拍照,按住摄像";
|
||||
"customCameraTakePhotoTips" = "轻触拍照";
|
||||
"customCameraRecordVideoTips" = "按住摄像";
|
||||
"minRecordTimeTips" = "至少录制%ld秒";
|
||||
|
||||
"cameraRoll" = "最近项目";
|
||||
"panoramas" = "全景照片";
|
||||
"videos" = "视频";
|
||||
"favorites" = "个人收藏";
|
||||
"timelapses" = "延时摄影";
|
||||
"recentlyAdded" = "最近添加";
|
||||
"bursts" = "连拍快照";
|
||||
"slomoVideos" = "慢动作";
|
||||
"selfPortraits" = "自拍";
|
||||
"screenshots" = "屏幕快照";
|
||||
"depthEffect" = "人像";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "动图";
|
||||
"myPhotoStream" = "我的照片流";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "所有照片";
|
||||
"unableToAccessAllPhotos" = "无法访问相册中所有照片,\n请允许访问「照片」中的「所有照片」。";
|
||||
"textStickerRemoveTips" = "拖到此处删除";
|
||||
70
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zh-Hant.lproj/Localizable.strings
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
"previewCamera" = "拍照";
|
||||
"previewCameraRecord" = "拍攝";
|
||||
"previewAlbum" = "相冊";
|
||||
"cancel" = "取消";
|
||||
|
||||
"originalPhoto" = "原圖";
|
||||
"done" = "確定";
|
||||
"ok" = "確定";
|
||||
"editFinish" = "完成";
|
||||
|
||||
"back" = "返回";
|
||||
"edit" = "編輯";
|
||||
"revert" = "還原";
|
||||
"brightness" = "亮度";
|
||||
"contrast" = "對比度";
|
||||
"saturation" = "飽和度";
|
||||
|
||||
"photo" = "照片";
|
||||
"preview" = "預覽";
|
||||
|
||||
"noPhotoTips" = "無照片";
|
||||
|
||||
"hudLoading" = "正在處理...";
|
||||
|
||||
"exceededMaxSelectCount" = "最多只能選擇%ld張圖片";
|
||||
"longerThanMaxVideoDuration" = "不能選擇超過%ld秒的視頻";
|
||||
"shorterThanMinVideoDuration" = "不能選擇低於%ld秒的視頻";
|
||||
"largerThanMaxVideoDataSize" = "不能選擇大於%@MB的視頻";
|
||||
"smallerThanMinVideoDataSize" = "不能選擇小於%@MB的視頻";
|
||||
"exceededMaxVideoSelectCount" = "最多只能選擇%ld個視頻";
|
||||
"lessThanMinVideoSelectCount" = "最少選擇%ld個視頻";
|
||||
|
||||
"noCameraAuthority" = "請在iPhone的\"設置 > 隱私 > 相機\"選項中,允許%@訪問你的相機";
|
||||
"noPhotoLibratyAuthority" = "請在iPhone的\"設置 > 隱私 > 相冊\"選項中,允許%@訪問你的照片";
|
||||
"noMicrophoneAuthority" = "無法錄製聲音,前往\"設置 > %@\"中打開麥克風權限";
|
||||
"cameraUnavailable" = "相機不可用";
|
||||
"keepRecording" = "繼續拍攝";
|
||||
"gotoSettings" = "前往設置";
|
||||
|
||||
"iCloudVideoLoadFaild" = "iCloud無法同步";
|
||||
"imageLoadFailed" = "圖片加載失敗";
|
||||
|
||||
"save" = "保存";
|
||||
"saveImageError" = "圖片保存失敗";
|
||||
"saveVideoError" = "視頻保存失敗";
|
||||
"timeout" = "請求超時";
|
||||
|
||||
"customCameraTips" = "輕觸拍照,按住攝像";
|
||||
"customCameraTakePhotoTips" = "輕觸拍照";
|
||||
"customCameraRecordVideoTips" = "按住攝像";
|
||||
"minRecordTimeTips" = "至少錄制%ld秒";
|
||||
|
||||
"cameraRoll" = "最近項目";
|
||||
"panoramas" = "全景照片";
|
||||
"videos" = "視頻";
|
||||
"favorites" = "個人收藏";
|
||||
"timelapses" = "延時攝影";
|
||||
"recentlyAdded" = "最近添加";
|
||||
"bursts" = "連拍快照";
|
||||
"slomoVideos" = "慢動作";
|
||||
"selfPortraits" = "自拍";
|
||||
"screenshots" = "屏幕快照";
|
||||
"depthEffect" = "人像";
|
||||
"livePhotos" = "Live Photos";
|
||||
"animated" = "動圖";
|
||||
"myPhotoStream" = "我的照片流";
|
||||
|
||||
"noTitleAlbumListPlaceholder" = "所有照片";
|
||||
"unableToAccessAllPhotos" = "無法訪問相冊中所有照片,\n請允許訪問「照片」中的「所有照片」。";
|
||||
"textStickerRemoveTips" = "拖到此處刪除";
|
||||
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_ablumList_arrow@2x.png
generated
Normal file
|
After Width: | Height: | Size: 458 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_ablumList_arrow@3x.png
generated
Normal file
|
After Width: | Height: | Size: 578 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_addPhoto@2x.png
generated
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_addPhoto@3x.png
generated
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_adjust@2x.png
generated
Normal file
|
After Width: | Height: | Size: 565 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_adjust@3x.png
generated
Normal file
|
After Width: | Height: | Size: 682 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_adjust_selected@2x.png
generated
Normal file
|
After Width: | Height: | Size: 542 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_adjust_selected@3x.png
generated
Normal file
|
After Width: | Height: | Size: 686 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_albumSelect@2x.png
generated
Normal file
|
After Width: | Height: | Size: 541 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_albumSelect@3x.png
generated
Normal file
|
After Width: | Height: | Size: 727 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_arrow_down@2x.png
generated
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_arrow_down@3x.png
generated
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_ashbin@2x.png
generated
Normal file
|
After Width: | Height: | Size: 821 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_ashbin@3x.png
generated
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_ashbin_open@2x.png
generated
Normal file
|
After Width: | Height: | Size: 1002 B |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_ashbin_open@3x.png
generated
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_brightness@2x.png
generated
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_brightness@3x.png
generated
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_brightness_selected@2x.png
generated
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_brightness_selected@3x.png
generated
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Pods/ZLPhotoBrowser/Sources/ZLPhotoBrowser.bundle/zl_btn_circle@2x.png
generated
Normal file
|
After Width: | Height: | Size: 450 B |