Commit 409eabe4 authored by CZ1004's avatar CZ1004

修改

parent fe59c4f0
...@@ -165,10 +165,14 @@ ...@@ -165,10 +165,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources.sh\"\n";
...@@ -204,10 +208,14 @@ ...@@ -204,10 +208,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks.sh\"\n";
...@@ -234,11 +242,9 @@ ...@@ -234,11 +242,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = 7X8JL97Z3E;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6K23946NQ5;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PhoneManager/Info.plist; INFOPLIST_FILE = PhoneManager/Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "We need to access the network to load content"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "We need to access the network to load content";
...@@ -254,10 +260,9 @@ ...@@ -254,10 +260,9 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager; PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager1;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = phonemanager_dev;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
...@@ -274,11 +279,9 @@ ...@@ -274,11 +279,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = 7X8JL97Z3E;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6K23946NQ5;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PhoneManager/Info.plist; INFOPLIST_FILE = PhoneManager/Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "We need to access the network to load content"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "We need to access the network to load content";
...@@ -294,10 +297,9 @@ ...@@ -294,10 +297,9 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager; PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager1;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = phonemanager_dev;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
......
...@@ -29,6 +29,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -29,6 +29,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
window?.makeKeyAndVisible() window?.makeKeyAndVisible()
} }
// 加载压缩里面的数据
let viewModel = CompressViewModel()
viewModel.getAllPhotosToAssets(sortType: 0, assetType: 0, {[weak self] models in
guard let self else {return}
let singleton = Singleton.shared
singleton.resourceModel = models
})
return true return true
} }
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import Photos
typealias CompressSelectCellCallback = (ResourceModel,Bool)->Void typealias CompressSelectCellCallback = (ResourceModel,Bool)->Void
...@@ -13,18 +14,46 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -13,18 +14,46 @@ class CompressSelectCell : UICollectionViewCell {
var callBack : CompressSelectCellCallback = {model,choose in} var callBack : CompressSelectCellCallback = {model,choose in}
var currentMediaType : Int = 0
var model : ResourceModel? { var model : ResourceModel? {
didSet{ didSet{
guard let model = self.model else {return} guard let model = self.model else {return}
let image = PhotoAndVideoMananger.mananger.getImageFromAssetID(id: model.ident) self.backImageView.image = nil
self.backImageView.image = image let viewModel = CompressViewModel()
viewModel.getImageFromAssetIdentifier(identifier: model.ident) {[weak self] image in
guard let self else { return}
DispatchQueue.main.async {
self.backImageView.image = image
}
}
// 把压缩前的值减去压缩后的值就为可以节省的值。然后这里需要判定下如果是大于1000MB,则再除以1024换算成GB // 把压缩前的值减去压缩后的值就为可以节省的值。然后这里需要判定下如果是大于1000MB,则再除以1024换算成GB
let saveSize = model.orgSize - model.compressSize let options = PHImageRequestOptions()
if saveSize > 1000 { options.isSynchronous = false
self.saveSizeLabel.text = String(format: "Save %.2f GB" ,saveSize/1024.0) options.deliveryMode = .highQualityFormat
}else{ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.ident], options: nil)
self.saveSizeLabel.text = String(format: "Save %.2f MB" ,saveSize)
PHImageManager.default().requestImage(for: fetchResult.firstObject!, targetSize: PHImageManagerMaximumSize, contentMode:.aspectFit, options: options) { (image, _) in
if let originalImage = image {
// 项目中用到的是【0.2、0.5和0.8】,这里我们初始化的时候使用0.2去计算
if let compressedData = originalImage.jpegData(compressionQuality: 0.2) {
let compressCompletedSize = Double(compressedData.count)
let saveSize = model.orgSize - compressCompletedSize
let sizeKB : Double = saveSize/1024
DispatchQueue.main.async {
if sizeKB < 1024{
self.saveSizeLabel.text = String(format: "Save %.2f KB" ,sizeKB)
}else if sizeKB < (1024 * 1024) && sizeKB > 1024{
self.saveSizeLabel.text = String(format: "Save %.2f MB" ,sizeKB/1024)
}else{
self.saveSizeLabel.text = String(format: "Save %.2f GB" ,sizeKB/(1024 * 1024))
}
}
}
}
} }
} }
} }
...@@ -96,7 +125,12 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -96,7 +125,12 @@ class CompressSelectCell : UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(imageClick))
self.addGestureRecognizer(tap)
self.addSubview(self.backImageView) self.addSubview(self.backImageView)
self.addSubview(self.saveSizeView) self.addSubview(self.saveSizeView)
...@@ -110,14 +144,14 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -110,14 +144,14 @@ class CompressSelectCell : UICollectionViewCell {
self.saveSizeView.snp.makeConstraints { make in self.saveSizeView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12) make.left.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12) make.bottom.equalToSuperview().offset(-12)
make.height.equalTo(48) make.height.equalTo(25)
make.width.equalTo(120) make.width.equalTo(120)
} }
self.saveSizeLabel.snp.makeConstraints { make in self.saveSizeLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(8) make.left.equalToSuperview().offset(8)
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
make.height.equalTo(48) make.height.equalTo(25)
make.width.equalTo(120) make.width.equalTo(105)
} }
self.moreImageView.snp.makeConstraints { make in self.moreImageView.snp.makeConstraints { make in
...@@ -139,4 +173,18 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -139,4 +173,18 @@ class CompressSelectCell : UICollectionViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc func imageClick(){
if self.currentMediaType == 0 {
// 如果是图片
let vc : PreViewController = PreViewController()
vc.imageIdent = self.model!.ident
self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
}else{
// 如果是视频
let vc : PreVideoController = PreVideoController(localIdentifier: self.model!.ident)
self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
}
}
} }
...@@ -6,11 +6,18 @@ ...@@ -6,11 +6,18 @@
// //
import Foundation import Foundation
import Photos
class CompressCompletedViewController : BaseViewController{ class CompressCompletedViewController : BaseViewController{
var model : [ResourceModel]? var model : [ResourceModel]?
var comDataSource : [Data] = []
var comVideoDataSource : [URL?] = []
var currentMediaType : Int = 0
lazy var imageView: UIImageView = { lazy var imageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.clipsToBounds = true imageView.clipsToBounds = true
...@@ -248,24 +255,90 @@ class CompressCompletedViewController : BaseViewController{ ...@@ -248,24 +255,90 @@ class CompressCompletedViewController : BaseViewController{
} }
@objc func completedAction(){ @objc func completedAction(){
if currentMediaType == 0 {
// 将压缩后的照片存到相册
for imageData in self.comDataSource {
PHPhotoLibrary.shared().performChanges({
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: imageData, options: nil)
}) { success, error in
if(success){
print("保存照片成功")
}else {
if let error = error {
print("保存相片时出错: \(error.localizedDescription)")
}
}
}
}
}else{
for item in self.comVideoDataSource {
guard let item else {
print("保存视频失败,URL为空")
self.jumpToCompressVC()
return
}
// 保存视频到相册
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: item as URL)
}) { (success, saveError) in
if success {
print("保存视频成功")
}else{
if let error = saveError {
print("保存视频时出错: \(error.localizedDescription)")
}
}
}
}
}
// 提示是否删除照片 // 删除文件逻辑【系统自动提示是否删除】
let compressTipView : CompressTipView = CompressTipView(frame: self.view.bounds) var count = 0
self.view.addSubview(compressTipView) for data in self.model! {
compressTipView.callBack = {[weak self] allow in let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [data.ident], options: nil)
guard let self else {return} let assetToDelete = fetchResult.firstObject
PHPhotoLibrary.shared().performChanges ({
PHAssetChangeRequest.deleteAssets([assetToDelete] as NSFastEnumeration)
}){ success, error in
if(success){
self.updateCompressData(flag: data.ident)
print("删除文件成功")
}else {
if let error = error {
print("删除文件时出错: \(error.localizedDescription)")
}
}
count = count + 1
if count == self.model?.count{
self.jumpToCompressVC()
}
}
}
}
func jumpToCompressVC(){
DispatchQueue.main.async {
if let targetVC = self.navigationController?.viewControllers.first(where: { $0 is CompressController }) { if let targetVC = self.navigationController?.viewControllers.first(where: { $0 is CompressController }) {
self.navigationController?.popToViewController(targetVC, animated: true) self.navigationController?.popToViewController(targetVC, animated: true)
} }
// 如果不允许就两张都放进去,否则异步删除照片
if allow as! Bool == false {
// 删除照片逻辑
}
} }
} }
/// 更新数据
/// - Parameter flag: 图片的ident
func updateCompressData(flag : String){
DispatchQueue.main.async {
// 移除VC中的数据
let compressVC = self.navigationController?.viewControllers.first(where: { $0 is CompressController }) as! CompressController as CompressController
compressVC.resourceData.removeAll { $0.ident == flag }
// 移除单利中的数据
Singleton.shared.resourceModel.removeAll{ $0.ident == flag }
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
......
...@@ -96,19 +96,42 @@ class CompressController : BaseViewController { ...@@ -96,19 +96,42 @@ class CompressController : BaseViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .white view.backgroundColor = .white
self.navigationController?.navigationBar.isHidden = true self.navigationController?.navigationBar.isHidden = true
getViewData()
setUI() setUI()
} }
/// 获取当前页面数据
func getViewData(){
self.resourceData.removeAll()
let datas = Singleton.shared.resourceModel
if datas.count > 0 {
// 这里需要重新排序下
if self.currentSort == 0 {
self.resourceData = datas.sorted {$0.orgSize > $1.orgSize }
}else if self.currentSort == 1 {
self.resourceData = datas.sorted {$0.orgSize < $1.orgSize }
}else if self.currentSort == 2 {
self.resourceData = datas.sorted {$0.createDate > $1.createDate }
}else{
self.resourceData = datas.sorted {$0.createDate < $1.createDate }
}
}else{
CompressViewModel().getAllPhotosToAssets(sortType: self.currentSort, assetType: self.currentResourceType) { [weak self] models in
guard let self else {return}
self.resourceData = models
}
}
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// 这里默认去请求下当前的数据资源
let viewModel = CompressViewModel() // 目的是为了消除cell 的选择按钮状态
viewModel.getAllPhotos {[weak self] models in if self.selectedModel.count == 0 {
guard let self else {return} self.collectionView.reloadData()
self.resourceData = models
} }
} }
} }
...@@ -130,6 +153,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -130,6 +153,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompressSelectCell", for: indexPath) as! CompressSelectCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompressSelectCell", for: indexPath) as! CompressSelectCell
cell.model = self.resourceData[indexPath.row] cell.model = self.resourceData[indexPath.row]
cell.currentMediaType = self.currentResourceType
if self.selectedModel.count == 0 { if self.selectedModel.count == 0 {
cell.choose = false cell.choose = false
} }
...@@ -207,6 +231,23 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -207,6 +231,23 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
self.sortByType(sortType: self.currentSort, header: header) self.sortByType(sortType: self.currentSort, header: header)
} }
} }
header.changeView.callBack = {[weak self] flag in
guard let self else {return}
if self.currentResourceType != flag as! Int {
self.currentResourceType = flag as! Int
// 先移除下,防止点到
self.resourceData.removeAll()
// 如果是图片,直接从缓存中加载
if self.currentResourceType == 0 {
self.getViewData()
}else{
CompressViewModel().getAllPhotosToAssets(sortType: self.currentSort, assetType: flag as! Int) { [weak self] models in
guard let self else {return}
self.resourceData = models
}
}
}
}
header.modeData = self.resourceData header.modeData = self.resourceData
return header return header
}else{ }else{
...@@ -275,6 +316,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -275,6 +316,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
// 先将值传到下一个页面 // 先将值传到下一个页面
let vc : CompressQualityController = CompressQualityController() let vc : CompressQualityController = CompressQualityController()
vc.model = self.selectedModel vc.model = self.selectedModel
vc.currentMediaType = self.currentResourceType
vc.detailTiplabel.text = "You've selected \(self.selectedModel.count) out of \(self.resourceData.count) photos to compress." vc.detailTiplabel.text = "You've selected \(self.selectedModel.count) out of \(self.resourceData.count) photos to compress."
self.navigationController?.pushViewController(vc, animated: true) self.navigationController?.pushViewController(vc, animated: true)
...@@ -288,4 +330,6 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -288,4 +330,6 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
self.selectedModel.removeAll() self.selectedModel.removeAll()
self.updateSubmitButton() self.updateSubmitButton()
} }
} }
...@@ -20,6 +20,8 @@ class CompressQualityController : BaseViewController{ ...@@ -20,6 +20,8 @@ class CompressQualityController : BaseViewController{
var currentQulityType : Int = 0 var currentQulityType : Int = 0
var currentMediaType : Int = 0
private var compressNav:CompressNavView? private var compressNav:CompressNavView?
...@@ -179,6 +181,40 @@ class CompressQualityController : BaseViewController{ ...@@ -179,6 +181,40 @@ class CompressQualityController : BaseViewController{
} }
} }
fileprivate func updateNextView(_ compressAllSize: Double, _ compressingView: CompressingView,_ comDataSource : [Data],_ comVideoDataSource : [URL?]) {
DispatchQueue.main.async {
compressingView.removeFromSuperview()
let vc = CompressCompletedViewController()
vc.currentMediaType = self.currentMediaType
vc.comDataSource = comDataSource
vc.comVideoDataSource = comVideoDataSource
vc.model = self.model
vc.detailTipToplabel.text = "\(self.model!.count) items"
vc.detailTipBottomlabel.text = "\(self.model!.count) items"
var sum = 0.0
var orgAllSize = 0.0
for modelData in self.model! {
orgAllSize = orgAllSize + modelData.orgSize
}
sum = orgAllSize - compressAllSize
sum = sum / 1024
if sum < 1024{
vc.sizeToplabel.text = String(format:"%.2lfKB",sum)
}else if sum < (1024 * 1024) && sum > 1024{
sum = sum / 1024
vc.sizeToplabel.text = String(format:"%.2lfMB",sum)
}else{
sum = sum / (1024 * 1024)
vc.sizeToplabel.text = String(format:"%.2lfGB",sum)
}
let str = String(format:"%.2lf",(orgAllSize - compressAllSize) / orgAllSize)
vc.sizeBottomlabel.text = "\(str)%"
self.navigationController?.pushViewController(vc, animated: true)
}
}
@objc func submitAction(){ @objc func submitAction(){
let compressingView : CompressingView = CompressingView(frame: self.view.bounds) let compressingView : CompressingView = CompressingView(frame: self.view.bounds)
...@@ -195,77 +231,55 @@ class CompressQualityController : BaseViewController{ ...@@ -195,77 +231,55 @@ class CompressQualityController : BaseViewController{
if currentQulityType == 2 { if currentQulityType == 2 {
currentQulity = 0.8 currentQulity = 0.8
} }
manager.compress(assets: self.model!, compressionQuality: currentQulity) {progress in var comDataSource : [Data] = []
compressingView.animationView.setProgress(CGFloat(progress), animated: true, duration: 0.1)
} completion: { compressedDataArray, errorArray in if self.currentMediaType == 0 {
// 表示压缩图片
for (index, data) in compressedDataArray.enumerated() { manager.compress(assets: self.model!, compressionQuality: currentQulity) {progress in
if let error = errorArray[index] { compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
print("第 \(index + 1) 张图片压缩出错: \(error.localizedDescription)") } completion: { compressedDataArray, errorArray in
} else if let data = data {
print("第 \(index + 1) 张图片压缩完成,压缩后大小: \(data.count) 字节")
} else {
print("第 \(index + 1) 张图片压缩失败")
}
}
DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
compressingView.removeFromSuperview()
let vc = CompressCompletedViewController()
vc.model = self.model
vc.detailTipToplabel.text = "\(self.model!.count) items"
vc.detailTipBottomlabel.text = "\(self.model!.count) items"
var sum = 0.0
var orgAllSize = 0.0
var compressAllSize = 0.0 var compressAllSize = 0.0
for modelData in self.model! { for (index, data) in compressedDataArray.enumerated() {
orgAllSize = orgAllSize + modelData.orgSize if let error = errorArray[index] {
compressAllSize = compressAllSize + modelData.compressSize print("第 \(index + 1) 个文件压缩出错: \(error.localizedDescription)")
sum = sum + modelData.orgSize - modelData.compressSize } else if let data = data {
print("第 \(index + 1) 个文件压缩完成,压缩后大小: \(data.count) 字节")
compressAllSize = compressAllSize + Double(data.count)
comDataSource.append(data)
} else {
print("第 \(index + 1) 个文件压缩失败")
}
} }
if sum > 1000 { self.updateNextView(compressAllSize,compressingView,comDataSource,[])
sum = sum / 1024 }
vc.sizeToplabel.text = "\(sum)GB" }else{
}else{ // 压缩视频
vc.sizeToplabel.text = "\(sum)MB" var compressAllSize : Double = 0.0
manager.compressVideos(models: self.model!, quality: Float(currentQulity)) { (identifier, progress) in
compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
} completion: { (outputURLs, errors) in
for (index, outputURL) in outputURLs.enumerated() {
if let outputURL = outputURL {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: outputURL.path)
if let fileSize = attributes[.size] as? Int64 {
compressAllSize = compressAllSize + Double(fileSize)
}
} catch {
Print("获取视频文件大小失败")
}
print("Compressed video \(index) saved at: \(outputURL)")
} else if let error = errors[index] {
print("Error compressing video \(index): \(error.localizedDescription)")
}
} }
let str = String(format:"%.2lf",(orgAllSize - compressAllSize) / orgAllSize) self.updateNextView(compressAllSize,compressingView,[],outputURLs)
vc.sizeBottomlabel.text = "\(str)%"
self.navigationController?.pushViewController(vc, animated: true)
} }
} }
// DispatchQueue.main.async {
// compressingView.removeFromSuperview()
// let vc = CompressCompletedViewController()
// vc.model = self.model
// vc.detailTipToplabel.text = "\(self.model!.count) items"
// vc.detailTipBottomlabel.text = "\(self.model!.count) items"
//
// var sum = 0.0
// var orgAllSize = 0.0
// var compressAllSize = 0.0
// for modelData in self.model! {
// orgAllSize = orgAllSize + modelData.orgSize
// compressAllSize = compressAllSize + modelData.compressSize
// sum = sum + modelData.orgSize - modelData.compressSize
// }
// if sum > 1000 {
// sum = sum / 1024
// vc.sizeToplabel.text = "\(sum)GB"
// }else{
// vc.sizeToplabel.text = "\(sum)MB"
// }
// let str = String(format:"%.2lf",(orgAllSize - compressAllSize) / orgAllSize)
// vc.sizeBottomlabel.text = "\(str)%"
//
//
// self.navigationController?.pushViewController(vc, animated: true)
// }
} }
} }
//
// PreVideoController.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import Foundation
import AVFoundation
import Photos
class PreVideoController : BaseViewController {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private let localIdentifier: String
init(localIdentifier: String) {
self.localIdentifier = localIdentifier
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupVideoPlayer()
setupTapGesture()
}
private func setupVideoPlayer() {
guard let asset = fetchAsset() else { return }
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] (avAsset, _, _) in
guard let self = self, let avAsset = avAsset as? AVURLAsset else { return }
DispatchQueue.main.async {
self.player = AVPlayer(url: avAsset.url)
self.playerLayer = AVPlayerLayer(player: self.player)
self.playerLayer?.frame = CGRect(x: 0, y: self.titleView.height, width: self.view.width, height: self.view.height - self.titleView.height)
self.view.layer.addSublayer(self.playerLayer!)
self.player?.play()
}
}
}
private func fetchAsset() -> PHAsset? {
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
return fetchResult.firstObject
}
private func setupTapGesture() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapGesture)
}
@objc private func handleTap() {
if presentedViewController != nil {
dismiss(animated: true, completion: nil)
} else {
navigationController?.popViewController(animated: true)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
player?.pause()
}
}
//
// PreViewController.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import Foundation
class PreViewController : BaseViewController {
var imageIdent : String = ""
lazy var preImageView : UIImageView = {
let view = UIImageView(frame: CGRect(x: 0, y: self.titleView.height, width: self.view.width, height: self.view.height - self.titleView.height))
view.backgroundColor = .white
view.image = PhotoAndVideoMananger.mananger.getImageFromAssetID(id: self.imageIdent)
view.contentMode = .scaleAspectFit
view.clipsToBounds = true
view.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(selectClick))
view.addGestureRecognizer(tap)
return view
}()
@objc func selectClick(){
self.navigationController?.popViewController(animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.preImageView)
}
}
...@@ -10,16 +10,12 @@ import Photos ...@@ -10,16 +10,12 @@ import Photos
struct ResourceModel : Equatable{ struct ResourceModel : Equatable{
var ident : String var ident : String
var compressSize : Double
var orgSize: Double var orgSize: Double
var createDate : Date var createDate : Date
var imageAsset: PHAsset
init(ident: String, compressSize: Double, orgSize: Double, createDate: Date, imageAsset: PHAsset) { init(ident: String, orgSize: Double, createDate: Date) {
self.ident = ident self.ident = ident
self.compressSize = compressSize
self.orgSize = orgSize self.orgSize = orgSize
self.createDate = createDate self.createDate = createDate
self.imageAsset = imageAsset
} }
} }
...@@ -19,18 +19,24 @@ class CompressCustomHeaderView: UICollectionReusableView{ ...@@ -19,18 +19,24 @@ class CompressCustomHeaderView: UICollectionReusableView{
var saveSum = 0.0 var saveSum = 0.0
for model in self.modeData{ for model in self.modeData{
sum = sum + model.orgSize sum = sum + model.orgSize
saveSum = saveSum + (model.orgSize - model.compressSize) saveSum = saveSum + model.orgSize * 0.8
} }
if sum > 1000 { sum = sum / 1000
self.siezLabel.text = String(format: "%.2f GB" ,(sum/1024)) saveSum = saveSum / 1024
if sum < 1024 {
self.siezLabel.text = String(format: "%.2f KB" ,(sum))
}else if sum < (1024 * 1024) && sum > 1024{
self.siezLabel.text = String(format: "%.2f MB" ,(sum/1024))
}else{ }else{
self.siezLabel.text = String(format: "%.2f MB" ,(sum)) self.siezLabel.text = String(format: "%.2f GB" ,sum/(1024*1024))
} }
if saveSum > 1000 { if saveSum < 1024 {
self.saveSizeLabel.text = String(format: "%.2f GB" ,(saveSum/1024.0)) self.saveSizeLabel.text = String(format: "%.2f KB" ,(saveSum))
}else { }else if saveSum < (1024 * 1024) && saveSum > 1024{
self.saveSizeLabel.text = String(format: "%.2f MB" ,(saveSum)) self.saveSizeLabel.text = String(format: "%.2f MB" ,(saveSum/1024))
}else{
self.saveSizeLabel.text = String(format: "%.2f GB" ,saveSum/(1024*1024))
} }
} }
...@@ -152,7 +158,7 @@ class CompressCustomHeaderView: UICollectionReusableView{ ...@@ -152,7 +158,7 @@ class CompressCustomHeaderView: UICollectionReusableView{
self.selectlabel.snp.makeConstraints { make in self.selectlabel.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-12) make.right.equalToSuperview().offset(-12)
make.top.equalToSuperview().offset(6) make.top.equalToSuperview().offset(6)
make.width.equalTo(51 * RScreenW()) make.width.equalTo(60 * RScreenW())
make.height.equalTo(20) make.height.equalTo(20)
} }
self.siezLabel.snp.makeConstraints { make in self.siezLabel.snp.makeConstraints { make in
...@@ -196,6 +202,7 @@ class CompressCustomHeaderView: UICollectionReusableView{ ...@@ -196,6 +202,7 @@ class CompressCustomHeaderView: UICollectionReusableView{
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.backgroundColor = .white
setUI() setUI()
......
...@@ -98,6 +98,12 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource { ...@@ -98,6 +98,12 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource {
lazy var backView : UIView = { lazy var backView : UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000) view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000)
view.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(backViewClick))
view.addGestureRecognizer(tap)
return view return view
}() }()
...@@ -154,6 +160,9 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource { ...@@ -154,6 +160,9 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource {
// 移除自身 // 移除自身
self.removeFromSuperview() self.removeFromSuperview()
} }
@objc func backViewClick(){
// 移除自身
self.removeFromSuperview()
}
} }
...@@ -9,6 +9,8 @@ import Foundation ...@@ -9,6 +9,8 @@ import Foundation
class CompressSwitchView : UIView { class CompressSwitchView : UIView {
var callBack: callBack<Any> = {flag in }
var currentButton : UIButton? var currentButton : UIButton?
lazy var leftButton : UIButton = { lazy var leftButton : UIButton = {
...@@ -18,6 +20,7 @@ class CompressSwitchView : UIView { ...@@ -18,6 +20,7 @@ class CompressSwitchView : UIView {
button.setTitle("Photos", for: .normal) button.setTitle("Photos", for: .normal)
button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside) button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside)
button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal) button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal)
button.tag = 1000
return button return button
}() }()
...@@ -28,6 +31,7 @@ class CompressSwitchView : UIView { ...@@ -28,6 +31,7 @@ class CompressSwitchView : UIView {
button.setTitle("Videos", for: .normal) button.setTitle("Videos", for: .normal)
button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside) button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside)
button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal) button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal)
button.tag = 1001
return button return button
}() }()
...@@ -74,6 +78,8 @@ class CompressSwitchView : UIView { ...@@ -74,6 +78,8 @@ class CompressSwitchView : UIView {
sender.setTitleColor(UIColor(red: 0, green: 0.51, blue: 1, alpha: 1), for: .normal) sender.setTitleColor(UIColor(red: 0, green: 0.51, blue: 1, alpha: 1), for: .normal)
sender.backgroundColor = .white sender.backgroundColor = .white
self.currentButton = sender self.currentButton = sender
self.callBack(sender.tag - 1000)
} }
} }
...@@ -86,7 +86,7 @@ class CompressTipView : UIView { ...@@ -86,7 +86,7 @@ class CompressTipView : UIView {
lazy var tipLabel: UILabel = { lazy var tipLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "Allow ”Al Cleaner“ to delete2 photos?" label.text = "Allow ”Al Cleaner“ to delete 2 photos?"
label.textAlignment = .center label.textAlignment = .center
label.numberOfLines = 0 label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 17, weight: .bold) label.font = UIFont.systemFont(ofSize: 17, weight: .bold)
......
...@@ -37,17 +37,22 @@ class PhotoDataManager { ...@@ -37,17 +37,22 @@ class PhotoDataManager {
func loadFromFileSystem(filename: String = "photosManagerData.json",resultModel:@escaping (_ model:PhotosManagerModel) -> () = {mdoel in}) { func loadFromFileSystem(filename: String = "photosManagerData.json",resultModel:@escaping (_ model:PhotosManagerModel) -> () = {mdoel in}) {
let url = getDocumentsDirectory().appendingPathComponent(filename) let url = getDocumentsDirectory().appendingPathComponent(filename)
do { // do {
let data = try Data(contentsOf: url) // let data = try Data(contentsOf: url)
let decoder = JSONDecoder() // let decoder = JSONDecoder()
let model = try decoder.decode(PhotosManagerModel.self, from: data) // let model = try decoder.decode(PhotosManagerModel.self, from: data)
resultModel(model) // resultModel(model)
} catch { // } catch {
//
// loadDataFromPhotos { model in
//
// resultModel(model)
// }
// }
loadDataFromPhotos { model in
loadDataFromPhotos { model in resultModel(model)
resultModel(model)
}
} }
} }
...@@ -72,65 +77,76 @@ class PhotoDataManager { ...@@ -72,65 +77,76 @@ class PhotoDataManager {
let allModel:PhotosManagerModel = PhotosManagerModel(allFileNumber: 0, allFileSize: 0, titleModelArray: [model1,model2], otherModelArray: [model3,model4,model5,model6,model7]) let allModel:PhotosManagerModel = PhotosManagerModel(allFileNumber: 0, allFileSize: 0, titleModelArray: [model1,model2], otherModelArray: [model3,model4,model5,model6,model7])
resultModel(allModel) resultModel(allModel)
PhotoAndVideoMananger.mananger.doCompareSimilarPhotos(assets: PhotoAndVideoMananger.mananger.allAssets) { currentAssets in
model1.assets = currentAssets
model1.allFileSize = 0
resultModel(allModel)
} completeHandler: { finallyAsset in
model1.assets = finallyAsset
model1.allFileSize = 0
resultModel(allModel)
}
PhotoAndVideoMananger.mananger.fetXSOther { array in // PhotoAndVideoMananger.mananger.fetXSOther { array in
//
let dispatchGroup = DispatchGroup() // let dispatchGroup = DispatchGroup()
//
dispatchGroup.enter() // dispatchGroup.enter()
PhotoSimilarityFinder.processSimilarPhotoGroups(simalr:1,assetGroups: array) { array,fileSize in // PhotoSimilarityFinder.processSimilarPhotoGroups(simalr:1,assetGroups: array) { array,fileSize in
//
model1.assets = array // model1.assets = array
model1.allFileSize = fileSize // model1.allFileSize = fileSize
//
resultModel(allModel) // resultModel(allModel)
//
dispatchGroup.leave() // dispatchGroup.leave()
} // }
//
dispatchGroup.enter() // dispatchGroup.enter()
PhotoSimilarityFinder.processSimilarPhotoGroups(assetGroups: array) {array,fileSize in // PhotoSimilarityFinder.processSimilarPhotoGroups(assetGroups: array) {array,fileSize in
//
model2.assets = array // model2.assets = array
model2.allFileSize = fileSize // model2.allFileSize = fileSize
//
resultModel(allModel) // resultModel(allModel)
//
dispatchGroup.leave() // dispatchGroup.leave()
} // }
dispatchGroup.enter() // dispatchGroup.enter()
model2.assets = [ResourceManager.manager.getAllVideo()] // model2.assets = [ResourceManager.manager.getAllVideo()]
dispatchGroup.leave() // dispatchGroup.leave()
//
dispatchGroup.enter() // dispatchGroup.enter()
PhotoAndVideoMananger.mananger.fetXSVideo { array in // PhotoAndVideoMananger.mananger.fetXSVideo { array in
//
PhotoSimilarityFinder.processSimilarVideoGroups(videoGroups: array) {ids in // PhotoSimilarityFinder.processSimilarVideoGroups(videoGroups: array) {ids in
//
model4.assets = ids // model4.assets = ids
//
resultModel(allModel) // resultModel(allModel)
//
dispatchGroup.leave() // dispatchGroup.leave()
} // }
} // }
//
//
//
//
//
dispatchGroup.notify(queue: .main) { // dispatchGroup.notify(queue: .main) {
print("所有异步操作都已完成") // print("所有异步操作都已完成")
//
if model1.assets.count != 0 || model2.assets.count != 0 || model3.assets.count != 0 { // if model1.assets.count != 0 || model2.assets.count != 0 || model3.assets.count != 0 {
//
PhotoDataManager.manager.saveToFileSystem(model: allModel) // PhotoDataManager.manager.saveToFileSystem(model: allModel)
resultModel(allModel) // resultModel(allModel)
} // }
//
} // }
} // }
......
...@@ -50,6 +50,119 @@ class PhotoAndVideoMananger { ...@@ -50,6 +50,119 @@ class PhotoAndVideoMananger {
var ids:[String] = [] var ids:[String] = []
// func doCompareSimilarPhotos(assets:[PHAsset],processHandler:@escaping ([[String]])->Void,completeHandler:@escaping ([[String]])->Void){
//
// let syncQueue = DispatchQueue(label: "com.example.syncQueue")
// // 创建信号量,最多允许3个任务同时执行
// let semaphore = DispatchSemaphore(value: 2)
// let options = PHImageRequestOptions()
// options.isSynchronous = true
// options.deliveryMode = .highQualityFormat
//
// // 双重循环
// var groupAssets : [[String]] = []
// for index in 0..<assets.count{
//
// var currentGroup : [String] = []
// //获取第一张图片
// currentGroup.append(assets[index].localIdentifier)
// let nextIndex = index + 1
// if (nextIndex < assets.count - 1){
// semaphore.wait()
// syncQueue.async {
// PHImageManager.default().requestImage(for: assets[index], targetSize: CGSizeMake(400, 400), contentMode:.aspectFit, options: options) { (image1, _) in
//
// for changeIndex in nextIndex..<assets.count{
// PHImageManager.default().requestImage(for: assets[changeIndex], targetSize: CGSizeMake(400, 400), contentMode:.aspectFit, options: options) { (image2, _) in
// let isSimilar = OpenCVWrapper.areImagesSimilar(image1, withImage2: image2, threshold: 0.99)
// if isSimilar {
// currentGroup.append(assets[changeIndex].localIdentifier)
// }
// }
// }
// }
// }
// if currentGroup.count >= 2 {
// groupAssets.append(currentGroup)
// processHandler(groupAssets)
// }
// semaphore.signal()
// }
//
// }
//
// completeHandler(groupAssets)
// }
let imageCache = NSCache<NSString, UIImage>()
func getCacheImage(asset:PHAsset)->UIImage{
let imageTemp = imageCache.object(forKey: asset.localIdentifier as NSString)
if imageTemp != nil {
return imageTemp!
}
let options = PHImageRequestOptions()
options.isSynchronous = true
options.deliveryMode = .fastFormat
options.resizeMode = .exact
PHImageManager.default().requestImage(for: asset, targetSize: CGSizeMake(100, 100), contentMode:.aspectFit, options: options) { (image, _) in
if image != nil {
self.imageCache.setObject(image!, forKey: asset.localIdentifier as NSString)
}
}
return getCacheImage(asset: asset)
}
func doCompareSimilarPhotos(assets:[PHAsset],processHandler:@escaping ([[String]])->Void,completeHandler:@escaping ([[String]])->Void){
let options = PHImageRequestOptions()
options.isSynchronous = true
options.deliveryMode = .highQualityFormat
let customQueue = OperationQueue()
// 设置最大并发操作数,可根据实际情况调整
customQueue.maxConcurrentOperationCount = 3
DispatchQueue.global().async {
// 双重循环
Print("开始时间\(Date())")
var groupAssets : [[String]] = []
for index in 0..<assets.count{
var currentGroup : [String] = []
//获取第一张图片
currentGroup.append(assets[index].localIdentifier)
let nextIndex = index + 1
let operation = BlockOperation {
if (nextIndex < assets.count - 1){
for changeIndex in nextIndex..<assets.count{
let isSimilar = OpenCVWrapper.areImagesSimilar(self.getCacheImage(asset: assets[index]), withImage2: self.getCacheImage(asset: assets[changeIndex]), threshold: 0.999)
if isSimilar {
currentGroup.append(assets[changeIndex].localIdentifier)
}
}
}
if currentGroup.count >= 2 {
groupAssets.append(currentGroup)
processHandler(groupAssets)
Print("执行一次时间\(Date()),当前序列号:\(index)")
}
}
customQueue.addOperation(operation)
// 一次执行完成需要将这个image从cache中移除
self.imageCache.removeObject(forKey: assets[index].localIdentifier as NSString)
}
Print("完成时间\(Date())")
completeHandler(groupAssets)
}
}
func setAssets() { func setAssets() {
let fetchOptions = PHFetchOptions() let fetchOptions = PHFetchOptions()
......
//
// Singleton.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import Foundation
class Singleton {
// 使用静态常量来保存单例实例
static let shared = Singleton()
// 私有化初始化方法,防止外部创建新实例
private init() {}
var resourceModel : [ResourceModel] = []
}
...@@ -9,10 +9,10 @@ ...@@ -9,10 +9,10 @@
<key>NSAllowsArbitraryLoadsInWebContent</key> <key>NSAllowsArbitraryLoadsInWebContent</key>
<true/> <true/>
</dict> </dict>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>LaunchAppIntent</string> <string>LaunchAppIntent</string>
</array> </array>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment