Commit 59244d26 authored by edy's avatar edy

photomanager

parent 586f057f
......@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
8767D08846C136C74D7A38AD /* Pods_PhoneManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB0D368334BECAFE6594C5F7 /* Pods_PhoneManager.framework */; };
3A00E856852A8783E544CD7D /* Pods_PhoneManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6028F60B696E2F97EAA2325C /* Pods_PhoneManager.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
B29A159C241A6A66EFE07177 /* Pods-PhoneManager.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PhoneManager.debug.xcconfig"; path = "Target Support Files/Pods-PhoneManager/Pods-PhoneManager.debug.xcconfig"; sourceTree = "<group>"; };
CC121720387E40164142F3E2 /* Pods-PhoneManager.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PhoneManager.release.xcconfig"; path = "Target Support Files/Pods-PhoneManager/Pods-PhoneManager.release.xcconfig"; sourceTree = "<group>"; };
DB0D368334BECAFE6594C5F7 /* Pods_PhoneManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PhoneManager.framework; sourceTree = BUILT_PRODUCTS_DIR; };
295785B9009F20AC4C1C36C4 /* Pods-PhoneManager.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PhoneManager.debug.xcconfig"; path = "Target Support Files/Pods-PhoneManager/Pods-PhoneManager.debug.xcconfig"; sourceTree = "<group>"; };
6028F60B696E2F97EAA2325C /* Pods_PhoneManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PhoneManager.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8937DC9D81CEDE823C329A80 /* Pods-PhoneManager.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PhoneManager.release.xcconfig"; path = "Target Support Files/Pods-PhoneManager/Pods-PhoneManager.release.xcconfig"; sourceTree = "<group>"; };
EB388E5B2D8A61A800629B0D /* PhoneManager.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhoneManager.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
......@@ -43,17 +43,17 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8767D08846C136C74D7A38AD /* Pods_PhoneManager.framework in Frameworks */,
3A00E856852A8783E544CD7D /* Pods_PhoneManager.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1658DB6C9F7C23ADB69D9D4C /* Frameworks */ = {
27ECDADD9059AB5043B8E1E9 /* Frameworks */ = {
isa = PBXGroup;
children = (
DB0D368334BECAFE6594C5F7 /* Pods_PhoneManager.framework */,
6028F60B696E2F97EAA2325C /* Pods_PhoneManager.framework */,
);
name = Frameworks;
sourceTree = "<group>";
......@@ -61,8 +61,8 @@
CB2ACD1E9442B4500087E831 /* Pods */ = {
isa = PBXGroup;
children = (
B29A159C241A6A66EFE07177 /* Pods-PhoneManager.debug.xcconfig */,
CC121720387E40164142F3E2 /* Pods-PhoneManager.release.xcconfig */,
295785B9009F20AC4C1C36C4 /* Pods-PhoneManager.debug.xcconfig */,
8937DC9D81CEDE823C329A80 /* Pods-PhoneManager.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
......@@ -73,7 +73,7 @@
EB388E5D2D8A61A800629B0D /* PhoneManager */,
EB388E5C2D8A61A800629B0D /* Products */,
CB2ACD1E9442B4500087E831 /* Pods */,
1658DB6C9F7C23ADB69D9D4C /* Frameworks */,
27ECDADD9059AB5043B8E1E9 /* Frameworks */,
);
sourceTree = "<group>";
};
......@@ -92,11 +92,11 @@
isa = PBXNativeTarget;
buildConfigurationList = EB388E6E2D8A61AA00629B0D /* Build configuration list for PBXNativeTarget "PhoneManager" */;
buildPhases = (
0EB1FE5C41C26445102D1AF8 /* [CP] Check Pods Manifest.lock */,
594FD43819933850E07C4C9C /* [CP] Check Pods Manifest.lock */,
EB388E572D8A61A800629B0D /* Sources */,
EB388E582D8A61A800629B0D /* Frameworks */,
EB388E592D8A61A800629B0D /* Resources */,
5B498BF73901C867A61ED7C3 /* [CP] Embed Pods Frameworks */,
B06228D82143041809F900CE /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
......@@ -122,6 +122,7 @@
TargetAttributes = {
EB388E5A2D8A61A800629B0D = {
CreatedOnToolsVersion = 16.2;
LastSwiftMigration = 1620;
};
};
};
......@@ -155,7 +156,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0EB1FE5C41C26445102D1AF8 /* [CP] Check Pods Manifest.lock */ = {
594FD43819933850E07C4C9C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
......@@ -177,7 +178,7 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
5B498BF73901C867A61ED7C3 /* [CP] Embed Pods Frameworks */ = {
B06228D82143041809F900CE /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
......@@ -209,15 +210,21 @@
/* Begin XCBuildConfiguration section */
EB388E6F2D8A61AA00629B0D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B29A159C241A6A66EFE07177 /* Pods-PhoneManager.debug.xcconfig */;
baseConfigurationReference = 295785B9009F20AC4C1C36C4 /* Pods-PhoneManager.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4D62BC7QXG;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6K23946NQ5;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PhoneManager/Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "We need to access the network to load content";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "We need album permission to access image storage information";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
......@@ -229,9 +236,13 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager.PhoneManager;
PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = phonemanager_dev;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
......@@ -239,15 +250,21 @@
};
EB388E702D8A61AA00629B0D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = CC121720387E40164142F3E2 /* Pods-PhoneManager.release.xcconfig */;
baseConfigurationReference = 8937DC9D81CEDE823C329A80 /* Pods-PhoneManager.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4D62BC7QXG;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 6K23946NQ5;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PhoneManager/Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "We need to access the network to load content";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "We need album permission to access image storage information";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
......@@ -259,9 +276,12 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager.PhoneManager;
PRODUCT_BUNDLE_IDENTIFIER = com.app.phonemanager;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = phonemanager_dev;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
......@@ -312,6 +332,7 @@
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
"CV_NO_OBJC_KEYWORDS=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
......@@ -370,6 +391,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "CV_NO_OBJC_KEYWORDS=1";
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
......
......@@ -6,6 +6,7 @@
//
import UIKit
import AppIntents
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
......@@ -13,7 +14,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setupDefault()
window = UIWindow(frame: UIScreen.main.bounds)
......@@ -33,8 +33,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
private func setupDefault() {
NetStatusManager.manager.startNet { status in
switch status {
case .NoNet:
break
case .WIFI,.WWAN:
if (PhotoAndVideoMananger.mananger.allAssets.count == 0) {
PhotoAndVideoMananger.mananger.setAssets()
}
}
}
if #available(iOS 13.0, *) {
let window = UIApplication.shared.windows.first
......
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "IMG_0944.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "IMG_0982.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "IMG_0983.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "IMG_0984.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "IMG_0985 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_collect_com.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_collect_com@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_collect_com@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_details_charging.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_details_charging@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_details_charging@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_sel_com.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_sel_com@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_sel_com@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_unsel_com.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_unsel_com@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_unsel_com@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_check_similar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_check_similar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_check_similar@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_close_similar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_close_similar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_close_similar@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "icon_left_setting.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "icon_left_setting@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "icon_left_setting@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
//
// ChargeInfoViewController.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import UIKit
class ChargeInfoViewController:BaseViewController {
enum ChargeInfoType {
case setting
case charge
}
var model:ChargeViewCollectionModel?
var type:ChargeInfoType?
var isShowBack:Bool? {
didSet {
changeTitleView()
}
}
var isShowSettingView:Bool? {
didSet {
changeSettingViews()
}
}
lazy var backImageView:ChargeInfoBackView = {
let sview:ChargeInfoBackView = ChargeInfoBackView(frame: view.bounds, backImage: model?.CoverImage ?? "")
return sview
}()
lazy var settingView:ChargeInfoSettingView = {
let sview:ChargeInfoSettingView = ChargeInfoSettingView(frame: CGRect(x: 0, y: 0, width: view.width, height: 78 + safeHeight))
sview.isHidden = type == .setting ? false : true
return sview
}()
init(model: ChargeViewCollectionModel?,type:ChargeInfoType?) {
self.type = type
self.model = model
super.init(nibName: nil, bundle: nil)
}
// 由于继承自 UIViewController,必须实现这个必需的构造器
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
self.barHidden = true
titleView.model.title = ""
titleView.lineView.isHidden = true
titleView.backgroundColor = .clear
if type == .charge {
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {[weak self] in
guard let self else {return}
isShowBack = true
})
}
}
override func addViews() {
view.addSubview(backImageView)
view.addSubview(titleView)
view.addSubview(settingView)
}
func changeSettingViews() {
if type == .charge {
return
}
DispatchQueue.main.async {
if !(self.isShowSettingView ?? false) {
self.settingView.isHidden = false
}
UIView.animate(withDuration: 0.3, animations: {[weak self] in
guard let self else {return}
self.settingView.alpha = (self.isShowSettingView ?? false) ? 0 : 1
}, completion: {[weak self] _ in
guard let self else {return}
self.settingView.isHidden = self.isShowSettingView ?? false
})
}
}
func changeTitleView() {
if !(self.isShowBack ?? false) {
self.titleView.isHidden = false
}
UIView.animate(withDuration: 0.3, animations: {[weak self] in
guard let self else {return}
self.titleView.alpha = (self.isShowBack ?? false) ? 0 : 1
}, completion: {[weak self] _ in
guard let self else {return}
self.titleView.isHidden = self.isShowBack ?? false
})
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
backImageView.snp.makeConstraints { make in
make.center.width.height.equalToSuperview()
}
settingView.snp.makeConstraints { make in
make.bottom.width.centerX.equalToSuperview()
make.height.equalTo(safeHeight + (safeHeight == 0 ? 78 : 68))
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isShowSettingView = !(isShowSettingView ?? false)
isShowBack = !(isShowBack ?? false)
}
}
//
// ChargeViewController.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import UIKit
class ChargeViewController:BaseViewController {
lazy var detailsBtn:UIButton = {
let sview:UIButton = UIButton()
sview.setImage(UIImage(named: "ic_details_charging"), for: .normal)
sview.width = 20
sview.height = 20
sview.x = view.width - sview.width - 15
sview.centerY = navCenterY
return sview
}()
lazy var chargeView:ChargeView = {
let cY:CGFloat = titleView.height + titleView.y
let sview:ChargeView = ChargeView(frame: CGRect(x: 0, y: cY, width: view.width, height: view.height - cY))
sview.callBack = {[weak self] model in
guard let self else {return}
DispatchQueue.main.async {[weak self] in
guard let self else {return}
if let cModel = model as? ChargeViewCollectionModel {
let vc:ChargeInfoViewController = ChargeInfoViewController(model: cModel, type: ChargeInfoViewController.ChargeInfoType.setting)
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
return sview
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
titleView.addSubview(detailsBtn)
view.addSubview(chargeView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.barHidden = false
}
}
[
{
"isFree": true,
"CoverImage": "IMG_0985",
"filePath":"",
},
{
"isFree": true,
"CoverImage": "IMG_0984",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0983",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0982",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0944",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0985",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0984",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0983",
"filePath":"",
},
{
"isFree": false,
"CoverImage": "IMG_0982",
"filePath":"",
},
]
//
// HomeTitleCollectionModel.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import Foundation
func loadChargeImtesSONFromBundle() -> [ChargeViewCollectionModel]? {
// 获取 JSON 文件路径
guard let path = Bundle.main.path(forResource: "ChargeItemsData", ofType: "json") else {
print("未找到 JSON 文件")
return nil
}
do {
// 读取文件内容
let data = try Data(contentsOf: URL(fileURLWithPath: path))
// 解析 JSON 数据
let decoder = JSONDecoder()
let items = try decoder.decode([ChargeViewCollectionModel].self, from: data)
return items
} catch {
print("解析 JSON 失败:\(error)")
return nil
}
}
struct ChargeViewCollectionModel:Codable {
var isFree:Bool
var CoverImage:String
var filePath:String
enum Category: String, Codable {
case isFree,CoverImage,filePath
}
}
//
// ChargeInfoBackView.swift
// PhoneManager
//
// Created by edy on 2025/3/27.
//
import UIKit
import SnapKit
class ChargeInfoBackView:UIView {
var backImage:String?
private var timer: Timer?
lazy var backImageView:UIImageView = {
let sview:UIImageView = UIImageView()
sview.contentMode = .scaleToFill
sview.clipsToBounds = true
sview.isUserInteractionEnabled = true
return sview
}()
let timeLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 72, weight: .bold)
label.textColor = .white
label.textAlignment = .center
label.sizeToFit()
return label
}()
let weekLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 20, weight: .regular)
label.textColor = .white
label.textAlignment = .center
label.sizeToFit()
return label
}()
let batteryLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 40, weight: .regular)
label.textColor = .white
label.textAlignment = .center
label.sizeToFit()
return label
}()
init(frame: CGRect,backImage:String) {
self.backImage = backImage
super.init(frame: frame)
setupUI()
setupTimeUpdates()
BatteryMonitorManager.shared.delegate = self
BatteryMonitorManager.shared.startMonitoring()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backImageView.image = UIImage(named: self.backImage ?? "")
self.addSubview(backImageView)
self.addSubview(timeLabel)
self.addSubview(weekLabel)
self.addSubview(batteryLabel)
}
func setupTimeUpdates() {
// 初始更新
updateTime()
startTimer()
// 监听系统时间变化通知
NotificationCenter.default.addObserver(
self,
selector: #selector(updateTime),
name: UIApplication.significantTimeChangeNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(updateTime),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
func startTimer() {
stopTimer()
timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in
self?.updateTime()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
@objc func updateTime() {
let text = timeFormatter()
let attributedString = NSMutableAttributedString(string: text)
// 设置前部文本样式(除最后2个字符外)
let mainTextRange = NSRange(location: 0, length: max(text.count - 2, 0))
if mainTextRange.length > 0 {
attributedString.addAttribute(
.font,
value: UIFont.systemFont(ofSize: 72, weight: .regular),
range: mainTextRange
)
}
// 设置最后2个字符的样式
let lastTwoRange = NSRange(location: max(text.count - 2, 0), length: min(2, text.count))
if lastTwoRange.length > 0 {
attributedString.addAttribute(
.font,
value: UIFont.systemFont(ofSize: 28, weight: .regular),
range: lastTwoRange
)
}
// 应用到UILabel
DispatchQueue.main.async {
self.timeLabel.attributedText = attributedString
self.weekLabel.text = weekFormatter()
}
}
func changeBattery(level:Float) {
let text = String(format: "%.0f", (level * 100)) + "%"
let attributedString = NSMutableAttributedString(string: text)
// 设置前部文本样式(除最后2个字符外)
let mainTextRange = NSRange(location: 0, length: max(text.count - 1, 0))
if mainTextRange.length > 0 {
attributedString.addAttribute(
.font,
value: UIFont.systemFont(ofSize: 40, weight: .regular),
range: mainTextRange
)
}
// 设置最后2个字符的样式
let lastTwoRange = NSRange(location: max(text.count - 1, 0), length: min(1, text.count))
if lastTwoRange.length > 0 {
attributedString.addAttribute(
.font,
value: UIFont.systemFont(ofSize: 24, weight: .regular),
range: lastTwoRange
)
}
// 应用到UILabel
DispatchQueue.main.async {
self.batteryLabel.attributedText = attributedString
}
}
override func layoutSubviews() {
super.layoutSubviews()
backImageView.snp.makeConstraints { make in
make.centerX.width.height.equalToSuperview()
}
timeLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(statusBarHeight + 76)
}
weekLabel.sizeToFit()
weekLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(timeLabel.snp.bottom)
}
batteryLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-(safeHeight + 76))
}
}
deinit {
stopTimer()
BatteryMonitorManager.shared.stopMonitoring()
NotificationCenter.default.removeObserver(self)
Print("deinit")
}
}
extension ChargeInfoBackView:BatteryStatusDelegate {
func batteryStatusDidUpdate(level: Float, state: UIDevice.BatteryState) {
changeBattery(level: level)
}
}
//
// ChargeInfoSettingView.swift
// PhoneManager
//
// Created by edy on 2025/3/27.
//
import UIKit
class ChargeInfoSettingView:UIView {
lazy var settingBtn:UIButton = {
let sview:UIButton = UIButton()
sview.backgroundColor = UIColor.colorWithHex(hexStr: mColor)
sview.setTitle("Set Animation", for: .normal)
sview.setTitleColor(UIColor.white, for: .normal)
sview.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
sview.width = width - 2 * marginLR
sview.height = 46
sview.centerX = width / 2
sview.y = marginLR
sview.layer.cornerRadius = sview.height / 2
sview.layer.masksToBounds = true
sview.addTarget(self, action: #selector(settingBtnClick), for: .touchUpInside)
return sview
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backgroundColor = .white
self.addSubview(settingBtn)
}
@objc func settingBtnClick() {
}
}
//
// ChargeView.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import UIKit
class ChargeView:UIView {
var callBack:callBack<Any> = {model in }
let footerID:String = "footerID"
lazy var models:[ChargeViewCollectionModel] = []
lazy var collectionView:UICollectionView = {
let cY:CGFloat = 0.RW()
let layout = WaterfallMutiSectionFlowLayout()
layout.delegate = self
let sview:UICollectionView = UICollectionView.init(frame: CGRect(x: marginLR, y: cY, width: width - 2 * marginLR, height: height - cY), collectionViewLayout: layout)
sview.dataSource = self
sview.delegate = self
sview.showsVerticalScrollIndicator = false
sview.register(ChargeViewCollectionCell.self, forCellWithReuseIdentifier: ChargeViewCollectionCell.identifiers)
sview.register(
ChargeHeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: ChargeHeaderView.identifiers
)
sview.register(
UICollectionReusableView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
withReuseIdentifier: footerID
)
if #available(iOS 11.0, *) {
sview.contentInsetAdjustmentBehavior = .never
}
sview.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: safeHeight + 16, right: 0)
sview.backgroundColor = .clear
return sview
}()
override init(frame: CGRect) {
super.init(frame: frame)
models = loadChargeImtesSONFromBundle() ?? []
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backgroundColor = .white
self.addSubview(collectionView)
}
}
extension ChargeView:WaterfallMutiSectionDelegate,UICollectionViewDataSource,UICollectionViewDelegate {
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: WaterfallMutiSectionFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
return 297
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ChargeViewCollectionCell.identifiers, for: indexPath) as! ChargeViewCollectionCell
cell.model = models[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = models[indexPath.row]
self.callBack(model)
}
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: ChargeHeaderView.identifiers,
for: indexPath
) as! ChargeHeaderView
header.titleLabel.text = "Charging Animation"
return header
}else {
let footer = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: footerID,
for: indexPath
)
return footer
}
}
func columnNumber(collectionView collection: UICollectionView, layout: WaterfallMutiSectionFlowLayout, section: Int) -> Int {
return 2
}
func lineSpacing(collectionView collection: UICollectionView, layout: WaterfallMutiSectionFlowLayout, section: Int) -> CGFloat {
return 11
}
func interitemSpacing(collectionView collection: UICollectionView, layout: WaterfallMutiSectionFlowLayout, section: Int) -> CGFloat {
return 11
}
func referenceSizeForHeader(collectionView collection: UICollectionView, layout: WaterfallMutiSectionFlowLayout, section: Int) -> CGSize {
return CGSize(width: collection.width, height: 56)
}
}
class ChargeHeaderView: UICollectionReusableView {
static let identifiers = "ChargeHeaderViewID"
let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 20)
label.textColor = UIColor.colorWithHex(hexStr: black3Color)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
backgroundColor = .clear
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
titleLabel.topAnchor.constraint(equalTo: topAnchor),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
//
// ChargeViewCollectionCell.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import UIKit
import SnapKit
class ChargeViewCollectionCell:UICollectionViewCell {
static let identifiers = "ChargeViewCollectionCellID"
lazy var backImageView:UIImageView = {
let sview:UIImageView = UIImageView()
sview.contentMode = .scaleAspectFill
sview.clipsToBounds = true
return sview
}()
lazy var isFreeBtn:UIButton = {
let sview:UIButton = UIButton()
sview.setImage(UIImage(named: "ic_collect_com"), for: .normal)
return sview
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
addSubview1()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backgroundColor = UIColor.random
layer.cornerRadius = 12
layer.masksToBounds = true
}
func addSubview1() {
self.contentView.addSubview(backImageView)
self.contentView.addSubview(isFreeBtn)
}
var model:ChargeViewCollectionModel! {
didSet {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.backImageView.image = UIImage.init(named: model.CoverImage)
self.isFreeBtn.isHidden = model.isFree
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
backImageView.snp.makeConstraints { make in
make.center.width.height.equalToSuperview()
}
isFreeBtn.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.right.equalToSuperview().offset(-12)
make.width.height.equalTo(24)
}
}
}
//
// HomeInfoViewController.swift
// PhoneManager
//
// Created by edy on 2025/3/25.
//
import UIKit
class HomeInfoViewController:BaseViewController {
private var type:PhotsFileType?
lazy var seletedAllBtn:UIButton = {
let btn:UIButton = UIButton(frame: CGRect(x: 0, y: 0, width: 115, height: 32))
btn.addTarget(self, action: #selector(seletedAllBtnClick), for: .touchUpInside)
btn.backgroundColor = UIColor.colorWithHex(hexStr: "#F2F6FC")
btn.setImage(UIImage.init(named: "ic_check_similar"), for: .normal)
btn.setTitle("Select All", for: .normal)
btn.setImage(UIImage.init(named: "ic_close_similar"), for: .selected)
btn.setTitle("Deselect All", for: .selected)
btn.setTitleColor(UIColor.colorWithHex(hexStr: mColor), for: .normal)
btn.setTitleColor(UIColor.colorWithHex(hexStr: black3Color), for: .selected)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
btn.addTarget(self, action: #selector(seletedAllBtnClick), for: .touchUpInside)
btn.layer.cornerRadius = btn.height / 2
btn.layer.masksToBounds = true
btn.changBtnWithStytl(btnStyle: .defalut, margin: 8)
return btn
}()
lazy var tablewView:HomeInfoView = {
let cY:CGFloat = titleView.y + titleView.height
let sview:HomeInfoView = HomeInfoView(frame: CGRect(x: 0, y: cY, width: view.width, height: view.height - cY), ids: ids,type: self.type)
sview.callBack = {[weak self] isSeleted in
guard let self else {return}
if let cS = isSeleted as? Bool {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.seletedAllBtn.isSelected = cS
self.seletedAllBtn.width = cS ? 131 : 115
seletedAllBtn.x = titleView.width - marginLR - seletedAllBtn.width
}
}
}
sview.deleteCallBack = {array in
if let cA = array as? [String] {
PhotoAndVideoMananger.deleteAssets(localIdentifiers: cA) {[weak self] in
guard let self else {return}
self.tablewView.deleteModel()
}
}
}
return sview
}()
var ids: [[String]]?
init(ids: [[String]],type:PhotsFileType?) {
self.ids = ids
self.type = type
super.init(nibName: nil, bundle: nil)
}
// 由于继承自 UIViewController,必须实现这个必需的构造器
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
titleView.model.title = ""
}
override func addViews() {
super.addViews()
seletedAllBtn.x = titleView.width - marginLR - seletedAllBtn.width
seletedAllBtn.centerY = navCenterY
titleView.addSubview(seletedAllBtn)
view.addSubview(tablewView)
}
@objc func seletedAllBtnClick() {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
seletedAllBtn.isSelected = !seletedAllBtn.isSelected
self.seletedAllBtn.width = seletedAllBtn.isSelected ? 131 : 115
seletedAllBtn.x = titleView.width - marginLR - seletedAllBtn.width
tablewView.changeALlValue(isSeleted: seletedAllBtn.isSelected)
}
}
}
......@@ -8,11 +8,11 @@
import UIKit
class HomeViewController:UIViewController {
class HomeViewController:BaseViewController {
private var isShowPay:Bool = false
private var homeView:HomeView?
var homeView:HomeView?
override func viewDidLoad() {
......@@ -22,17 +22,100 @@ class HomeViewController:UIViewController {
homeView = HomeView(frame: view.bounds)
homeView?.titleCallBack = {[weak self] array in
DispatchQueue.main.async {[weak self] in
guard let self else {return}
let vc:HomeInfoViewController = HomeInfoViewController(ids: array as! [[String]], type: .similar)
self.navigationController?.pushViewController(vc, animated: true)
}
}
homeView?.indexCallBack = {[weak self] index in
guard let self else {return}
if let cIndex = index as? Int {
switch cIndex {
case 0 :
DispatchQueue.main.async {[weak self] in
guard let self else {return}
let vc:ChargeViewController = ChargeViewController()
self.navigationController?.pushViewController(vc, animated: true)
}
default:
break
}
}
}
view.addSubview(homeView!)
}
override func addViews() {
}
func setupData() {
PhotoDataManager.manager.loadFromFileSystem(resultModel: {[weak self] model in
self?.homeView?.model = model
})
PhotoAndVideoMananger.mananger.fetchAllFile {[weak self] index, FileSize in
guard let self else {return}
self.homeView?.model?.allFileNumber = index
self.homeView?.model?.allFileSize = FileSize
self.homeView?.setTitle()
} completion: {[weak self] fileSize,index in
guard let self else {return}
self.homeView?.model?.allFileNumber = index
self.homeView?.model?.allFileSize = fileSize
self.homeView?.setTitle()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.barHidden = false
if !isShowPay {
setupData()
isShowPay = true
if BatteryMonitorManager.shared.getBatteryIsCharging() {
let vc:ChargeInfoViewController = ChargeInfoViewController(model:loadChargeImtesSONFromBundle()?.first, type: ChargeInfoViewController.ChargeInfoType.charge)
self.navigationController?.pushViewController(vc, animated: false)
}else {
let vc:HomePayViewController = HomePayViewController()
let nav:BaseNavViewController = BaseNavViewController(rootViewController: vc)
......@@ -42,4 +125,6 @@ class HomeViewController:UIViewController {
self.navigationController?.present(nav, animated: true)
}
}
}
}
//
// HomeInfoView.swift
// PhoneManager
//
// Created by edy on 2025/3/25.
//
import UIKit
class HomeInfoView :UIView{
var ids:[[String]]?
var models:[HomeInfoTableItem] = []
var callBack:callBack<Any> = {text in}
var deleteCallBack:callBack<Any> = {array in }
lazy var tableView:UITableView = {
let sview:UITableView = UITableView.init(frame: bounds)
sview.backgroundColor = .clear
sview.separatorStyle = .none
sview.showsVerticalScrollIndicator = false
sview.delegate = self
sview.dataSource = self
sview.register(HomeInfoTableViewCell.self, forCellReuseIdentifier: HomeInfoTableViewCell.identifier)
sview.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
if #available(iOS 15.0, *) {
sview.sectionHeaderTopPadding = 0
}
return sview
}()
lazy var headerView:HomeInfoTitleView = {
let sview:HomeInfoTitleView = HomeInfoTitleView(frame: CGRect(x: 0, y: 0, width: width, height: 84))
return sview
}()
lazy var deleteView:HomeInfoDeleteView = {
let cH:CGFloat = 48 + 2 * marginLR + safeHeight
let sview:HomeInfoDeleteView = HomeInfoDeleteView(frame: CGRect(x: 0, y: height - cH, width: width, height: cH))
sview.callBack = {[weak self] status in
guard let self else {return}
if let operstatus = status as? OperStatus {
switch operstatus {
case .delete:
self.deleteCallBack(getSelectedArray())
default:
break
}
}
}
return sview
}()
init(frame: CGRect,ids:[[String]]?,type:PhotsFileType?) {
self.ids = ids
for array in ids ?? [] {
var smodels:[ImageSeletedCollectionItem] = []
for id in array {
let smodel = ImageSeletedCollectionItem()
smodel.id = id
smodel.isSeleted = false
smodels.append(smodel)
}
let smodel = HomeInfoTableItem()
smodel.type = type
smodel.smodels = smodels
models.append(smodel)
}
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backgroundColor = .white
self.setTitleView()
self.addSubview(tableView)
self.addSubview(deleteView)
}
func changeValue() {
var isSeleted:Bool = false
for smodel in models {
for smodel2 in smodel.smodels ?? [] {
if (smodel2.isSeleted ?? false) {
isSeleted = true
}
}
}
setTitleView()
callBack(isSeleted)
}
func changeALlValue(isSeleted:Bool) {
// 在这里进行数据更新
for smodel in models {
for (index,smodel2) in (smodel.smodels ?? []).enumerated() {
if index == 0 {
smodel2.isSeleted = false
}else {
smodel2.isSeleted = isSeleted
}
}
}
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
setTitleView()
}
func getSelectedArray() -> [String] {
var selectedArray:[String] = []
for smodel in models {
for smodel2 in smodel.smodels ?? []{
if smodel2.isSeleted ?? false {
selectedArray.append(smodel2.id)
}
}
}
return selectedArray
}
func deleteModel() {
for i in 0..<models.count {
// 过滤掉 isSelected 为 true 的 smodel2
models[i].smodels = models[i].smodels?.filter { !($0.isSeleted ?? false) }
}
DispatchQueue.main.async {[weak self] in
guard let self else {return}
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
setTitleView()
}
}
func setTitleView() {
var number:Int = 0
var seletedNumber:Int = 0
for smodel in models {
number += smodel.smodels?.count ?? 0
for smodel2 in smodel.smodels ?? []{
if smodel2.isSeleted ?? false {
seletedNumber += 1
}
}
}
headerView.changeContent(title: PhotsFileType.duplicates.rawValue, allNumber: number, seletedCount: seletedNumber)
deleteView.changeContent(title: PhotsFileType.duplicates.rawValue, seletedCount: seletedNumber)
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: deleteView.isHidden ? 12 : deleteView.height + 12 , right: 0)
}
}
extension HomeInfoView:UITableViewDataSource,UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return ids?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: HomeInfoTableViewCell.identifier, for: indexPath) as! HomeInfoTableViewCell
cell.model = models[indexPath.row]
cell.callBack = {[weak self] text in
guard let self else {return}
changeValue()
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let models = ids?[indexPath.row]
return ((models?.count ?? 0) > 2 ? 190 : 214) + 8
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return headerView.height
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return headerView
}
}
class HomeInfoTitleView:UIView {
lazy var titleLabel:UILabel = {
let sview:UILabel = UILabel()
sview.font = .systemFont(ofSize: 20, weight: .bold)
sview.textColor = UIColor.colorWithHex(hexStr: black3Color)
sview.x = 15
sview.y = 14
return sview
}()
lazy var numberLabel:UILabel = {
let sview:UILabel = UILabel()
sview.font = .systemFont(ofSize: 14, weight: .regular)
sview.textColor = UIColor.colorWithHex(hexStr: black6Color)
sview.x = 15
sview.y = 40
sview.width = width - 2 * marginLR
sview.height = 20
return sview
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backgroundColor = .white
addSubview(titleLabel)
addSubview(numberLabel)
}
func changeContent(title:String,allNumber:Int,seletedCount:Int) {
titleLabel.text = title
titleLabel.sizeToFit()
let allNumberStr = "\(allNumber)"
let seletedCountStr = "\(seletedCount)"
let fullText = allNumberStr + " photos · \(seletedCountStr) selected"
let attributedString2 = NSMutableAttributedString(string: fullText, attributes: [
.font: UIFont.systemFont(ofSize: 14),
.foregroundColor: UIColor.colorWithHex(hexStr: "#666666")
])
let termsRange = (fullText as NSString).range(of: allNumberStr)
let privacyRange = (fullText as NSString).range(of: seletedCountStr)
attributedString2.addAttributes([
.foregroundColor: UIColor.colorWithHex(hexStr: "#0082FF"),
], range: termsRange)
attributedString2.addAttributes([
.foregroundColor: UIColor.colorWithHex(hexStr: "#0082FF"),
], range: privacyRange)
numberLabel.attributedText = attributedString2
numberLabel.y = height - numberLabel.height - 8
}
}
class HomeInfoDeleteView:UIView {
var callBack:callBack<Any> = {text in}
lazy var deleteBtn:UIButton = {
let sview:UIButton = UIButton()
sview.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
sview.addTarget(self, action: #selector(deleteBtnClick), for: .touchUpInside)
sview.backgroundColor = UIColor.colorWithHex(hexStr: mColor)
sview.width = width - 2 * marginLR
sview.height = 48
sview.centerX = width / 2
sview.y = marginLR
sview.layer.cornerRadius = sview.height / 2
sview.layer.masksToBounds = true
return sview
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
self.setShadow()
backgroundColor = .white
addSubview(deleteBtn)
}
func changeContent(title:String,seletedCount:Int) {
deleteBtn.setTitle("Delete \(seletedCount) \(title)", for: .normal)
self.isHidden = seletedCount > 0 ? false : true
}
@objc func deleteBtnClick() {
callBack(OperStatus.delete)
}
}
......@@ -74,7 +74,21 @@ class HomeNavView:UIView {
self.addSubview(tipLabel)
// 设置文本内容
let text = "202 files · 1.15 GB of storage to clean up"
}
@objc private func settingBtnClick() {
}
func setFileAndCount(count:Int,fileSize:Double) {
let countString = "\(count)"
let fileSizeString = formatFileSize(fileSize)
let text = countString + " files · " + fileSizeString + " of storage to clean up"
let attributedText = NSMutableAttributedString(string: text)
// 设置整体文本样式
......@@ -85,17 +99,19 @@ class HomeNavView:UIView {
attributedText.addAttributes(fullTextAttributes, range: NSRange(location: 0, length: text.count))
// 设置 "202" 为蓝色
if let range1 = text.range(of: "202") {
if let range1 = text.range(of: countString) {
let nsRange1 = NSRange(range1, in: text)
attributedText.addAttributes([
.foregroundColor: UIColor.colorWithHex(hexStr: mColor)
.foregroundColor: UIColor.colorWithHex(hexStr: mColor),
.font:UIFont.systemFont(ofSize: 14, weight: .bold)
], range: nsRange1)
}
// 设置 "1.15 GB" 为蓝色
if let range2 = text.range(of: "1.15 GB") {
if let range2 = text.range(of: fileSizeString) {
let nsRange2 = NSRange(range2, in: text)
attributedText.addAttributes([
.font:UIFont.systemFont(ofSize: 14, weight: .bold),
.foregroundColor: UIColor.colorWithHex(hexStr: mColor)
], range: nsRange2)
}
......@@ -109,11 +125,5 @@ class HomeNavView:UIView {
make.centerX.equalToSuperview()
make.width.equalToSuperview().offset(-2 * marginLR)
}
}
@objc private func settingBtnClick() {
}
}
......@@ -11,6 +11,8 @@ class HomeTabbarView:UIView {
private var tabbarItems:[HomeTabbarItem] = []
var indexCallBack:callBack<Any> = {index in }
override init(frame: CGRect) {
super.init(frame: frame)
......@@ -48,7 +50,7 @@ class HomeTabbarView:UIView {
btn.addTarget(self, action: #selector(tabbarClick(_:)), for: .touchUpInside)
btn.width = cW
btn.height = cH
btn.y = 12
btn.y = safeHeight == 0 ? 6 : 12
btn.x = 8 + Double(index) * cW
btn.changBtnWithStytl(btnStyle: .imageTop, margin: 5)
......@@ -64,7 +66,13 @@ class HomeTabbarView:UIView {
@objc func tabbarClick(_ btn:UIButton) {
let btnText = btn.titleLabel?.text
for (index, item) in tabbarItems.enumerated() {
if item.text == btnText {
indexCallBack(index)
}
}
}
......
......@@ -16,9 +16,24 @@ class HomeView:UIView {
private var bottomView:UIView?
private var titleModels:[HomePhotosModel] = []
var titleCallBack:callBack<Any> = {array in}
private var contentModels:[String] = ["123","1234","12323","12"]
var indexCallBack:callBack<Any> = {index in }
var model:PhotosManagerModel? {
didSet {
guard model != nil else {return}
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.collectionView.reloadData()
}
}
}
lazy var collectionView:UICollectionView = {
......@@ -32,6 +47,11 @@ class HomeView:UIView {
sview.delegate = self
sview.showsVerticalScrollIndicator = false
sview.register(HomeTitleCollectionCell.self, forCellWithReuseIdentifier: HomeTitleCollectionCell.identifiers)
sview.register(HomeOtherCollectionCell.self, forCellWithReuseIdentifier: HomeOtherCollectionCell.identifier)
if #available(iOS 11.0, *) {
sview.contentInsetAdjustmentBehavior = .never
}
sview.backgroundColor = .clear
return sview
......@@ -50,12 +70,76 @@ class HomeView:UIView {
fatalError("init(coder:) has not been implemented")
}
func reload(type:PhotsFileType) {
var indexPath:IndexPath!
if type == .duplicates {
indexPath = IndexPath(row: 0, section: 0)
}else if type == .similar{
indexPath = IndexPath(row: 1, section: 0)
}else if type == .videos{
indexPath = IndexPath(row: 0, section: 1)
}
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.collectionView.reloadData()
}
}
func setTitle() {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.homeNavView?.setFileAndCount(count: model?.allFileNumber ?? 0, fileSize: model?.allFileSize ?? 0)
}
}
func refreshData(model:PhotosManagerModel) {
if self.model == nil {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.homeNavView?.setFileAndCount(count: model.allFileNumber, fileSize: model.allFileSize)
self.collectionView.reloadData()
}
}else {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.homeNavView?.setFileAndCount(count: model.allFileNumber, fileSize: model.allFileSize)
self.collectionView.reloadData()
}
}
self.model = model
}
func setData() {
let model:HomePhotosModel = HomePhotosModel(folderName: "Duplicates", filePaths: [], firstItemPath: "dfdf", allFileSize: 123213132)
titleModels.append(model)
titleModels.append(model)
}
private func setupUI() {
......@@ -72,8 +156,16 @@ class HomeView:UIView {
make.height.equalTo(safeHeight + 66)
})
homeTabbarView?.indexCallBack = {[weak self] index in
guard let self else {return}
self.indexCallBack(index)
}
homeNavView = HomeNavView(frame: CGRect(x: 0, y: 0, width: width, height: statusBarHeight + 96))
self.addSubview(homeNavView!)
homeNavView?.snp.makeConstraints({ make in
......@@ -82,6 +174,8 @@ class HomeView:UIView {
make.height.equalTo(statusBarHeight + 96)
})
Print("statusBarHeight----\(statusBarHeight)")
bottomView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: safeHeight + 10))
bottomView?.backgroundColor = .white
self.addSubview(bottomView!)
......@@ -92,14 +186,16 @@ class HomeView:UIView {
make.height.equalTo(safeHeight + 10)
})
collectionView.contentInset = UIEdgeInsets(top: homeTabbarView!.height, left: 0, bottom: homeTabbarView!.height - safeHeight + 16, right: 0)
self.insertSubview(collectionView, at: 0)
collectionView.snp.makeConstraints { make in
make.center.height.equalToSuperview()
make.width.equalToSuperview().offset(-2 * marginLR)
}
collectionView.contentInset = UIEdgeInsets(top: homeTabbarView!.height + 49, left: 0, bottom: homeTabbarView!.height + 16, right: 0)
}
func etCell(indexPath: IndexPath) -> UICollectionViewCell {
......@@ -120,10 +216,10 @@ extension HomeView:WaterfallMutiSectionDelegate,UICollectionViewDataSource,UICol
switch section {
case 0:
return titleModels.count
return model?.titleModelArray.count ?? 0
case 1:
return contentModels.count
return model?.otherModelArray.count ?? 0
default:
return 0
......@@ -132,28 +228,36 @@ extension HomeView:WaterfallMutiSectionDelegate,UICollectionViewDataSource,UICol
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeTitleCollectionCell.identifiers, for: indexPath) as! HomeTitleCollectionCell
let section = indexPath.section
switch section {
case 0:
cell.model = titleModels[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeTitleCollectionCell.identifiers, for: indexPath) as! HomeTitleCollectionCell
cell.model = model?.titleModelArray[indexPath.row]
return cell
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeOtherCollectionCell.identifier, for: indexPath) as! HomeOtherCollectionCell
cell.model = model?.otherModelArray[indexPath.row]
return cell
default:
break
return UICollectionViewCell()
}
return cell
}
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: WaterfallMutiSectionFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
if indexPath.section == 0 {
return 190
let model = model?.titleModelArray[indexPath.row]
return (model?.assets.first?.count ?? 0) > 2 ? ((collection.width - marginLR - 20) / 2.5) + 64 : ((collection.width - 2 * marginLR - 10) / 2) + 64
}else {
return indexPath.row % 2 == 1 ? 197 : 217
let model = model?.otherModelArray[indexPath.row]
return itemWidth + 12 + UILabel.getSizeWith(font: UIFont.systemFont(ofSize: 16, weight: .bold),lineSpacing: 5, width: itemWidth - 32, numberOfLines: 0, content: model?.folderName ?? "").height
}
}
......@@ -197,4 +301,14 @@ extension HomeView:WaterfallMutiSectionDelegate,UICollectionViewDataSource,UICol
return 0
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if (indexPath.section == 0) {
let smodel = model?.titleModelArray[indexPath.row]
titleCallBack(smodel?.assets ?? [])
}
}
}
......@@ -6,11 +6,151 @@
//
import Foundation
import Photos
struct HomePhotosModel {
class PhotoDataManager {
static let manager:PhotoDataManager = PhotoDataManager()
// 文件路径
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
// 保存到文件
func saveToFileSystem(model: PhotosManagerModel, filename: String = "photosManagerData.json") {
let url = getDocumentsDirectory().appendingPathComponent(filename)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let data = try encoder.encode(model)
try data.write(to: url)
print("数据保存成功: \(url.path)")
} catch {
print("保存失败: \(error.localizedDescription)")
}
}
// 从文件读取
func loadFromFileSystem(filename: String = "photosManagerData.json",resultModel:@escaping (_ model:PhotosManagerModel) -> () = {mdoel in}) {
let url = getDocumentsDirectory().appendingPathComponent(filename)
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let model = try decoder.decode(PhotosManagerModel.self, from: data)
resultModel(model)
} catch {
loadDataFromPhotos { model in
resultModel(model)
}
}
}
private func loadDataFromPhotos(resultModel:@escaping (_ model:PhotosManagerModel) -> () = {mdoel in}) {
let model1:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.duplicates.rawValue, allFileSize: 0, assets: [])
let model2:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.similar.rawValue, allFileSize: 0, assets: [])
let model3:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.videos.rawValue, allFileSize: 0, assets: [])
let model4:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.similarScreenshots.rawValue, allFileSize: 0, assets: [])
let model5:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.screenshots.rawValue, allFileSize: 0, assets: [])
let model6:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.SimilarVideos.rawValue, allFileSize: 0, assets: [])
let model7:HomePhotosModel = HomePhotosModel(folderName: PhotsFileType.Other.rawValue, allFileSize: 0, assets: [])
let allModel:PhotosManagerModel = PhotosManagerModel(allFileNumber: 0, allFileSize: 0, titleModelArray: [model1,model2], otherModelArray: [model3,model4,model5,model6,model7])
resultModel(allModel)
PhotoAndVideoMananger.mananger.fetXSOther { array in
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
PhotoSimilarityFinder.processSimilarPhotoGroups(simalr:1,assetGroups: array) { array,fileSize in
model1.assets = array
model1.allFileSize = fileSize
resultModel(allModel)
dispatchGroup.leave()
}
dispatchGroup.enter()
PhotoSimilarityFinder.processSimilarPhotoGroups(assetGroups: array) {array,fileSize in
model2.assets = array
model2.allFileSize = fileSize
resultModel(allModel)
dispatchGroup.leave()
}
dispatchGroup.enter()
PhotoAndVideoMananger.mananger.fetXSVideo { array in
PhotoSimilarityFinder.processSimilarVideoGroups(videoGroups: array) {ids in
model3.assets = ids
resultModel(allModel)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
print("所有异步操作都已完成")
if model1.assets.count != 0 || model2.assets.count != 0 || model3.assets.count != 0 {
PhotoDataManager.manager.saveToFileSystem(model: allModel)
resultModel(allModel)
}
}
}
}
}
class PhotosManagerModel:Codable {
var allFileNumber:Int
var allFileSize:Double
var titleModelArray:[HomePhotosModel] = []
var otherModelArray:[HomePhotosModel] = []
init(allFileNumber: Int, allFileSize: Double, titleModelArray: [HomePhotosModel], otherModelArray: [HomePhotosModel]) {
self.allFileNumber = allFileNumber
self.allFileSize = allFileSize
self.titleModelArray = titleModelArray
self.otherModelArray = otherModelArray
}
}
class HomePhotosModel:Codable {
var folderName:String
var filePaths:[String]
var firstItemPath:String
var allFileSize:Double
var assets:[[String]]
init(folderName: String, allFileSize: Double, assets: [[String]]) {
self.folderName = folderName
self.allFileSize = allFileSize
self.assets = assets
}
}
//
// ImageCollectionModel.swift
// PhoneManager
//
// Created by edy on 2025/3/24.
//
import Foundation
import Photos
struct ImageCollectionModel {
var asset:String
}
//
// ImageSeletedCollectionItem.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import Foundation
import UIKit
class ImageSeletedCollectionItem {
var id:String = ""
var image:UIImage?
var isSeleted:Bool?
}
class HomeInfoTableItem {
var type:PhotsFileType?
var smodels:[ImageSeletedCollectionItem]?
}
//
// HomeInfoTableViewCell.swift
// PhoneManager
//
// Created by edy on 2025/3/25.
//
import UIKit
import SnapKit
class HomeInfoTableViewCell:UITableViewCell {
static let identifier = "HomeInfoTableViewCellID"
private var backView:UIView?
private var collectionView: UICollectionView?
private var numberLabel:UILabel?
private var seletedAllBtn:UIButton?
var callBack:callBack<Any> = {text in}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
addSubview1()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
self.selectionStyle = .none
backView = UIView()
backView?.backgroundColor = UIColor.colorWithHex(hexStr: "#F2F6FC")
backView?.layer.cornerRadius = 12
backView?.layer.masksToBounds = true
backView?.x = marginLR
backView?.y = marginLR
backView?.isUserInteractionEnabled = true
numberLabel = UILabel()
numberLabel?.textColor = UIColor.colorWithHex(hexStr: black3Color)
numberLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
seletedAllBtn = UIButton()
seletedAllBtn?.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
seletedAllBtn?.setTitle("Select All", for: .normal)
seletedAllBtn?.setTitle("Deselect All", for: .selected)
seletedAllBtn?.setTitleColor(UIColor.colorWithHex(hexStr: mColor), for: .normal)
seletedAllBtn?.sizeToFit()
seletedAllBtn?.addTarget(self, action: #selector(seletedAllBtnClick), for: .touchUpInside)
let flowlayout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
flowlayout.scrollDirection = .horizontal
flowlayout.minimumLineSpacing = 10
collectionView = UICollectionView.init(frame: CGRectMake(0, 0 , ScreenW , ScreenH), collectionViewLayout: flowlayout)
collectionView?.backgroundColor = .clear
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.showsVerticalScrollIndicator = false
collectionView?.register(ImageSeletedCollectionCell.self, forCellWithReuseIdentifier: ImageSeletedCollectionCell.identifiers)
collectionView?.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: marginLR)
collectionView?.dataSource = self
collectionView?.delegate = self
}
func addSubview1() {
contentView.addSubview(backView!)
contentView.addSubview(numberLabel!)
contentView.addSubview(seletedAllBtn!)
contentView.addSubview(collectionView!)
}
var model:HomeInfoTableItem? {
didSet {
guard let model else {return}
let cH = ((model.smodels?.count ?? 0) > 2 ? 190 : 214) + 8
backView?.height = CGFloat(cH - 8)
backView?.width = ScreenW - 16 * 2
backView?.centerY = CGFloat(cH / 2)
backView?.centerX = ScreenW / 2
numberLabel?.text = "\(model.smodels?.count ?? 0) " + "Duplicates"
numberLabel?.sizeToFit()
numberLabel?.x = (backView?.x ?? 0) + marginLR
numberLabel?.y = (backView?.y ?? 0) + marginLR
seletedAllBtn?.centerY = numberLabel?.centerY ?? 0
seletedAllBtn?.x = (backView?.x ?? 0) + (backView?.width ?? 0) - 12 - (seletedAllBtn?.width ?? 0)
checkSeletedAll()
collectionView?.height = CGFloat(cH - 64)
collectionView?.width = (backView?.width ?? 0) - marginLR
collectionView?.x = (backView?.x ?? 0) + marginLR
collectionView?.y = (backView?.y ?? 0) + (backView?.height ?? 0) - (collectionView?.height ?? 0) - 16
self.collectionView?.reloadData()
}
}
@objc func seletedAllBtnClick() {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
seletedAllBtn?.isSelected = !(seletedAllBtn?.isSelected ?? true)
seletedAllBtn?.sizeToFit()
seletedAllBtn?.centerY = numberLabel?.centerY ?? 0
seletedAllBtn?.x = (backView?.x ?? 0) + (backView?.width ?? 0) - 12 - (seletedAllBtn?.width ?? 0)
}
for (index,smodel) in (model?.smodels ?? []).enumerated() {
if index == 0 {
smodel.isSeleted = false
}else {
smodel.isSeleted = !(seletedAllBtn?.isSelected ?? false)
}
}
collectionView?.reloadData()
callBack("changgeSeleted")
}
func checkSeletedAll() {
var isSeletedAll:Bool = false
for smodel in model?.smodels ?? [] {
if (smodel.isSeleted ?? false) {
isSeletedAll = true
}
}
seletedAllBtn?.isSelected = isSeletedAll
callBack("changgeSeleted")
DispatchQueue.main.async {[weak self] in
guard let self else {return}
seletedAllBtn?.sizeToFit()
seletedAllBtn?.centerY = numberLabel?.centerY ?? 0
seletedAllBtn?.x = (backView?.x ?? 0) + (backView?.width ?? 0) - 12 - (seletedAllBtn?.width ?? 0)
}
}
override func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
extension HomeInfoTableViewCell:UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return model?.smodels?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageSeletedCollectionCell.identifiers, for: indexPath) as! ImageSeletedCollectionCell
cell.model = model?.smodels?[indexPath.row]
cell.callBack = {[weak self] _ in
guard let self else {return}
self.checkSeletedAll()
self.callBack("changgeSeleted")
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 计算 cell 宽度
/*return CGSize(width:collectionView.height, height: collectionView.height) */ // 宽高相等,形成网格
return CGSize(width:collectionView.height, height: collectionView.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 10 // 设置列间距
}
}
struct HomeInfoTableViewModel {
var ids:[String]?
var height:CGFloat?
}
import UIKit
class HomeOtherCollectionCell: UICollectionViewCell {
// MARK: - Properties
static let identifier = "HomeOtherCollectionCellID"
private let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.layer.cornerRadius = 8
iv.backgroundColor = .clear
return iv
}()
private let countLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textAlignment = .center
return label
}()
private let sizeLabel: UILabel = {
let label = UILabel()
label.textColor = .systemGray
label.font = .systemFont(ofSize: 12)
label.textAlignment = .left
return label
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor.colorWithHex(hexStr: black3Color)
label.textAlignment = .left
label.numberOfLines = 0
return label
}()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup
private func setupUI() {
backgroundColor = UIColor.colorWithHex(hexStr: "#F2F6FC")
contentView.addSubview(imageView)
contentView.addSubview(countLabel)
contentView.addSubview(sizeLabel)
contentView.addSubview(titleLabel)
}
var model:HomePhotosModel? {
didSet {
guard let model else {return}
titleLabel.text = model.folderName
guard let asset = model.assets.first?.first else {return}
let image = PhotoAndVideoMananger.mananger.getImageFromAssetID(id: asset)
DispatchQueue.main.async {[weak self] in
guard let self else {return}
imageView.image = image
}
}
}
// MARK: - Configuration
func configure(with image: UIImage?, count: Int, size: String) {
imageView.image = image
countLabel.text = "\(count) Photos"
sizeLabel.text = size
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
countLabel.text = nil
sizeLabel.text = nil
}
override func layoutSubviews() {
self.layer.cornerRadius = 12
self.layer.masksToBounds = true
imageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-16)
make.width.equalToSuperview().offset(-32)
make.height.equalTo(self.width - 32)
}
titleLabel.snp.makeConstraints { make in
make.left.top.equalToSuperview().offset(16)
make.width.equalToSuperview().offset(-32)
}
titleLabel.sizeToFit()
}
}
......@@ -14,8 +14,14 @@ class HomeTitleCollectionCell:UICollectionViewCell {
private var titleLabel: UILabel?
private var fileLabel:UILabel?
private var collectionView: UICollectionView?
private var nextImage:UIImageView?
private var assetsModels:[ImageCollectionModel] = []
override init(frame: CGRect) {
super.init(frame: frame)
......@@ -38,19 +44,75 @@ class HomeTitleCollectionCell:UICollectionViewCell {
titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
titleLabel?.textColor = UIColor.colorWithHex(hexStr: black3Color)
fileLabel = UILabel()
fileLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
fileLabel?.textColor = UIColor.colorWithHex(hexStr: mColor)
let flowlayout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
flowlayout.scrollDirection = .horizontal
flowlayout.minimumLineSpacing = 10
collectionView = UICollectionView.init(frame: CGRectMake(0, 0 , ScreenW , ScreenH), collectionViewLayout: flowlayout)
collectionView?.backgroundColor = .clear
collectionView?.decelerationRate = UIScrollView.DecelerationRate.fast;
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.showsVerticalScrollIndicator = false
collectionView?.register(ImageCollectionCell.self, forCellWithReuseIdentifier: ImageCollectionCell.identifiers)
collectionView?.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: marginLR)
collectionView?.dataSource = self
collectionView?.delegate = self
nextImage = UIImageView(image: UIImage(named: "icon_left_setting"))
}
func addSubview1() {
addSubview(titleLabel!)
addSubview(fileLabel!)
addSubview(collectionView!)
addSubview(nextImage!)
}
var model:HomePhotosModel! {
var model:HomePhotosModel? {
didSet {
guard let model else {return}
titleLabel?.text = model.folderName
titleLabel?.sizeToFit()
var count = 0
for array in model.assets {
count += array.count
}
fileLabel?.text = "\(count)" + " Photos " + (model.allFileSize > 0 ? "(\(formatFileSize(model.allFileSize)))" : "(Calculating...)")
fileLabel?.sizeToFit()
assetsModels = []
for asset in model.assets.first ?? [] {
let smodel = ImageCollectionModel(asset: asset)
assetsModels.append(smodel)
}
Print(assetsModels)
Print(model.assets)
DispatchQueue.main.async {[weak self] in
guard let self else {return}
self.collectionView?.reloadData()
}
}
}
......@@ -62,6 +124,60 @@ class HomeTitleCollectionCell:UICollectionViewCell {
make.top.left.equalTo(16)
})
titleLabel?.sizeToFit()
fileLabel?.snp.makeConstraints({ make in
make.top.centerY.equalTo(titleLabel!)
make.right.equalToSuperview().offset(-34)
})
fileLabel?.sizeToFit()
collectionView?.snp.makeConstraints({ make in
make.left.equalToSuperview().offset(16)
make.bottom.equalToSuperview().offset(-16)
make.width.equalToSuperview().offset((model?.assets.count ?? 0) > 2 ? -16 : -32)
make.height.equalToSuperview().offset(-64)
})
nextImage?.snp.makeConstraints({ make in
make.centerY.equalTo(fileLabel!)
make.right.equalToSuperview().offset(-12)
make.width.height.equalTo(20)
})
}
}
extension HomeTitleCollectionCell:UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return assetsModels.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCollectionCell.identifiers, for: indexPath) as! ImageCollectionCell
cell.model = assetsModels[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 计算 cell 宽度
return CGSize(width:collectionView.height - 2.5, height: collectionView.height - 2.5) // 宽高相等,形成网格
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 10 // 设置列间距
}
}
//
// ImageCollectionCell.swift
// PhoneManager
//
// Created by edy on 2025/3/24.
//
import UIKit
import SnapKit
class ImageCollectionCell:UICollectionViewCell {
static let identifiers = "ImageCollectionCellID"
private var backImageView: UIImageView?
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
addViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var model:ImageCollectionModel! {
didSet {
DispatchQueue.global().async {[weak self] in
guard let self else {return}
let image = PhotoAndVideoMananger.mananger.getImageFromAssetID(id: model.asset)
DispatchQueue.main.async {[weak self] in
guard let self else {return}
backImageView?.image = image
}
}
}
}
func setupUI() {
backImageView = UIImageView()
backImageView?.isUserInteractionEnabled = true
backImageView?.contentMode = .scaleAspectFill
backImageView?.clipsToBounds = true
backImageView?.layer.masksToBounds = true
self.backgroundColor = .clear
}
func addViews() {
self.addSubview(backImageView!)
}
override func layoutSubviews() {
super.layoutSubviews()
backImageView?.snp.makeConstraints({ make in
make.top.left.width.height.equalToSuperview()
})
backImageView?.layer.cornerRadius = 12
}
}
//
// ImageSeletedCollectionCell.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import UIKit
import SnapKit
class ImageSeletedCollectionCell:UICollectionViewCell {
static let identifiers = "ImageSeletedCollectionCellID"
private var backImageView: UIImageView?
private var seletedBtn:UIButton?
var callBack:callBack<Any> = {text in}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
addViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var model:ImageSeletedCollectionItem! {
didSet {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
seletedBtn?.isSelected = model.isSeleted ?? false
}
if let image = model.image {
DispatchQueue.main.async {[weak self] in
guard let self else {return}
backImageView?.image = image
}
}else {
DispatchQueue.global().async {[weak self] in
guard let self else {return}
if let asset = PhotoAndVideoMananger.mananger.getPHAsssetwithID(ids: [model.id]) {
let image = PhotoAndVideoMananger.mananger.getImageFromAsset(asset: asset)
model.image = image
DispatchQueue.main.async {[weak self] in
guard let self else {return}
backImageView?.image = image
}
}
}
}
}
}
func setupUI() {
backImageView = UIImageView()
backImageView?.contentMode = .scaleAspectFill
backImageView?.clipsToBounds = true
backImageView?.layer.masksToBounds = true
seletedBtn = UIButton()
seletedBtn?.setImage(UIImage(named: "home_info_norl"), for: .normal)
seletedBtn?.setImage(UIImage(named: "home_info_seleted"), for: .selected)
seletedBtn?.addTarget(self, action: #selector(seletedBtnClick), for: .touchUpInside)
self.backgroundColor = .clear
}
func addViews() {
self.addSubview(backImageView!)
self.addSubview(seletedBtn!)
}
@objc func seletedBtnClick() {
model.isSeleted = !(seletedBtn?.isSelected ?? true)
seletedBtn?.isSelected = model.isSeleted ?? false
callBack("")
}
override func layoutSubviews() {
super.layoutSubviews()
backImageView?.snp.makeConstraints({ make in
make.top.left.width.height.equalToSuperview()
})
backImageView?.layer.cornerRadius = 12
seletedBtn?.snp.makeConstraints({ make in
make.width.height.equalTo(24)
make.bottom.equalToSuperview().offset(-12)
make.right.equalToSuperview().offset(-20)
})
}
}
......@@ -42,6 +42,8 @@ class HomePayViewController:UIViewController {
switch operstatus {
case .close:
self.navigationController?.dismiss(animated: true)
default:
break
}
}
......
//
// BatteryMonitorManager.swift
// PhoneManager
//
// Created by edy on 2025/3/27.
//
import UIKit
protocol BatteryStatusDelegate: AnyObject {
func batteryStatusDidUpdate(level: Float, state: UIDevice.BatteryState)
}
class BatteryMonitorManager {
// Singleton instance
static let shared = BatteryMonitorManager()
// Delegate for status updates
weak var delegate: BatteryStatusDelegate?
// Current battery level (0.0 to 1.0)
private(set) var batteryLevel: Float = 0.0
// Current battery state
private(set) var batteryState: UIDevice.BatteryState = .unknown
// Flag to track if monitoring is active
private var isMonitoring = false
private init() {
// Private initializer for singleton
}
// MARK: - Public Methods
/// Start monitoring battery status
func startMonitoring() {
guard !isMonitoring else { return }
UIDevice.current.isBatteryMonitoringEnabled = true
// Get initial values
updateBatteryStatus()
// Register for notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryLevelDidChange),
name: UIDevice.batteryLevelDidChangeNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryStateDidChange),
name: UIDevice.batteryStateDidChangeNotification,
object: nil
)
isMonitoring = true
print("Battery monitoring started")
}
/// Stop monitoring battery status
func stopMonitoring() {
guard isMonitoring else { return }
NotificationCenter.default.removeObserver(self)
UIDevice.current.isBatteryMonitoringEnabled = false
isMonitoring = false
print("Battery monitoring stopped")
}
/// Get current battery status as a formatted string
// func currentBatteryStatus() -> String {
// let levelPercent = Int(batteryLevel * 100)
// let stateString = batteryStateDescription()
//
// return "Battery: \(levelPercent)% (\(stateString))"
// }
func getBatteryIsCharging() -> Bool {
UIDevice.current.isBatteryMonitoringEnabled = true
return UIDevice.current.batteryState == .charging
}
// MARK: - Private Methods
@objc private func batteryLevelDidChange() {
updateBatteryStatus()
}
@objc private func batteryStateDidChange() {
updateBatteryStatus()
}
private func updateBatteryStatus() {
batteryLevel = UIDevice.current.batteryLevel
batteryState = UIDevice.current.batteryState
// Notify delegate
delegate?.batteryStatusDidUpdate(level: batteryLevel, state: batteryState)
// Print to console (for debugging)
}
private func batteryStateDescription() -> String {
switch batteryState {
case .unknown:
return "Unknown"
case .unplugged:
return "Unplugged"
case .charging:
return "Charging"
case .full:
return "Full"
@unknown default:
return "Unknown state"
}
}
deinit {
stopMonitoring()
}
}
//
// NetStatusManager.swift
// PhoneManager
//
// Created by edy on 2025/3/27.
//
import Alamofire
import Network
enum NetStatus{
case NoNet
case WIFI
case WWAN
}
class NetStatusManager: NSObject {
static let manager:NetStatusManager = NetStatusManager()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitorQueue")
var currentStatus:NetStatus?
func startNet(netStatus: @escaping(NetStatus)->Void) {
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
Print("网络连接正常")
if path.isExpensive {
self.currentStatus = .WWAN
netStatus(.WWAN)
} else {
self.currentStatus = .WIFI
netStatus(.WIFI)
}
} else {
self.currentStatus = .NoNet
netStatus(.NoNet)
}
}
monitor.start(queue: queue)
}
func stop() {
monitor.cancel()
}
}
import Foundation
import SystemConfiguration.CaptiveNetwork
func getWiFiAddress() -> String? {
var address: String?
var ifaddr: UnsafeMutablePointer<ifaddrs>?
// 获取网络接口列表
guard getifaddrs(&ifaddr) == 0 else { return nil }
guard let firstAddr = ifaddr else { return nil }
// 遍历接口列表
for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
let interface = ifptr.pointee
let addrFamily = interface.ifa_addr.pointee.sa_family
// 检查接口是否为 IPv4 或 IPv6
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
let name = String(cString: interface.ifa_name)
// 筛选出 Wi-Fi (en0) 接口的 IP 地址
if name == "en0" {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST)
address = String(cString: hostname)
}
}
}
freeifaddrs(ifaddr)
return address
}
//
// OpenCVWrapper.h
// PhoneManager
//
// Created by edy on 2025/3/25.
//
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface OpenCVWrapper : NSObject
/// 是否相似
+ (BOOL)areImagesSimilar:(UIImage *)image1 withImage2:(UIImage *)image2 threshold:(double)threshold;
/// 获取相似度
+ (double)compareImageSimilarity:(UIImage *)image1 withImage2:(UIImage *)image2;
@end
#endif
//
// OpenCVWrapper.m
// PhoneManager
//
// Created by edy on 2025/3/25.
//
#import "OpenCVWrapper.h"
#import <opencv2/core.hpp>
#import <opencv2/imgproc.hpp>
#import <opencv2/features2d.hpp>
@implementation OpenCVWrapper
+ (cv::Mat)cvMatFromUIImage:(UIImage *)image {
CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
CGFloat cols = image.size.width;
CGFloat rows = image.size.height;
cv::Mat cvMat(rows, cols, CV_8UC4); // 8 bits per component, 4 channels (RGBA)
CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to data
cols, // Width of bitmap
rows, // Height of bitmap
8, // Bits per component
cvMat.step[0], // Bytes per row
colorSpace, // Colorspace
kCGImageAlphaNoneSkipLast |
kCGBitmapByteOrderDefault); // Bitmap info flags
CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
CGContextRelease(contextRef);
return cvMat;
}
+ (double)compareImageSimilarity:(UIImage *)image1 withImage2:(UIImage *)image2 {
@try {
// 检查输入
if (!image1 || !image2) {
return 0.0;
}
// 转换为 OpenCV 格式
cv::Mat mat1 = [self cvMatFromUIImage:image1];
cv::Mat mat2 = [self cvMatFromUIImage:image2];
// 检查矩阵是否为空
if (mat1.empty() || mat2.empty()) {
return 0.0;
}
// 转换为灰度图
cv::Mat gray1, gray2;
cv::cvtColor(mat1, gray1, cv::COLOR_RGBA2GRAY);
cv::cvtColor(mat2, gray2, cv::COLOR_RGBA2GRAY);
// 调整大小
cv::Size size(256, 256);
cv::resize(gray1, gray1, size);
cv::resize(gray2, gray2, size);
// 计算直方图
cv::Mat hist1, hist2;
int channels[] = {0};
int histSize[] = {256};
float range[] = {0, 256};
const float* ranges[] = {range};
cv::calcHist(&gray1, 1, channels, cv::Mat(), hist1, 1, histSize, ranges);
cv::calcHist(&gray2, 1, channels, cv::Mat(), hist2, 1, histSize, ranges);
// 归一化直方图
cv::normalize(hist1, hist1, 0, 1, cv::NORM_MINMAX);
cv::normalize(hist2, hist2, 0, 1, cv::NORM_MINMAX);
// 计算相似度
double similarity = cv::compareHist(hist1, hist2, cv::HISTCMP_CORREL);
// 确保返回值在 0-1 之间
return std::max(0.0, std::min(1.0, similarity));
}
@catch (NSException *exception) {
NSLog(@"Error comparing images: %@", exception);
return 0.0;
}
}
+ (BOOL)areImagesSimilar:(UIImage *)image1 withImage2:(UIImage *)image2 threshold:(double)threshold {
@try {
if (!image1 || !image2) {
return NO;
}
double similarity = [self compareImageSimilarity:image1 withImage2:image2];
return similarity >= threshold;
}
@catch (NSException *exception) {
NSLog(@"Error comparing images: %@", exception);
return NO;
}
}
@end
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "OpenCVWrapper.h"
......@@ -8,6 +8,9 @@
import Foundation
import Photos
import UIKit
import Vision
import CoreML
enum SoureceType {
case Photo
......@@ -22,10 +25,50 @@ enum PrivacyType:String {
case restricted = "restricted"
}
enum PhotsFileType:String {
case duplicates = "Duplicates"
case similar = "Similar"
case videos = "Videos"
case similarScreenshots = "Similar Screenshots"
case screenshots = "Screenshots"
case SimilarVideos = "Similar Videos"
case Other = "Other"
}
class PhotoAndVideoMananger {
static let mananger:PhotoAndVideoMananger = PhotoAndVideoMananger()
var allAssets:[PHAsset] = []
var imageAssets:[PHAsset] = []
var videoAssets:[PHAsset] = []
var ids:[String] = []
func setAssets() {
let fetchOptions = PHFetchOptions()
let photoAllAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let videoAllAssets = PHAsset.fetchAssets(with: .video, options: fetchOptions)
let photoAssetsArray = photoAllAssets.objects(at: IndexSet(0..<photoAllAssets.count))
let videoAssetsArray = videoAllAssets.objects(at: IndexSet(0..<videoAllAssets.count))
let combinedArray = photoAssetsArray + videoAssetsArray
imageAssets = photoAssetsArray
videoAssets = videoAssetsArray
allAssets = combinedArray
}
class func getPrivacy(suc:@escaping callBack<Any> = {text in}) {
PHPhotoLibrary.requestAuthorization { status in
......@@ -52,4 +95,529 @@ class PhotoAndVideoMananger {
}
}
func fetchAsset(type:PhotsFileType,propress: (Int,Double) ,completion:@escaping (Any) -> Void) {
switch type {
case .duplicates:
break
case .similar:
break
case .videos:
break
case .similarScreenshots:
break
case .screenshots:
break
case .SimilarVideos:
break
case .Other:
break
}
}
func fetchAllFile(propress:@escaping (Int,Double) -> Void,completion: @escaping (Double,Int) -> Void) {
if (self.allAssets.count == 0 ) {
let fetchOptions = PHFetchOptions()
let photoAllAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let videoAllAssets = PHAsset.fetchAssets(with: .video, options: fetchOptions)
let photoAssetsArray = photoAllAssets.objects(at: IndexSet(0..<photoAllAssets.count))
let videoAssetsArray = videoAllAssets.objects(at: IndexSet(0..<videoAllAssets.count))
let combinedArray = photoAssetsArray + videoAssetsArray
DispatchQueue.global().async {[weak self] in
guard let self = self else { return }
// 计算总大小
self.calculateTotalSize(of: combinedArray,progress: { fileSiez, index in
propress(index,Double(fileSiez))
}, completion: { fileSize,index in
completion(Double(fileSize),index)
})
}
}else {
let combinedArray = self.allAssets
DispatchQueue.global().async {[weak self] in
guard let self = self else { return }
// 计算总大小
self.calculateTotalSize(of: combinedArray,progress: { fileSiez, index in
propress(index,Double(fileSiez))
}, completion: { fileSize,index in
completion(Double(fileSize),index)
})
}
}
}
func fetXSOther( resulte:@escaping ([[PHAsset]]) -> Void) {
let fetchOptions = PHFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let assetsArray = allAssets.objects(at: IndexSet(0..<allAssets.count))
DispatchQueue.global().async {[weak self] in
guard let self = self else { return }
let newArray = self.groupSimilarAssets(from: assetsArray)
resulte(newArray)
}
}
func fetXSVideo( resulte:@escaping ([[PHAsset]]) -> Void) {
let fetchOptions = PHFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .video, options: fetchOptions)
let assetsArray = allAssets.objects(at: IndexSet(0..<allAssets.count))
DispatchQueue.global().async {[weak self] in
guard let self = self else { return }
let newArray = self.groupSimilarAssets(from: assetsArray)
resulte(newArray)
}
}
func fetchXSPhotos(type:PhotsFileType,propress:@escaping (Int,Double) -> Void,completion: @escaping ([[String]]) -> Void) {
let fetchOptions = PHFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let assetsArray = allAssets.objects(at: IndexSet(0..<allAssets.count))
DispatchQueue.global().async {[weak self] in
guard let self = self else { return }
let newArray = type == .duplicates ? self.groupSimilarAssets(from: assetsArray) : self.groupSalmaeAssets(from: assetsArray)
let identifiers: [[String]] = newArray.map { $0.map { $0.localIdentifier } }
completion(identifiers)
var fileSizeArray:[PHAsset] = []
for assetArray in newArray {
for asset in assetArray {
fileSizeArray.append(asset)
}
}
// 计算总大小
self.calculateTotalSize(of: fileSizeArray, completion: { fileSize,index in
propress(0,Double(fileSize))
})
}
}
func fetchAssets(completion: @escaping ([[String]],Int,Double) -> Void) {
// 获取所有照片
let fetchOptions = PHFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let assetsArray = allAssets.objects(at: IndexSet(0..<allAssets.count))
DispatchQueue.global().async {[weak self] in
guard let self = self else { return }
let newArray = self.groupSimilarAssets(from: assetsArray)
let identifiers: [[String]] = newArray.map { $0.map { $0.localIdentifier } }
completion(identifiers, identifiers.count, 0)
// 计算总大小
self.calculateTotalSize(of: assetsArray, completion: { filsSize, index in
// 使用过滤后的标识符
completion(identifiers, index, Double(filsSize))
})
}
}
func getImageFromAssetID(id: String) -> UIImage? {
if let asset = getPHAsssetwithID(ids: [id]) {
return getImageFromAsset(asset: asset)
}else {
return nil
}
}
func getImageFromAsset(asset: PHAsset) -> UIImage? {
var image: UIImage?
let imageManager = PHImageManager.default()
let targetSize = CGSize(width: 400, height: 400)
let options = PHImageRequestOptions()
options.isSynchronous = true
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { result, _ in
image = result
}
return image
}
func groupSimilarAssets(from assets: [PHAsset]) -> [[PHAsset]] {
var groupedAssets: [[PHAsset]] = []
var visitedAssets = Set<String>() // 记录已处理的 asset ID
for asset in assets {
// 跳过已分组的 asset
if visitedAssets.contains(asset.localIdentifier) {
continue
}
var similarGroup: [PHAsset] = [asset]
visitedAssets.insert(asset.localIdentifier)
for otherAsset in assets {
if asset.localIdentifier == otherAsset.localIdentifier || visitedAssets.contains(otherAsset.localIdentifier) {
continue
}
if areAssetsSimilar(asset1: asset, asset2: otherAsset) {
similarGroup.append(otherAsset)
visitedAssets.insert(otherAsset.localIdentifier)
}
}
if similarGroup.count > 1 {
groupedAssets.append(similarGroup)
}
}
return groupedAssets
}
func groupSalmaeAssets(from assets: [PHAsset]) -> [[PHAsset]] {
var groupedAssets: [[PHAsset]] = []
var visitedAssets = Set<String>() // 记录已处理的 asset ID
for asset in assets {
// 跳过已分组的 asset
if visitedAssets.contains(asset.localIdentifier) {
continue
}
var similarGroup: [PHAsset] = [asset]
visitedAssets.insert(asset.localIdentifier)
for otherAsset in assets {
if asset.localIdentifier == otherAsset.localIdentifier || visitedAssets.contains(otherAsset.localIdentifier) {
continue
}
if areAssetsSalmae(asset1: asset, asset2: otherAsset) {
similarGroup.append(otherAsset)
visitedAssets.insert(otherAsset.localIdentifier)
}
}
if similarGroup.count > 1 {
groupedAssets.append(similarGroup)
}
}
return groupedAssets
}
func areAssetsSimilar(asset1: PHAsset, asset2: PHAsset) -> Bool {
// 1. 判断创建时间(误差 < 1s)
// 2. 判断 GPS 位置是否接近(误差 < 10 米)
if let location1 = asset1.location, let location2 = asset2.location {
let distance = location1.distance(from: location2)
//5000
if distance > 10 { // 设定10米误差范围
return false
}
}
// 3. 判断尺寸是否相同
if asset1.pixelWidth != asset2.pixelWidth || asset1.pixelHeight != asset2.pixelHeight {
return false
}
let timeC = abs((asset1.creationDate?.timeIntervalSince1970 ?? 0) - (asset2.creationDate?.timeIntervalSince1970 ?? 0))
//5000000
if timeC > 1000 {
return false
}
let resource1 = PHAssetResource.assetResources(for: asset1).first
let resource2 = PHAssetResource.assetResources(for: asset2).first
//100000
if let size1 = resource1?.value(forKey: "fileSize") as? Int,
let size2 = resource2?.value(forKey: "fileSize") as? Int,
abs(size1 - size2) > 50000 {
return false
}
return true
}
func areAssetsSalmae(asset1: PHAsset, asset2: PHAsset) -> Bool {
// 1. 判断创建时间(误差 < 1s)
let timeC = abs((asset1.creationDate?.timeIntervalSince1970 ?? 0) - (asset2.creationDate?.timeIntervalSince1970 ?? 0))
//
if timeC > 1000 {
return false
}
// 2. 判断 GPS 位置是否接近(误差 < 10 米)
if let location1 = asset1.location, let location2 = asset2.location {
let distance = location1.distance(from: location2)
if distance > 100 { // 设定10米误差范围
return false
}
}
// 3. 判断尺寸是否相同
if asset1.pixelWidth != asset2.pixelWidth || asset1.pixelHeight != asset2.pixelHeight {
return false
}
// // 4. 判断文件大小
let resource1 = PHAssetResource.assetResources(for: asset1).first
let resource2 = PHAssetResource.assetResources(for: asset2).first
if let size1 = resource1?.value(forKey: "fileSize") as? Int,
let size2 = resource2?.value(forKey: "fileSize") as? Int,
abs(size1 - size2) > 1000 {
return false
}
return true
}
func getPhotoAssetSize(_ asset: PHAsset, completion: @escaping (Int64) -> Void) {
let options = PHImageRequestOptions()
options.isSynchronous = false
options.version = .original // 获取原始数据
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, _, _, _) in
let size = Int64(data?.count ?? 0)
completion(size)
}
}
func getVideoAssetSize(_ asset: PHAsset, completion: @escaping (Int64) -> Void) {
let options = PHVideoRequestOptions()
options.version = .original
PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (avAsset, _, _) in
if let urlAsset = avAsset as? AVURLAsset {
let size = try? urlAsset.url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
completion(Int64(size ?? 0))
} else {
completion(0)
}
}
}
func getDuplicatePhotos(completion: @escaping ([[String]]) -> Void) {
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else {
print("未获得相册权限")
DispatchQueue.main.async { completion([]) }
return
}
// 获取 Utilities (更多项目) 智能相册
let utilities = PHAssetCollection.fetchAssetCollections(
with: .smartAlbum,
subtype: .smartAlbumRecentlyAdded, // 这里会获取所有智能相册
options: nil
)
var duplicateGroups: [[String]] = []
utilities.enumerateObjects { collection, _, _ in
// 查找 Utilities 相册
if collection.localizedTitle == "Utilities" || collection.localizedTitle == "更多项目" {
// 获取 Utilities 中的所有子相册
if let utilityAlbums = PHCollectionList.fetchCollections(
in: collection as! PHCollectionList,
options: nil
) as? PHFetchResult<PHCollection> {
// 遍历查找 Duplicates 相册
utilityAlbums.enumerateObjects { subCollection, _, _ in
if let assetCollection = subCollection as? PHAssetCollection,
assetCollection.localizedTitle == "Duplicates" || assetCollection.localizedTitle == "重复项目" {
// 获取所有重复照片资源
let assets = PHAsset.fetchAssets(in: assetCollection, options: nil)
var currentGroup: [String] = []
var lastGroupId: String?
assets.enumerateObjects { asset, _, _ in
// 使用 groupingIdentifier 来分组重复照片
if let groupId = asset.value(forKey: "groupingIdentifier") as? String {
// 如果是新的组
if lastGroupId != nil && lastGroupId != groupId {
// 保存当前组(如果包含多个项目)
if currentGroup.count >= 2 {
duplicateGroups.append(currentGroup)
}
currentGroup = []
}
currentGroup.append(asset.localIdentifier)
lastGroupId = groupId
}
}
// 处理最后一组
if currentGroup.count >= 2 {
duplicateGroups.append(currentGroup)
}
}
}
}
}
}
DispatchQueue.main.async {
print("找到 \(duplicateGroups.count) 组重复照片")
completion(duplicateGroups)
}
}
}
func calculateTotalSize(of assets: [PHAsset],progress:@escaping (Int64,Int) -> Void = {pro,index in}, completion: @escaping (Int64,Int) -> Void = {fileSize,index in}) {
let group = DispatchGroup()
var totalSize: Int64 = 0
for (index,asset) in assets.enumerated() {
group.enter()
if asset.mediaType == .image {
getPhotoAssetSize(asset) { size in
totalSize += size
if index % 5 == 0{
progress(totalSize,index)
}
group.leave()
}
} else if asset.mediaType == .video {
getVideoAssetSize(asset) { size in
totalSize += size
if index % 5 == 0{
progress(totalSize,index)
}
group.leave()
}
} else {
group.leave()
}
}
group.notify(queue: .main) {
completion(totalSize,assets.count)
}
}
func getPHAsssetwithID(ids :[String]) -> PHAsset? {
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil)
let assetsArray = fetchResult.objects(at: IndexSet(0..<fetchResult.count))
return assetsArray.first
}
func findBestQualityImage(from identifiers: [String]) async -> [String] {
var bestIds: [String] = []
var maxFileSize: Int64 = 0
for id in identifiers {
if let asset = getPHAsssetwithID(ids: [id]) {
let resources = PHAssetResource.assetResources(for: asset)
if let resource = resources.first {
var sizeOnDisk: Int64 = 0
resource.value(forKey: "fileSize") as? Int64 ?? 0
// 选择文件大小最大的图片(通常质量更好)
if sizeOnDisk > maxFileSize {
maxFileSize = sizeOnDisk
bestIds = [id]
} else if sizeOnDisk == maxFileSize {
bestIds.append(id)
}
}
}
}
return bestIds.isEmpty ? [identifiers.first!] : bestIds
}
static func deleteAssets(localIdentifiers: [String],suc:@escaping () -> ()) {
// 获取要删除的 PHAsset
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: localIdentifiers, options: nil)
// 开始删除操作
PHPhotoLibrary.shared().performChanges({
// 创建删除请求
PHAssetChangeRequest.deleteAssets(fetchResult)
}) { success, error in
if success {
suc()
} else if let error = error {
print("删除失败: \(error.localizedDescription)")
}
}
}
}
//
// NewPhotoManager.swift
// PhoneManager
//
// Created by edy on 2025/3/25.
//
import Photos
import UIKit
class PhotoSimilarityFinder {
static func findSimilarPhotos(threshold: Double = 0.85, completion: @escaping ([[String]]) -> Void) {
print("开始查找相似照片...")
// 请求相册权限
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else {
print("未获得相册权限")
DispatchQueue.main.async { completion([]) }
return
}
// 创建获取选项
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
fetchOptions.predicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue)
// 获取所有图片资源
let assets = PHAsset.fetchAssets(with: fetchOptions)
print("找到 \(assets.count) 张图片")
if assets.count == 0 {
DispatchQueue.main.async { completion([]) }
return
}
// 创建并发队列
let concurrentQueue = DispatchQueue(label: "com.photoSimilarity.concurrent",
qos: .userInitiated,
attributes: .concurrent)
// 创建串行队列用于同步结果
let serialQueue = DispatchQueue(label: "com.photoSimilarity.serial")
// 用于存储处理过的资产
var processedAssets: [(asset: PHAsset, image: UIImage)] = []
let group = DispatchGroup()
// 图片请求选项
let imageRequestOptions = PHImageRequestOptions()
imageRequestOptions.deliveryMode = .highQualityFormat
imageRequestOptions.isSynchronous = false
imageRequestOptions.resizeMode = .exact
// 处理进度计数
let progressLock = NSLock()
var processedCount = 0
let totalCount = assets.count
// 分批处理
let batchSize = 10 // 每批处理的图片数量
let batches = stride(from: 0, to: assets.count, by: batchSize)
for batchStart in batches {
let batchEnd = min(batchStart + batchSize, assets.count)
concurrentQueue.async(group: group) {
for i in batchStart..<batchEnd {
group.enter()
let asset = assets.object(at: i)
PHImageManager.default().requestImage(
for: asset,
targetSize: CGSize(width: 300, height: 300),
contentMode: .aspectFit,
options: imageRequestOptions
) { image, info in
if let image = image {
serialQueue.async {
processedAssets.append((asset, image))
}
}
progressLock.lock()
processedCount += 1
print("处理进度: \(processedCount)/\(totalCount)")
progressLock.unlock()
group.leave()
}
}
}
}
// 等待所有图片加载完成
group.notify(queue: concurrentQueue) {
print("所有图片加载完成,开始比较相似度...")
// 使用并发处理进行相似度比较
let similarGroups = findSimilarGroupsConcurrently(
assets: processedAssets,
threshold: threshold
)
print("比较完成,共找到 \(similarGroups.count) 组相似图片")
DispatchQueue.main.async {
completion(similarGroups)
}
}
}
}
private static func findSimilarGroupsConcurrently(
assets: [(asset: PHAsset, image: UIImage)],
threshold: Double
) -> [[String]] {
var similarGroups: [[String]] = []
let groupLock = NSLock()
let processedIndicesLock = NSLock()
var processedIndices = Set<Int>()
let concurrentQueue = DispatchQueue(label: "com.similarity.compare.concurrent",
qos: .userInitiated,
attributes: .concurrent)
let group = DispatchGroup()
// 将比较任务分成多个块并发处理
let chunks = assets.count / 4 // 分成4个块
let ranges = stride(from: 0, to: assets.count, by: max(1, chunks))
for start in ranges {
let end = min(start + chunks, assets.count)
concurrentQueue.async(group: group) {
for i in start..<end {
processedIndicesLock.lock()
if processedIndices.contains(i) {
processedIndicesLock.unlock()
continue
}
processedIndicesLock.unlock()
var currentGroup: [String] = [assets[i].asset.localIdentifier]
var currentProcessed = Set<Int>()
for j in (i + 1)..<assets.count {
processedIndicesLock.lock()
if processedIndices.contains(j) {
processedIndicesLock.unlock()
continue
}
processedIndicesLock.unlock()
let isSimilar = OpenCVWrapper.areImagesSimilar(
assets[i].image,
withImage2: assets[j].image,
threshold: threshold
)
if isSimilar {
currentGroup.append(assets[j].asset.localIdentifier)
currentProcessed.insert(j)
}
}
if currentGroup.count > 1 {
groupLock.lock()
similarGroups.append(currentGroup)
groupLock.unlock()
processedIndicesLock.lock()
processedIndices.insert(i)
processedIndices.formUnion(currentProcessed)
processedIndicesLock.unlock()
print("找到一组相似图片,包含 \(currentGroup.count) 张图片")
}
}
}
}
group.wait()
return similarGroups
}
static func processSimilarPhotoGroups(simalr:CGFloat = 0.94,assetGroups: [[PHAsset]], completion: @escaping ([[String]],Double) -> Void) {
print("开始处理相似照片组...")
// 创建并发队列
let concurrentQueue = DispatchQueue(label: "com.photoSimilarity.concurrent",
qos: .userInitiated,
attributes: .concurrent)
// 创建串行队列用于同步结果
let serialQueue = DispatchQueue(label: "com.photoSimilarity.serial")
let group = DispatchGroup()
var filteredGroups: [[PHAsset]] = []
let groupsLock = NSLock()
// 图片请求选项
let imageRequestOptions = PHImageRequestOptions()
imageRequestOptions.deliveryMode = .highQualityFormat
imageRequestOptions.isSynchronous = true // 使用同步请求简化逻辑
imageRequestOptions.resizeMode = .exact
// 处理每个资产组
for assetGroup in assetGroups {
group.enter()
concurrentQueue.async {
var currentGroup = assetGroup
// 比较组内所有图片
for i in (0..<currentGroup.count).reversed() {
let asset1 = currentGroup[i]
var shouldRemove = false
// 获取第一张图片
var image1: UIImage?
PHImageManager.default().requestImage(
for: asset1,
targetSize: CGSize(width: 300, height: 300),
contentMode: .aspectFit,
options: imageRequestOptions
) { img, _ in
image1 = img
}
guard let firstImage = image1 else { continue }
// 与其他图片比较
for j in (0..<i).reversed() {
let asset2 = currentGroup[j]
var image2: UIImage?
PHImageManager.default().requestImage(
for: asset2,
targetSize: CGSize(width: 300, height: 300),
contentMode: .aspectFit,
options: imageRequestOptions
) { img, _ in
image2 = img
}
guard let secondImage = image2 else { continue }
// 使用OpenCV比较相似度
let similarity = OpenCVWrapper.compareImageSimilarity(firstImage, withImage2: secondImage)
if similarity >= 1 && simalr < 1 {
shouldRemove = true
break
}
// 如果相似度低于0.85,标记移除当前图片
if similarity < simalr {
shouldRemove = true
break
}
}
if shouldRemove {
currentGroup.remove(at: i)
}
}
// 如果组内至少有2张图片,保存这个组
if currentGroup.count >= 2 {
groupsLock.lock()
filteredGroups.append(currentGroup)
groupsLock.unlock()
}
group.leave()
}
}
// 等待所有处理完成
group.notify(queue: .main) {
// 将结果转换为 localIdentifier 数组
let result = filteredGroups.map { group in
group.map { $0.localIdentifier }
}
var fileSizeArray:[PHAsset] = []
for assetArray in filteredGroups {
for asset in assetArray {
fileSizeArray.append(asset)
}
}
PhotoAndVideoMananger.mananger.calculateTotalSize(of: fileSizeArray, completion: { fileSize,index in
completion(result,Double(fileSize))
})
print("处理完成,剩余 \(result.count) 组相似图片")
}
}
static func processSimilarVideoGroups(videoGroups: [[PHAsset]], completion: @escaping ([[String]]) -> Void) {
print("开始处理相似视频组...")
// 创建并发队列
let concurrentQueue = DispatchQueue(label: "com.videoSimilarity.concurrent",
qos: .userInitiated,
attributes: .concurrent)
let group = DispatchGroup()
var filteredGroups: [[PHAsset]] = []
let groupsLock = NSLock()
// 视频请求选项
let videoRequestOptions = PHVideoRequestOptions()
videoRequestOptions.deliveryMode = .highQualityFormat
videoRequestOptions.isNetworkAccessAllowed = true
// 处理每个视频组
for videoGroup in videoGroups {
group.enter()
concurrentQueue.async {
var currentGroup = videoGroup
// 比较组内所有视频
for i in (0..<currentGroup.count).reversed() {
let asset1 = currentGroup[i]
var shouldRemove = false
// 获取第一个视频的关键帧
var image1: UIImage?
let semaphore1 = DispatchSemaphore(value: 0)
extractKeyFrame(from: asset1, options: videoRequestOptions) { keyFrame in
image1 = keyFrame
semaphore1.signal()
}
semaphore1.wait()
guard let firstImage = image1 else { continue }
// 与其他视频比较
for j in (0..<i).reversed() {
let asset2 = currentGroup[j]
var image2: UIImage?
let semaphore2 = DispatchSemaphore(value: 0)
extractKeyFrame(from: asset2, options: videoRequestOptions) { keyFrame in
image2 = keyFrame
semaphore2.signal()
}
semaphore2.wait()
guard let secondImage = image2 else { continue }
// 使用OpenCV比较相似度
let similarity = OpenCVWrapper.compareImageSimilarity(firstImage, withImage2: secondImage)
// 如果相似度低于0.85,标记移除当前视频
if similarity < 0.85 {
shouldRemove = true
break
}
}
if shouldRemove {
currentGroup.remove(at: i)
}
}
// 如果组内至少有2个视频,保存这个组
if currentGroup.count >= 2 {
groupsLock.lock()
filteredGroups.append(currentGroup)
groupsLock.unlock()
}
group.leave()
}
}
// 等待所有处理完成
group.notify(queue: .main) {
// 将结果转换为 localIdentifier 数组
let result = filteredGroups.map { group in
group.map { $0.localIdentifier }
}
print("处理完成,剩余 \(result.count) 组相似视频")
completion(result)
}
}
// 辅助方法:从视频资源中提取关键帧
private static func extractKeyFrame(from videoAsset: PHAsset,
options: PHVideoRequestOptions,
completion: @escaping (UIImage?) -> Void) {
// 确保资源是视频
guard videoAsset.mediaType == .video else {
completion(nil)
return
}
// 创建AVAsset
PHImageManager.default().requestAVAsset(forVideo: videoAsset,
options: options) { asset, _, _ in
guard let asset = asset else {
completion(nil)
return
}
// 创建视频生成器
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
generator.maximumSize = CGSize(width: 300, height: 300)
// 获取视频时长
let duration = CMTimeGetSeconds(asset.duration)
// 在视频1/3处取帧
let time = CMTime(seconds: duration / 3, preferredTimescale: 600)
do {
let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
let image = UIImage(cgImage: cgImage)
completion(image)
} catch {
print("提取视频关键帧失败: \(error)")
completion(nil)
}
}
}
}
//
// UserActivityManager.swift
// PhoneManager
//
// Created by edy on 2025/3/26.
//
import AppIntents
@available(iOS 16, *)
struct LaunchAppIntent: AppIntent {
static var title: LocalizedStringResource = "打开应用"
static var description: IntentDescription = IntentDescription("打开并启动应用")
// 执行操作
func perform() async throws -> some IntentResult {
// 这里可以添加启动应用后的具体操作
return .result()
}
}
@available(iOS 16, *)
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: LaunchAppIntent(),
phrases: [
"打开 \(.applicationName)",
"启动 \(.applicationName)",
"运行 \(.applicationName)"
],
systemImageName: "app.fill"
)
}
}
......@@ -43,7 +43,7 @@ let whiteColor:String = "#ffffff"
let mColor:String = "#0082FF"
let navBack:String = "#0F0F0F"
let navBack:String = "#ffffff"
let blackColor:String = "#000000"
......
......@@ -24,6 +24,36 @@ let cWindow:UIWindow? = {
}()
func formatFileSize(_ bytes: Double) -> String {
let units = ["B", "KB", "MB", "GB", "TB"]
var size = bytes
var unitIndex = 0
// 循环计算合适的单位
while size >= 1024 && unitIndex < units.count - 1 {
size /= 1024
unitIndex += 1
}
// 格式化输出
return String(format: "%.2f %@", size, units[unitIndex])
}
func timeFormatter() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "h:mma" // 注意这里的格式
formatter.amSymbol = "AM"
formatter.pmSymbol = "PM"
return formatter.string(from: Date())
}
func weekFormatter() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, MMMM d" // 自定义格式
return formatter.string(from: Date())
}
public func Print(_ items: Any...) {
#if DEBUG
......
......@@ -15,6 +15,7 @@ enum AnimationStatus {
enum OperStatus {
case close
case delete
}
enum CommonPush {
......
......@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need album permission to access image storage information</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
......@@ -11,8 +9,10 @@
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>We need to access the network to load content</string>
<key>NSUserActivityTypes</key>
<array>
<string>LaunchAppIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
......
......@@ -9,5 +9,6 @@ target 'PhoneManager' do
pod 'Alamofire'
pod 'lottie-ios'
pod 'SnapKit'
pod 'OpenCV', '~> 4.0.0'
end
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