100Years пре 1 недеља
родитељ
комит
1407bf8720

+ 18 - 2
AIEmoji.xcodeproj/project.pbxproj

@@ -155,6 +155,8 @@
 		A8BA76472DA4CC70000B6707 /* TSPTPSelectStyleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BA76462DA4CC6C000B6707 /* TSPTPSelectStyleView.swift */; };
 		A8BA764F2DA50B52000B6707 /* CpuMapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BA764E2DA50B52000B6707 /* CpuMapManager.swift */; };
 		A8BA76522DA51600000B6707 /* TSPTPImageHintVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BA76512DA515FF000B6707 /* TSPTPImageHintVC.swift */; };
+		A8BA76572DA61A00000B6707 /* TSUploadPhotoPrivacyAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BA76562DA619FF000B6707 /* TSUploadPhotoPrivacyAlertVC.swift */; };
+		A8BA765B2DA63E9C000B6707 /* TSClickableLinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BA765A2DA63E9B000B6707 /* TSClickableLinkLabel.swift */; };
 		A8EEADD42D3E6C660032C5A0 /* Flower💐.json in Resources */ = {isa = PBXBuildFile; fileRef = A8EEADD32D3E6C610032C5A0 /* Flower💐.json */; };
 		A8EEADD62D3E6CD80032C5A0 /* Fish🐠.json in Resources */ = {isa = PBXBuildFile; fileRef = A8EEADD52D3E6CD30032C5A0 /* Fish🐠.json */; };
 		A8EEADD82D3E74D20032C5A0 /* Pink🩷.json in Resources */ = {isa = PBXBuildFile; fileRef = A8EEADD72D3E74CB0032C5A0 /* Pink🩷.json */; };
@@ -386,6 +388,8 @@
 		A8BA764C2DA4F689000B6707 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		A8BA764E2DA50B52000B6707 /* CpuMapManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuMapManager.swift; sourceTree = "<group>"; };
 		A8BA76512DA515FF000B6707 /* TSPTPImageHintVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSPTPImageHintVC.swift; sourceTree = "<group>"; };
+		A8BA76562DA619FF000B6707 /* TSUploadPhotoPrivacyAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSUploadPhotoPrivacyAlertVC.swift; sourceTree = "<group>"; };
+		A8BA765A2DA63E9B000B6707 /* TSClickableLinkLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSClickableLinkLabel.swift; sourceTree = "<group>"; };
 		A8EEADD32D3E6C610032C5A0 /* Flower💐.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Flower💐.json"; sourceTree = "<group>"; };
 		A8EEADD52D3E6CD30032C5A0 /* Fish🐠.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Fish🐠.json"; sourceTree = "<group>"; };
 		A8EEADD72D3E74CB0032C5A0 /* Pink🩷.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Pink🩷.json"; sourceTree = "<group>"; };
@@ -943,6 +947,7 @@
 		A80EDDDC2D6EB17D003CD332 /* TSPTPGeneratorVC */ = {
 			isa = PBXGroup;
 			children = (
+				A8BA76552DA619DD000B6707 /* TSUploadPhotoPrivacyAlertVC */,
 				A8BA76502DA515F9000B6707 /* TSPTPImageHintVC */,
 				A8BA763D2DA4C8F9000B6707 /* TSPTPInputVC */,
 				A83404CF2D9D16E400C140E4 /* TSAIPhotoGeneratorBaseVC */,
@@ -1286,6 +1291,14 @@
 			path = TSPTPImageHintVC;
 			sourceTree = "<group>";
 		};
+		A8BA76552DA619DD000B6707 /* TSUploadPhotoPrivacyAlertVC */ = {
+			isa = PBXGroup;
+			children = (
+				A8BA76562DA619FF000B6707 /* TSUploadPhotoPrivacyAlertVC.swift */,
+			);
+			path = TSUploadPhotoPrivacyAlertVC;
+			sourceTree = "<group>";
+		};
 		A8F774602D38E8B000AA6E93 = {
 			isa = PBXGroup;
 			children = (
@@ -1588,6 +1601,7 @@
 		A8F776292D3A70AA00AA6E93 /* UILabel */ = {
 			isa = PBXGroup;
 			children = (
+				A8BA765A2DA63E9B000B6707 /* TSClickableLinkLabel.swift */,
 				A8F7762A2D3A70AF00AA6E93 /* PaddedLabel.swift */,
 			);
 			path = UILabel;
@@ -1964,10 +1978,12 @@
 				A80E72772D41EFF900C64288 /* TSEmojisTutorialsVC.swift in Sources */,
 				A83404C82D9BEC0E00C140E4 /* UIFont+TSEx.swift in Sources */,
 				A8F776482D3DE9F600AA6E93 /* TSSmallIconBrowseCell.swift in Sources */,
+				A8BA765B2DA63E9C000B6707 /* TSClickableLinkLabel.swift in Sources */,
 				A8F7753D2D3918F800AA6E93 /* TSNetWork+Business.swift in Sources */,
 				A83404D12D9D16FA00C140E4 /* TSAIPhotoGeneratorBaseVC.swift in Sources */,
 				A80327BF2D81578900AF7878 /* TSPromptTextView.swift in Sources */,
 				A80EDE022D6F1CCD003CD332 /* TSPTPGeneratorVC.swift in Sources */,
+				A8BA76572DA61A00000B6707 /* TSUploadPhotoPrivacyAlertVC.swift in Sources */,
 				A8BA76452DA4CB9A000B6707 /* TSPTPUploadView.swift in Sources */,
 				A85E479F2D6859FA0018D62D /* TSRandomTextPicker.swift in Sources */,
 				A8BA76522DA51600000B6707 /* TSPTPImageHintVC.swift in Sources */,
@@ -2086,7 +2102,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 4;
+				CURRENT_PROJECT_VERSION = 5;
 				DEVELOPMENT_TEAM = 65UD255J84;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -2125,7 +2141,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 4;
+				CURRENT_PROJECT_VERSION = 5;
 				DEVELOPMENT_TEAM = 65UD255J84;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;

+ 22 - 0
AIEmoji/Assets.xcassets/Common/privacy_shield.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "privacy_shield@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "privacy_shield@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
AIEmoji/Assets.xcassets/Common/privacy_shield.imageset/privacy_shield@2x.png


BIN
AIEmoji/Assets.xcassets/Common/privacy_shield.imageset/privacy_shield@3x.png


+ 14 - 1
AIEmoji/Business/TSPTPGeneratorVC/TSPTPInputVC/TSPTPInputVC.swift

@@ -150,7 +150,10 @@ class TSPTPInputVC: TSBaseVC {
                 uploadView.upLoadImage = nil
             }else{//添加
                 
-                if UserDefaults.standard.string(forKey: "isFirstUploadImagePTP") == nil {
+                if TSUploadPhotoPrivacyAlertVC.isShowUploadImagePrivacyHint {
+                    TSUploadPhotoPrivacyAlertVC.isShowUploadImagePrivacyHint = false
+                    presentPrivacyAlertVC()
+                }else if UserDefaults.standard.string(forKey: "isFirstUploadImagePTP") == nil {
                     UserDefaults.standard.set("1", forKey: "isFirstUploadImagePTP")
                     UserDefaults.standard.synchronize()
                     presentModalHintVC()
@@ -403,6 +406,16 @@ extension TSPTPInputVC {
         }
         kPresentModalVC(target: self, modelVC: vc,transitionStyle: .crossDissolve)
     }
+    
+    func presentPrivacyAlertVC(){
+        let vc = TSUploadPhotoPrivacyAlertVC()
+        vc.clickHandle = { [weak self]  in
+            guard let self = self else { return }
+            presentModalHintVC()
+        }
+        kPresentModalVC(target: self, modelVC: vc,transitionStyle: .crossDissolve)
+    }
+    
     func setUpCusStackView(){
         
         collectionComponent.collectionView.addSubview(cusStackView)

+ 187 - 0
AIEmoji/Business/TSPTPGeneratorVC/TSUploadPhotoPrivacyAlertVC/TSUploadPhotoPrivacyAlertVC.swift

@@ -0,0 +1,187 @@
+//
+//  TSUploadPhotoPrivacyAlertVC.swift
+//  AIEmoji
+//
+//  Created by 100Years on 2025/4/8.
+//
+
+class TSUploadPhotoPrivacyAlertVC: TSBaseVC {
+    
+    var clickHandle:(()->Void)?
+    lazy var cusStackView: TSCustomStackView = {
+        let cusStackView = TSCustomStackView(axis: .vertical,spacing: 0)
+        return cusStackView
+    }()
+    
+    
+    lazy var submitBtn: UIButton = {
+        let submitBtn = kCreateNormalSubmitBtn(title: "Accept".localized) { [weak self]  in
+            guard let self = self else { return }
+            dismiss()
+            clickHandle?()
+        }
+        submitBtn.cornerRadius = 24.0
+        return submitBtn
+    }()
+    
+    lazy var topImageTtileView: UIView = {
+        let topImageTtileView = UIView()
+        
+        let imageView:UIImageView = UIImageView.createImageView(imageName: "privacy_shield")
+        topImageTtileView.addSubview(imageView)
+        imageView.snp.makeConstraints { make in
+            make.trailing.equalTo(-23)
+            make.centerY.equalToSuperview()
+            make.width.equalTo(85)
+            make.height.equalTo(98)
+            make.top.bottom.equalTo(0)
+        }
+        
+        let textLabel = UILabel.createLabel(text: "We Value Your Privacy".localized,font: .font(name: .PoppinsBlackItalic,size: 24),textColor: .white,numberOfLines: 0)
+        topImageTtileView.addSubview(textLabel)
+        textLabel.snp.makeConstraints { make in
+            make.leading.equalTo(16)
+            make.trailing.equalTo(imageView.snp.leading).offset(-10)
+            make.centerY.equalToSuperview()
+        }
+        return topImageTtileView
+    }()
+    
+    
+    override func createView() {
+        setNavBarViewHidden(true)
+        
+        contentView.addSubview(submitBtn)
+        submitBtn.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.width.equalTo(250*kDesignScale)
+            make.height.equalTo(48)
+            make.bottom.equalTo(-10-k_Height_safeAreaInsetsBottom())
+        }
+        
+        contentView.addSubview(cusStackView)
+        cusStackView.snp.makeConstraints { make in
+            make.top.equalTo(0)
+            make.leading.trailing.equalToSuperview()
+            make.bottom.equalTo(submitBtn.snp.top).offset(-10)
+        }
+        
+
+        setUpUI()
+    }
+    
+    let lineSpacing = 6.0
+    func setUpUI(){
+        
+        cusStackView.addSpacing(height: 28+k_Height_StatusBar)
+        cusStackView.addSubviewToStack(topImageTtileView)
+        cusStackView.addSpacing(height: 23)
+        
+        let textLabel = UILabel.createLabel(text: "We need your consent to use this feature:".localized,font: .font(size: 16),textColor: .white,numberOfLines: 0)
+        textLabel.setLineSpacing(8)
+        cusStackView.addSubviewToStack(textLabel)
+        textLabel.snp.makeConstraints { make in
+            make.leading.equalTo(16)
+            make.trailing.equalTo(-35)
+        }
+        cusStackView.addSpacing(height: 16)
+        addTextInfo()
+        cusStackView.addSpacing(height: 16)
+        addTextPrivacy()
+    }
+
+    func addTextInfo(){
+        cusStackView.addSubviewToStack(getTextInfoCell(text: "This App is photo or video editors that allow you to edit portraits with highly realistic facial transformations. We do not use photos or videos you provide when you use the Apps for any reason other than to provide you with the editing functionality of the Apps.".localized))
+        
+        cusStackView.addSpacing(height: lineSpacing)
+        
+        cusStackView.addSubviewToStack(getTextInfoCell(text: "Every photo you select for editing will be encrypted and temporarily cached on cloud servers for image processing and face transformation.".localized))
+        
+        cusStackView.addSpacing(height: lineSpacing)
+        
+        cusStackView.addSubviewToStack(getTextInfoCell(text: "We use third-party cloud providers - Amazon Web Services - to process and edit photos and videos.".localized))
+        
+        cusStackView.addSpacing(height: lineSpacing)
+        
+        cusStackView.addSubviewToStack(getTextInfoCell(text: "Photos or videos only remain in the cloud for a maximum limited period of 24 to 48 hours, for improved performance and lower bandwidth allowing additional changes to recently selected photos or videos in an optimal manner.".localized))
+    }
+    
+    func getTextInfoCell(text:String) -> UIView {
+        
+        let bgView = UIView()
+        let pointView = UIView()
+        pointView.backgroundColor = .white.withAlphaComponent(0.7)
+        pointView.cornerRadius = 2
+        bgView.addSubview(pointView)
+        pointView.snp.makeConstraints { make in
+            make.top.equalTo(7)
+            make.leading.equalTo(22)
+            make.width.height.equalTo(4)
+        }
+        
+        let textLabel1 = UILabel.createLabel(text: text,font: .font(size: 14),textColor: .white.withAlphaComponent(0.7),numberOfLines: 0)
+        textLabel1.setLineSpacing(lineSpacing)
+        bgView.addSubview(textLabel1)
+        textLabel1.snp.makeConstraints { make in
+            make.top.bottom.equalToSuperview()
+            make.leading.equalTo(16+16)
+            make.trailing.equalTo(-16)
+        }
+        return bgView
+    }
+    
+    func addTextPrivacy(){
+        let bgView = UIView()
+        let fullText = "By clicking \"Accept\", you give explicit consent that our Privacy Policy. To learn more about how we handle your personal data and our third-party partners, please refer to our Privacy Policy."
+        let label = TSClickableLinkLabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.setText(
+                    fullText,
+                    linkTexts: ["Privacy Policy"],
+                    highlightLastOccurrence: true, // 关键参数,只高亮最后一个匹配项
+                    lineSpacing: lineSpacing, // 设置行间距为8点
+                    normalAttributes: [
+                        .foregroundColor: UIColor.white.withAlphaComponent(0.8),
+                        .font: UIFont.font(size: 14)
+                    ],
+                    linkAttributes: [
+                        .foregroundColor: "#4E89FF".uiColor,
+                        .underlineStyle: NSUnderlineStyle.single.rawValue,
+                        .font: UIFont.font(size: 14)
+                    ]) { [weak self] linkText in
+                        guard let self = self else { return }
+                        print("Link tapped: \(linkText)")
+                        // 处理点击事件,例如打开隐私政策页面
+                        
+                        let vc = TSBusinessWebVC(urlType: .privacy)
+                        kPresentModalVC(target: self, modelVC: vc,transitionStyle:.crossDissolve)
+                    }
+ 
+        cusStackView.addSubviewToStack(bgView)
+        bgView.snp.makeConstraints { make in
+            make.leading.equalTo(0)
+            make.trailing.equalTo(0)
+        }
+
+        bgView.addSubview(label)
+        label.snp.makeConstraints { make in
+            make.top.bottom.equalToSuperview()
+            make.leading.equalTo(16)
+            make.trailing.equalTo(-16)
+        }
+    }
+    
+    
+    static var isShowUploadImagePrivacyHint:Bool{
+        get {
+            return UserDefaults.standard.string(forKey: "isFirstUploadPhotoPrivacy") == nil
+        }
+        
+        set {
+            UserDefaults.standard.set(newValue ? nil : "1", forKey: "isFirstUploadPhotoPrivacy")
+            UserDefaults.standard.synchronize()
+        }
+    }
+
+}
+

+ 1 - 0
AIEmoji/Business/TSSetingVC/SetingVC/TSSetingModel.swift

@@ -7,6 +7,7 @@
 
 enum SettingType: String, CaseIterable {
     case howToUse = "How to add emojis to iMessages?"
+    case removeCloudData = "Remove Cloud Data"
     case update = "Update Version"
     case shareus = "Share us"
     case rateus = "Rate us"

+ 3 - 0
AIEmoji/Business/TSSetingVC/SetingVC/TSSetingVC.swift

@@ -64,6 +64,9 @@ class TSSetingVC: TSBaseVC {
             case .update:
                 viewModel.updateApp(parent: self)
                 break
+            case .removeCloudData:
+                viewModel.removeCloudData(parent: self)
+                break
 //            case .about:
 //                break
             case .rateus:

+ 10 - 0
AIEmoji/Business/TSSetingVC/SetingVC/TSSetingViewModel.swift

@@ -92,6 +92,16 @@ class TSSetingViewModel: ObservableObject {
             isViper = PurchaseManager.default.isVip
         }
     }
+    
+    
+    func removeCloudData(parent: UIViewController) {
+        TSToastShared.showLoading(containerView: parent.view)
+        kDelayOnMainThread(5) {
+            TSToastShared.hideLoading()
+            kSavePhotoSuccesswShared.show(atView: parent.view,text: "Removed Successfully".localized,showViewBtn: false)
+        }
+    }
+    
 }
 
 extension UIImage {

+ 1 - 1
AIEmoji/Business/TSSetingVC/TSBusinessWebVC/TSBusinessWebVC.swift

@@ -10,7 +10,7 @@ import WebKit
 class TSBusinessWebVC: TSBaseVC , WKNavigationDelegate {
     
     enum UrlType:String {
-        case privacy = "http://100yearslater.com/AI-Chat-Privacy-Policy.html"
+        case privacy = "http://100yearslater.com/Privacy.html"
         case terms = "https://doc-hosting.flycricket.io/hahaemoji-terms-of-use/7488c423-9bb6-480e-9a38-f52fba511335/terms"
 
         func getTitle() -> String {

+ 181 - 0
AIEmoji/Common/View/UILabel/TSClickableLinkLabel.swift

@@ -0,0 +1,181 @@
+//
+//  TSClickableLinkLabel.swift
+//  AIEmoji
+//
+//  Created by 100Years on 2025/4/8.
+//
+
+import UIKit
+
+class TSClickableLinkLabel: UILabel {
+    
+    // MARK: - Properties
+    private var tapHandler: ((String) -> Void)?
+    private var linkRanges: [NSRange] = []
+    private var linkAttributes: [NSAttributedString.Key: Any] = [
+        .foregroundColor: UIColor.blue,
+        .underlineStyle: NSUnderlineStyle.single.rawValue
+    ]
+    private var normalAttributes: [NSAttributedString.Key: Any] = [
+        .foregroundColor: UIColor.black,
+        .font: UIFont.systemFont(ofSize: 16)
+    ]
+    private var lineSpacing: CGFloat = 0
+    
+    // MARK: - Initialization
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setup()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setup()
+    }
+    
+    private func setup() {
+        isUserInteractionEnabled = true
+        numberOfLines = 0
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
+        addGestureRecognizer(tapGesture)
+    }
+    
+    // MARK: - Public Methods
+    func setText(_ text: String,
+                 linkTexts: [String],
+                 highlightLastOccurrence: Bool = false,
+                 lineSpacing: CGFloat = 0,
+                 normalAttributes: [NSAttributedString.Key: Any]? = nil,
+                 linkAttributes: [NSAttributedString.Key: Any]? = nil,
+                 tapHandler: ((String) -> Void)? = nil) {
+        
+        self.lineSpacing = lineSpacing
+        
+        if let normalAttrs = normalAttributes {
+            self.normalAttributes = normalAttrs
+        }
+        
+        if let linkAttrs = linkAttributes {
+            self.linkAttributes = linkAttrs
+        }
+        
+        self.tapHandler = tapHandler
+        self.linkRanges.removeAll()
+        
+        // 创建段落样式,设置行间距
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = lineSpacing
+        
+        // 将段落样式添加到普通文本属性
+        var finalNormalAttributes = self.normalAttributes
+        finalNormalAttributes[.paragraphStyle] = paragraphStyle
+        
+        let attributedString = NSMutableAttributedString(string: text, attributes: finalNormalAttributes)
+        
+        // 查找并设置所有链接文本的样式
+        for linkText in linkTexts {
+            let ranges = rangesOfString(linkText, in: text)
+            
+            if highlightLastOccurrence, let lastRange = ranges.last {
+                // 只标记最后一个匹配项
+                var finalLinkAttributes = self.linkAttributes
+                finalLinkAttributes[.paragraphStyle] = paragraphStyle
+                attributedString.addAttributes(finalLinkAttributes, range: lastRange)
+                linkRanges.append(lastRange)
+            } else {
+                // 标记所有匹配项(原始行为)
+                for range in ranges {
+                    var finalLinkAttributes = self.linkAttributes
+                    finalLinkAttributes[.paragraphStyle] = paragraphStyle
+                    attributedString.addAttributes(finalLinkAttributes, range: range)
+                    linkRanges.append(range)
+                }
+            }
+        }
+        
+        attributedText = attributedString
+    }
+    
+    // 查找字符串中所有匹配的范围
+    private func rangesOfString(_ substring: String, in string: String) -> [NSRange] {
+        var ranges: [NSRange] = []
+        var searchRange = NSRange(location: 0, length: string.utf16.count)
+        
+        while searchRange.location < string.utf16.count {
+            let foundRange = (string as NSString).range(of: substring, options: [], range: searchRange)
+            if foundRange.location != NSNotFound {
+                ranges.append(foundRange)
+                searchRange.location = foundRange.location + foundRange.length
+                searchRange.length = string.utf16.count - searchRange.location
+            } else {
+                break
+            }
+        }
+        
+        return ranges
+    }
+    
+    // MARK: - Tap Handling
+    @objc private func handleTap(_ gesture: UITapGestureRecognizer) {
+        guard gesture.state == .ended else { return }
+        
+        let layoutManager = NSLayoutManager()
+        let textContainer = NSTextContainer(size: bounds.size)
+        let textStorage = NSTextStorage(attributedString: attributedText ?? NSAttributedString())
+        
+        layoutManager.addTextContainer(textContainer)
+        textStorage.addLayoutManager(layoutManager)
+        
+        textContainer.lineFragmentPadding = 0
+        textContainer.lineBreakMode = lineBreakMode
+        textContainer.maximumNumberOfLines = numberOfLines
+        
+        let location = gesture.location(in: self)
+        let textBoundingBox = layoutManager.usedRect(for: textContainer)
+        let textContainerOffset = CGPoint(
+            x: (bounds.width - textBoundingBox.width) * 0.5 - textBoundingBox.minX,
+            y: (bounds.height - textBoundingBox.height) * 0.5 - textBoundingBox.minY
+        )
+        
+        let locationInTextContainer = CGPoint(
+            x: location.x - textContainerOffset.x,
+            y: location.y - textContainerOffset.y
+        )
+        
+        let characterIndex = layoutManager.characterIndex(
+            for: locationInTextContainer,
+            in: textContainer,
+            fractionOfDistanceBetweenInsertionPoints: nil
+        )
+        
+        // 检查点击是否在链接范围内
+        for range in linkRanges {
+            if NSLocationInRange(characterIndex, range) {
+                let linkText = (textStorage.string as NSString).substring(with: range)
+                tapHandler?(linkText)
+                return
+            }
+        }
+    }
+    
+    // MARK: - 计算带行间距的文本高度
+    func calculateTextHeight(width: CGFloat) -> CGFloat {
+        guard let text = text else { return 0 }
+        
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = lineSpacing
+        
+        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
+        let boundingBox = text.boundingRect(
+            with: constraintRect,
+            options: [.usesLineFragmentOrigin, .usesFontLeading],
+            attributes: [
+                .font: font ?? UIFont.systemFont(ofSize: 17),
+                .paragraphStyle: paragraphStyle
+            ],
+            context: nil
+        )
+        
+        return ceil(boundingBox.height)
+    }
+}

+ 4 - 0
AIEmoji/de.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "Beispiele für schlechte Fotos";
 "Group photos, covered faces, nudes" = "Gruppenfoto, verdecktes Gesicht, Nacktaufnahmen";
 "No Style" = "Kein Stil";
+
+"Remove Cloud Data" = "Cloud-Daten entfernen";
+"Removed Successfully" = "Erfolgreich entfernt";
+"Do not show again" = "Nicht mehr anzeigen";

+ 4 - 0
AIEmoji/en.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "Bad photo examples";
 "Group photos, covered faces, nudes" = "Group photos, covered faces, nudes";
 "No Style" = "No Style";
+
+"Remove Cloud Data" = "Remove Cloud Data";
+"Removed Successfully" = "Removed Successfully";
+"Do not show again" = "Do not show again";

+ 4 - 0
AIEmoji/es.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "Ejemplos de malas fotos";
 "Group photos, covered faces, nudes" = "Foto grupal, rostro cubierto, desnudo";
 "No Style" = "Sin estilo";
+
+"Remove Cloud Data" = "Eliminar datos de la nube";
+"Removed Successfully" = "Eliminado con éxito";
+"Do not show again" = "No volver a mostrar";

+ 4 - 0
AIEmoji/ja.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "悪い写真の例";
 "Group photos, covered faces, nudes" = "集合写真、顔が隠れている、裸体";
 "No Style" = "スタイルがありません";
+
+"Remove Cloud Data" = "クラウドデータの削除";
+"Removed Successfully" = "正常に削除されました";
+"Do not show again" = "ヒントはもういらない";

+ 4 - 0
AIEmoji/ko.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "나쁜 사진 예시";
 "Group photos, covered faces, nudes" = "단체 사진, 얼굴 가려짐, 노출";
 "No Style" = "스타일 없음";
+
+"Remove Cloud Data" = "클라우드 데이터 제거";
+"Removed Successfully" = "성공적으로 제거되었습니다";
+"Do not show again" = "다시 표시하지 않음";

+ 4 - 0
AIEmoji/pt-BR.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "Exemplos de más fotos";
 "Group photos, covered faces, nudes" = "Foto em grupo, rosto coberto, nudez";
 "No Style" = "Sem estilo";
+
+"Remove Cloud Data" = "Remover dados da nuvem";
+"Removed Successfully" = "Removido com sucesso";
+"Do not show again" = "Não mostre novamente";

+ 4 - 0
AIEmoji/pt-PT.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "Exemplos de más fotos";
 "Group photos, covered faces, nudes" = "Foto em grupo, rosto coberto, nudez";
 "No Style" = "Sem estilo";
+
+"Remove Cloud Data" = "Remover dados da nuvem";
+"Removed Successfully" = "Removido com sucesso";
+"Do not show again" = "Não mostre novamente";

+ 4 - 0
AIEmoji/zh-Hans.lproj/Localizable.strings

@@ -132,3 +132,7 @@
 "Bad photo examples" = "糟糕照片示例";
 "Group photos, covered faces, nudes" = "多人集体照、脸部遮挡、裸体";
 "No Style" = "无风格";
+
+"Remove Cloud Data" = "删除云数据";
+"Removed Successfully" = "删除成功";
+"Do not show again" = "不再提示";

+ 4 - 0
AIEmoji/zh-Hant.lproj/Localizable.strings

@@ -128,3 +128,7 @@
 "Hyperrealistic macro view of dewdrops on spiderweb, morning sunlight creating rainbow spectra, blurred forest background with bokeh effects" = "蜘蛛網上露珠的超現實宏觀視圖、晨光創造彩虹光譜、模糊的森林背景與散景效果";
 "Cybernetic wildlife sanctuary at dawn, solar-panel trees powering force fields, robotic caretakers feeding holographic animals in savanna simulation" = "黎明時分的控制論野生動物保護區,太陽能電池板樹為力場提供動力,機器人看護者在稀樹草原類比中餵養全息動物";
 "Minimalist geometric landscape with gradient-colored pyramids, clean lines intersecting with organic cloud formations, symmetrical composition" = "極簡主義幾何景觀,漸變色金字塔,乾淨的線條與有機雲層相交,對稱的構圖";
+
+"Remove Cloud Data" = "刪除雲端數據";
+"Removed Successfully" = "刪除成功";
+"Do not show again" = "不再提示";