This commit is contained in:
DDIsFriend
2023-08-18 17:28:57 +08:00
commit f0e8a1709d
4282 changed files with 192396 additions and 0 deletions

22
Pods/ZLPhotoBrowser/LICENSE generated Normal file
View 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
View File

@@ -0,0 +1,199 @@
[![Version](https://img.shields.io/github/v/tag/longitachi/ZLPhotoBrowser.svg?color=blue&include_prereleases=&sort=semver)](https://cocoapods.org/pods/ZLPhotoBrowser)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage)
[![SPM supported](https://img.shields.io/badge/SwiftPM-supported-E57141.svg)](https://swift.org/package-manager/)
[![License](https://img.shields.io/badge/license-MIT-black)](https://raw.githubusercontent.com/longitachi/ZLPhotoBrowser/master/LICENSE)
[![Platform](https://img.shields.io/badge/Platforms-iOS-blue?style=flat)](https://img.shields.io/badge/Platforms-iOS-blue?style=flat)
![Language](https://img.shields.io/badge/Language-%20Swift%20-E57141.svg)
[![Usage](https://img.shields.io/badge/Usage-Doc-yarn?style=flat)](https://github.com/longitachi/ZLPhotoBrowser/wiki/How-to-use-(Swift))
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/preview_with_title.png)
----------------------------------------
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 dont 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](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/%E5%BF%AB%E9%80%9F%E9%80%89%E6%8B%A9.gif)
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/%E7%9B%B8%E5%86%8C%E5%86%85%E9%83%A8%E9%80%89%E6%8B%A9.gif)
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/%E9%A2%84%E8%A7%88%E5%A4%A7%E5%9B%BE.gif)
- Image editor
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/editImage.gif)
- Video editor
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/editVideo.gif)
- Multi-language
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/%E5%A4%9A%E8%AF%AD%E8%A8%80.gif)
- Custom camera
![image](https://github.com/longitachi/ImageFolder/blob/master/ZLPhotoBrowser/introduce.png)

View 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)
}
}
}

View 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) {}
}

View 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
// xy
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)
}
}
}

File diff suppressed because it is too large Load Diff

View 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?()
}
}
}

View 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~360360
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
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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")
}
}

View 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
}
}

View 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)
}

View 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()
}
}

View 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).lengthcountemojicount2
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
// rectrect2
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")
}
}
}

View 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()
}
}

View 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 }
}
}

View 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
}
}

View 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)
}
}

View 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
}
}

View 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)
}
}

View 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")
}

View 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
}
}

View 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))
}
}

View 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
}
/// 0title
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)
}
}

View 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)
}
}

View 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)
}
}

View 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
}
}

View 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)
}
}

View 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)
}
}

View 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")
}
}
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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()
}
}
}

View 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
}
}

View 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() }
}

View 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)
}
}
}

View 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---103 -0.33---0.33
return value / 3
case .contrast:
// 0---410.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---21
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)
}

View 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)
}
}
}

View 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
}
}

View 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
}
}

View 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
}

View 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.
}
}

View 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)
}
}

View 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)
}

View 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 { }

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

File diff suppressed because it is too large Load Diff

View 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
}
}

View 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
}
}

View 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" = "اسحب هنا للإزالة";

View 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";

View 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";

View 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";

View 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";

View 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";

View 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";

View 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" = "ここにドラッグして削除します";

View 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" = "제거하려면 여기로 드래그하세요";

View 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";

View 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";

View 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" = "Перетащите сюда, чтобы удалить";

View 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";

View 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";

View 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" = "拖到此处删除";

View 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" = "拖到此處刪除";

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Some files were not shown because too many files have changed in this diff Show More