// // KMSignUpView.swift // PDF Reader Pro // // Created by wanjun on 2024/10/23. // import Cocoa import Combine class KMSignUpView: KMBaseXibView { @IBOutlet weak var signUpLabel: NSTextField! @IBOutlet weak var loginModeBox: NSBox! @IBOutlet weak var verificationCodeButton: NSButton! @IBOutlet weak var selectBox1: NSBox! @IBOutlet weak var passwordButton: NSButton! @IBOutlet weak var selectBox2: NSBox! @IBOutlet weak var emailBox: NSBox! @IBOutlet weak var emailTextField: NSTextField! @IBOutlet weak var emailErrorLabel: NSTextField! @IBOutlet weak var passwordBox: NSBox! @IBOutlet weak var verifficationView: NSView! @IBOutlet weak var verifficationBox: NSBox! @IBOutlet weak var verifficationTextField: NSTextField! @IBOutlet weak var sendBox: KMBox! @IBOutlet weak var sendLabel: NSTextField! @IBOutlet weak var passwordView: NSView! @IBOutlet weak var passwordTextField: NSTextField! @IBOutlet weak var passwordTextField1: NSSecureTextField! @IBOutlet weak var visibleButton: NSButton! @IBOutlet weak var passwordErrorLabel: NSTextField! @IBOutlet weak var stayCheckButton: NSButton! @IBOutlet weak var stayLabel: NSTextField! @IBOutlet weak var forgetButton: NSButton! @IBOutlet weak var signUpBox: NSBox! @IBOutlet weak var signUpButton: NSButton! @IBOutlet weak var privacyCheckButton: NSButton! @IBOutlet weak var privacyLabel: NSTextField! private var viewModel = KMSignUpViewModel() private var cancellables = Set() private var popOver_: NSPopover? private lazy var codePrivacyAttri_: NSAttributedString = { let tipsString = NSLocalizedString("I have read and agreed to the %@ and %@. An account will be automatically created after signing in with an unregistered email address.", tableName: "MemberCenterLocalizable", comment: "") let specialOffer = NSLocalizedString("Terms of Service", tableName: "MemberCenterLocalizable", comment: "") let contactsUs = NSLocalizedString("Privacy Policy", tableName: "MemberCenterLocalizable", comment: "") let fullString = String(format: tipsString, specialOffer, contactsUs) let attributedString = NSMutableAttributedString(string: fullString) // 定义链接的范围 let specialOfferRange = (fullString as NSString).range(of: specialOffer) let contactsUsRange = (fullString as NSString).range(of: contactsUs) let linkColor = NSColor(named: "4982E6") ?? NSColor.blue let font = NSFont.SFProTextRegularFont(11.0) // 与普通文本相同的字体 attributedString.addAttributes([ .foregroundColor: NSColor(named: "0E1114") ?? NSColor.black as Any, .font: font ], range: (fullString as NSString).range(of: fullString)) attributedString.addAttributes([ .foregroundColor: linkColor, .link: NSLocalizedString("https://www.pdfreaderpro.com/terms_of_service", comment: ""), .font: font ], range: specialOfferRange) attributedString.addAttributes([ .foregroundColor: linkColor, .link: NSLocalizedString("https://www.pdfreaderpro.com/privacy-policy", comment: ""), .font: font ], range: contactsUsRange) return attributedString }() private lazy var privacyAttri_: NSAttributedString = { let tipsString = NSLocalizedString("I have read and agreed to the %@ and %@.", tableName: "MemberCenterLocalizable", comment: "") let specialOffer = NSLocalizedString("Terms of Service", tableName: "MemberCenterLocalizable", comment: "") let contactsUs = NSLocalizedString("Privacy Policy", tableName: "MemberCenterLocalizable", comment: "") let fullString = String(format: tipsString, specialOffer, contactsUs) let attributedString = NSMutableAttributedString(string: fullString) // 定义链接的范围 let specialOfferRange = (fullString as NSString).range(of: specialOffer) let contactsUsRange = (fullString as NSString).range(of: contactsUs) let linkColor = NSColor(named: "4982E6") ?? NSColor.blue let font = NSFont.SFProTextRegularFont(11.0) // 与普通文本相同的字体 attributedString.addAttributes([ .foregroundColor: NSColor(named: "0E1114") ?? NSColor.black as Any, .font: font ], range: (fullString as NSString).range(of: fullString)) attributedString.addAttributes([ .foregroundColor: linkColor, .link: NSLocalizedString("https://www.pdfreaderpro.com/terms_of_service", comment: ""), .font: font ], range: specialOfferRange) attributedString.addAttributes([ .foregroundColor: linkColor, .link: NSLocalizedString("https://www.pdfreaderpro.com/privacy-policy", comment: ""), .font: font ], range: contactsUsRange) return attributedString }() convenience init(model: KMSignUpViewModel, superView: NSView) { self.init(frame: superView.bounds) viewModel = model viewModel.screenType = .signUp bindViewModel() languageLocalized() initializeUI() if model.sendCode { self.viewModel.countDown(type: .login, callback: nil) model.sendCode = false } } public override init(frame frameRect: NSRect) { super.init(frame: frameRect) } public required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func resetTextFileData() { if(self.superview != nil) { emailTextField.stringValue = "" passwordTextField.stringValue = "" passwordTextField1.stringValue = "" verifficationTextField.stringValue = "" emailErrorLabel.isHidden = true passwordErrorLabel.isHidden = true sendLabel.stringValue = NSLocalizedString("Send", tableName: "MemberCenterLocalizable", comment: "") viewModel.sendContent = NSLocalizedString("Send", tableName: "MemberCenterLocalizable", comment: "") viewModel.email = "" sendBoxRefresh() } } override func updateUI() { super.updateUI() NotificationCenter.default.addObserver(self, selector: #selector(loginSuccessNotification), name: NSNotification.Name(rawValue: "MemberCenterLoginSuccess"), object: nil) DispatchQueue.main.async { [weak self] in self?.bindViewModel() self?.languageLocalized() self?.initializeUI() } } // MARK: Private Method private func languageLocalized() -> Void { signUpLabel.stringValue = NSLocalizedString("Sign in(titile)", tableName: "MemberCenterLocalizable", comment: "") verificationCodeButton.title = NSLocalizedString("Verification Code", tableName: "MemberCenterLocalizable", comment: "") passwordButton.title = NSLocalizedString("Password", tableName: "MemberCenterLocalizable", comment: "") stayLabel.stringValue = NSLocalizedString("Stay signed in", tableName: "MemberCenterLocalizable", comment: "") forgetButton.title = NSLocalizedString("Forgot password?", tableName: "MemberCenterLocalizable", comment: "") signUpButton.title = NSLocalizedString("Sign in(button)", tableName: "MemberCenterLocalizable", comment: "") emailErrorLabel.stringValue = String(format: "*%@", NSLocalizedString("Email format error. Please enter the correct email.", tableName: "MemberCenterLocalizable", comment: "")) passwordErrorLabel.stringValue = String(format: "*%@", NSLocalizedString("Verification code error.", tableName: "MemberCenterLocalizable", comment: "")) emailTextField.placeholderString = NSLocalizedString("Please enter email address", tableName: "MemberCenterLocalizable", comment: "") verifficationTextField.placeholderString = NSLocalizedString("Please enter code", tableName: "MemberCenterLocalizable", comment: "") passwordTextField.placeholderString = NSLocalizedString("Please enter password", tableName: "MemberCenterLocalizable", comment: "") passwordTextField1.placeholderString = NSLocalizedString("Please enter password", tableName: "MemberCenterLocalizable", comment: "") privacyCheckButton.toolTip = NSLocalizedString("Please agree and check the agreement first.", tableName: "MemberCenterLocalizable", comment: "") emailTextField.stringValue = viewModel.email verifficationTextField.stringValue = viewModel.verificationCode passwordTextField.stringValue = viewModel.password passwordTextField1.stringValue = viewModel.password } private func initializeUI() -> Void { emailBox.fillColor = NSColor(named: "texefiedfillcolor") ?? NSColor.white passwordBox.fillColor = NSColor(named: "texefiedfillcolor") ?? NSColor.white emailTextField.delegate = self verifficationTextField.delegate = self passwordTextField.delegate = self passwordTextField1.delegate = self emailErrorLabel.isHidden = !viewModel.emailError() passwordErrorLabel.isHidden = !viewModel.passwordError() signUpLabel.textColor = NSColor(named: "000000") signUpLabel.font = NSFont.SFMediumFontWithSize(20) selectBox1.fillColor = NSColor(named: "4982E6") ?? NSColor.blue emailErrorLabel.textColor = NSColor(named: "FA1E5D") emailErrorLabel.font = NSFont.SFProTextRegularFont(9) passwordBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray verifficationBox.borderColor = NSColor(named: "FA1E5D") ?? NSColor.gray if viewModel.isValidEmail() { sendBox.fillColor = NSColor(named: "273C62") ?? NSColor.blue } else { sendBox.fillColor = NSColor(named: "273C62_0.4") ?? NSColor.blue } sendBox.borderColor = NSColor(named: "273C62") ?? NSColor.blue sendLabel.textColor = NSColor(named: "FFFFFF") ?? NSColor.white sendLabel.font = NSFont.SFProTextRegularFont(13) passwordErrorLabel.textColor = NSColor(named: "FA1E5D") passwordErrorLabel.font = NSFont.SFProTextRegularFont(9) stayCheckButton.image = NSImage(named: "CheckBoxNor") stayLabel.textColor = NSColor(named: "0E1114") ?? NSColor.black stayLabel.font = NSFont.SFProTextRegularFont(12) forgetButton.setTitleColor(color: NSColor(named: "4982E6") ?? NSColor.blue, font: NSFont.SFProTextRegularFont(12)) signUpBox.fillColor = NSColor(named: "273C62") ?? NSColor.blue signUpButton.setTitleColor(color: NSColor(named: "FFFFFF") ?? NSColor.white, font: NSFont.SFProTextRegularFont(16)) privacyCheckButton.image = NSImage(named: "CheckBoxNor") privacyLabel.isEditable = false privacyLabel.isSelectable = true privacyLabel.allowsEditingTextAttributes = true privacyLabel.textColor = NSColor.black privacyLabel.font = NSFont.SFProTextRegularFont(16.0) privacyLabel.attributedStringValue = codePrivacyAttri_ signUpStateChange() visibleStateChange() textfieldInputState(isEmail: true) textfieldInputState(isEmail: false) sendBoxRefresh() sendBox.moveCallback = { [weak self](mouseEntered: Bool, mouseBox: KMBox) -> Void in guard let self = self else { return } if self.viewModel.email.count <= 0 { return } if self.viewModel.sendBoxSelect { return } if !self.viewModel.isValidEmail() { return } if mouseEntered { self.sendBox.fillColor = NSColor(named: "000000_0.1") ?? NSColor.blue } else { self.sendBox.fillColor = NSColor(named: "273C62") ?? NSColor.blue } } sendBox.downCallback = { [weak self](downEntered: Bool, mouseBox: KMBox, event) -> Void in guard let self = self else { return } if self.viewModel.email.count <= 0 { return } if self.viewModel.sendBoxSelect { return } if downEntered { self.sendBox.fillColor = NSColor(named: "273C62_0.4") ?? NSColor.blue self.viewModel.countDown(type: .login, callback: nil) } } } private func signUpStateChange() -> Void { if viewModel.signUpState == .verificationCode { selectBox1.isHidden = false selectBox2.isHidden = true verificationCodeButton.setTitleColor(color: NSColor(named: "4982E6") ?? NSColor.systemBlue, font: NSFont.SFProTextRegularFont(14)) passwordButton.setTitleColor(color: NSColor(named: "42464D") ?? NSColor.black, font: NSFont.SFProTextRegularFont(14)) verifficationView.isHidden = false passwordView.isHidden = true verifficationTextField.placeholderString = NSLocalizedString("Please enter code", tableName: "MemberCenterLocalizable", comment: "") forgetButton.isHidden = true privacyLabel.attributedStringValue = codePrivacyAttri_ } else if viewModel.signUpState == .password { selectBox1.isHidden = true selectBox2.isHidden = false verificationCodeButton.setTitleColor(color: NSColor(named: "42464D") ?? NSColor.black, font: NSFont.SFProTextRegularFont(14)) passwordButton.setTitleColor(color: NSColor(named: "4982E6") ?? NSColor.black, font: NSFont.SFProTextRegularFont(14)) verifficationView.isHidden = true passwordView.isHidden = false passwordTextField.placeholderString = NSLocalizedString("Please enter password", tableName: "MemberCenterLocalizable", comment: "") passwordTextField1.placeholderString = NSLocalizedString("Please enter password", tableName: "MemberCenterLocalizable", comment: "") forgetButton.isHidden = false privacyLabel.attributedStringValue = privacyAttri_ } } private func checkStateChange(button: NSButton!, state: Bool) -> Void { button.state = state ? .on : .off if button.state == .on { button.image = NSImage(named: "CheckBoxSel") } else { button.image = NSImage(named: "CheckBoxNor") } } private func visibleStateChange() -> Void { if viewModel.isVisible { visibleButton.image = NSImage(named: "passwordVisible") passwordTextField.isHidden = false passwordTextField1.isHidden = true passwordTextField.stringValue = viewModel.password } else { visibleButton.image = NSImage(named: "passwordUnVisible") passwordTextField.isHidden = true passwordTextField1.isHidden = false passwordTextField1.stringValue = viewModel.password } } private func textfieldInputState(isEmail: Bool) -> Void { if isEmail { if viewModel.emailError() { emailBox.borderColor = NSColor(named: "FA1E5D") ?? NSColor.red } else { emailBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray } emailErrorLabel.isHidden = !viewModel.emailError() } else { if viewModel.passwordError() { if viewModel.signUpState == .verificationCode { passwordBox.borderWidth = 0 verifficationBox.borderWidth = 1 passwordBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray verifficationBox.borderColor = NSColor(named: "FA1E5D") ?? NSColor.red } else if viewModel.signUpState == .password { passwordBox.borderWidth = 1 verifficationBox.borderWidth = 0 verifficationBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray passwordBox.borderColor = NSColor(named: "FA1E5D") ?? NSColor.red } } else { if viewModel.signUpState == .verificationCode { passwordBox.borderWidth = 1 verifficationBox.borderWidth = 0 } else if viewModel.signUpState == .password { passwordBox.borderWidth = 1 verifficationBox.borderWidth = 0 } } passwordErrorLabel.isHidden = !viewModel.passwordError() } } private func sendBoxRefresh() -> Void { sendLabel.stringValue = viewModel.sendContent if viewModel.sendContent == NSLocalizedString("Send", tableName: "MemberCenterLocalizable", comment: "") || viewModel.sendContent == NSLocalizedString("Resend", tableName: "MemberCenterLocalizable", comment: "") { if viewModel.email.count > 0 { if viewModel.isValidEmail() { sendBox.fillColor = NSColor(named: "273C62") ?? NSColor.blue } else { sendBox.fillColor = NSColor(named: "273C62_0.4") ?? NSColor.blue } } else { sendBox.fillColor = NSColor(named: "273C62_0.4") ?? NSColor.blue } sendLabel.textColor = NSColor(named: "FFFFFF") ?? NSColor.white } else { sendBox.fillColor = NSColor(named: "DADBDE") ?? NSColor.gray sendLabel.stringValue = String(format: "%@s", viewModel.sendContent) sendLabel.textColor = NSColor(named: "0E1114") ?? NSColor.black } } private func _showPop() { guard let _ = self.window else { return } if (self.popOver_ != nil) { return } let popViewController = KMToolbarItemPopViewController() self.popOver_ = NSPopover() self.popOver_?.contentViewController = popViewController self.popOver_?.animates = false self.popOver_?.behavior = .semitransient self.popOver_?.contentSize = popViewController.view.frame.size self.popOver_?.delegate = self // 0.8 var color = NSColor(hex: "#273C62").withAlphaComponent(1) if KMAppearance.isDarkMode() { color = NSColor(hex: "#4E7FDB").withAlphaComponent(1) } popViewController.view.wantsLayer = true popViewController.view.layer?.backgroundColor = color.cgColor self.popOver_?.setBackgroundColor(color) popViewController.toolbarHelpTipLabel.textColor = .white popViewController.toolbarHelpTipLabel.font = .SFProTextRegularFont(13) popViewController.contentInset = .init(top: 12, left: 8, bottom: 12, right: 8) popViewController.updateWithHelpTip(helpTip: NSLocalizedString("Please agree and check the agreement first.", tableName: "MemberCenterLocalizable", comment: "")) self.popOver_?.show(relativeTo: NSMakeRect(0, 0, 0, 12), of: privacyCheckButton, preferredEdge: .maxY) } // MARK: Bind Method func bindViewModel() -> Void { viewModel.$isVisible .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.visibleStateChange() } .store(in: &cancellables) viewModel.$stayState .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.checkStateChange(button: self?.stayCheckButton, state: newValue) } .store(in: &cancellables) viewModel.$privacyState .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.checkStateChange(button: self?.privacyCheckButton, state: newValue) self?.popOver_?.close() } .store(in: &cancellables) viewModel.$signUpState .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.signUpStateChange() } .store(in: &cancellables) viewModel.$emailErrorMessage .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.emailErrorLabel.stringValue = newValue self?.emailErrorLabel.isHidden = false self?.textfieldInputState(isEmail: true) } .store(in: &cancellables) viewModel.$passwordErrorMessage .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.passwordErrorLabel.stringValue = newValue if self?.viewModel.passwordErrorMessage == "" { self?.passwordErrorLabel.isHidden = true } else { self?.passwordErrorLabel.isHidden = false } } .store(in: &cancellables) viewModel.$sendContent .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.sendBoxRefresh() } .store(in: &cancellables) viewModel.$email .receive(on: RunLoop.main) .sink { [weak self] newValue in self?.sendBoxRefresh() } .store(in: &cancellables) viewModel.$verificationCode .receive(on: RunLoop.main) .sink { [weak self] newValue in if newValue.count <= 6 && newValue.count >= 0 { self?.viewModel.passwordErrorMessage = "" } else { self?.viewModel.passwordErrorMessage = NSLocalizedString("Verification code error.", tableName: "MemberCenterLocalizable", comment: "") } } .store(in: &cancellables) viewModel.$password .receive(on: RunLoop.main) .sink { [weak self] newValue in if newValue.count <= 30 && newValue.count >= 0 { self?.viewModel.passwordErrorMessage = "" } else { self?.viewModel.passwordErrorMessage = NSLocalizedString("Password cannot exceed 30 characters.", tableName: "MemberCenterLocalizable", comment: "") } } .store(in: &cancellables) viewModel.$privacyPopShow .receive(on: RunLoop.main) .sink { [weak self] newValue in if newValue { KMMainThreadExecute { self?._showPop() } } } .store(in: &cancellables) } // MARK: Action Method @IBAction func verificationCodeAction(_ sender: NSButton) { viewModel.signUpStateChange(state: .verificationCode) } @IBAction func passwordAction(_ sender: NSButton) { viewModel.signUpStateChange(state: .password) } @IBAction func visibleAction(_ sender: NSButton) { viewModel.isVisible.toggle() } @IBAction func stayCheckAction(_ sender: NSButton) { viewModel.stayState.toggle() viewModel.stayStateAction() } @IBAction func signUpAction(_ sender: NSButton) { self.window?.makeFirstResponder(nil) viewModel.emailErrorMessage = "" viewModel.passwordErrorMessage = "" window?.showWaitingView() viewModel.signUpAction { [weak self] result, _ in self?.window?.hideWaitingView() } } @IBAction func forgetAction(_ sender: NSButton) { guard let parentView = self.superview else { return } if parentView is NSBox { let model = KMSignUpViewModel() model.email = viewModel.email let forgotView = KMForgotPasswordView(model: model, superView: parentView) NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 self.animator().alphaValue = 0 } completionHandler: { self.removeFromSuperview() forgotView.alphaValue = 0 (parentView as! NSBox).contentView = forgotView NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 forgotView.animator().alphaValue = 1 }, completionHandler: nil) } } else { let model = KMSignUpViewModel() model.email = viewModel.email let forgotView = KMForgotPasswordView(model: model, superView: parentView) NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 self.animator().alphaValue = 0 } completionHandler: { self.removeFromSuperview() forgotView.alphaValue = 0 parentView.addSubview(forgotView) NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 forgotView.animator().alphaValue = 1 }, completionHandler: nil) } } } @IBAction func privacyCheckAction(_ sender: NSButton) { viewModel.privacyState.toggle() } } extension KMSignUpView: NSTextFieldDelegate { func controlTextDidEndEditing(_ obj: Notification) { let textField = obj.object as? NSTextField if textField == emailTextField { viewModel.email = textField!.stringValue } else if textField == verifficationTextField { viewModel.verificationCode = textField!.stringValue } else if textField == passwordTextField { viewModel.password = textField!.stringValue } else if textField == passwordTextField1 { viewModel.password = textField!.stringValue } } func controlTextDidChange(_ obj: Notification) { let textField = obj.object as? NSTextField if textField == emailTextField { viewModel.emailErrorMessage = "" viewModel.email = textField!.stringValue emailBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray } else if textField == verifficationTextField { viewModel.passwordErrorMessage = "" viewModel.verificationCode = textField!.stringValue verifficationBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray } else if textField == passwordTextField { viewModel.passwordErrorMessage = "" viewModel.password = textField!.stringValue passwordBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray } else if textField == passwordTextField1 { viewModel.passwordErrorMessage = "" viewModel.password = textField!.stringValue passwordBox.borderColor = NSColor(named: "DADBDE") ?? NSColor.gray } } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(NSResponder.insertTab) { if control.isEqual(to: emailTextField) { if viewModel.signUpState == .password { // window?.selectNextKeyView(passwordTextField) if viewModel.isVisible { window?.makeFirstResponder(passwordTextField) } else { window?.makeFirstResponder(passwordTextField1) } return true } else if viewModel.signUpState == .verificationCode { // window?.selectNextKeyView(verifficationTextField) window?.makeFirstResponder(verifficationTextField) return true } } } return false } @objc func loginSuccessNotification() -> Void { resetTextFileData() } } extension KMSignUpView: NSPopoverDelegate { func popoverDidClose(_ notification: Notification) { if let data = self.popOver_?.isEqual(to: notification.object), data { self.popOver_ = nil } } }