// // KMNewUserGuideWindowController.swift // PDF Reader Pro // // Created by User-Tangchao on 2024/12/18. // import Cocoa @objc enum KMNewUserGuideType: Int { case none = 0 case ai case note case editPDF case pageEdit case convert case ocr case fillForms } // 新手引导 Layout 模块 @objcMembers class KMNewUserGuideWindowController: KMBaseWindowController { @IBOutlet weak var contentBox: NSBox! @IBOutlet weak var backgroundIv: NSImageView! @IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var scrollView: NSScrollView! @IBOutlet weak var tableView: NSTableView! @IBOutlet weak var rightBox: NSBox! static let shared = KMNewUserGuideWindowController(windowNibName: "KMNewUserGuideWindowController") private lazy var displayView_ = KMNewUserGuideDisplayView() private var datas_: [KMNewUserGuideModel] = [] var showType: KMNewUserGuideType = .ai { didSet { selectType(showType) } } // 28+8 private var defaultViewH_: CGFloat = 36 private let hasShowKey_ = "newUserGuidehasShow" override func windowDidLoad() { super.windowDidLoad() window?.title = "" window?.standardWindowButton(.zoomButton)?.isHidden = true window?.standardWindowButton(.miniaturizeButton)?.isHidden = true let closeButton = window?.standardWindowButton(.closeButton) closeButton?.target = self closeButton?.action = #selector(closeAction) backgroundIv.image = NSImage(named: "KMImageNameNewUserGuideBg") backgroundIv.image?.size = window?.frame.size ?? .zero titleLabel.font = .SFProTextSemiboldFont(20) titleLabel.stringValue = NSLocalizedString("The Beginner's Guide", comment: "") tableView.delegate = self tableView.dataSource = self tableView.enclosingScrollView?.borderType = .noBorder tableView.backgroundColor = .clear tableView.enclosingScrollView?.drawsBackground = false rightBox.borderWidth = 0 rightBox.contentView = displayView_ displayView_.contentInset = .init(top: 0, left: 10, bottom: 34, right: 18) interfaceThemeDidChanged(self.window?.appearance?.name ?? .vibrantLight) _initData() tableView.reloadData() } // MARK: - Private Methods private func _initData() { datas_.removeAll() let types: [KMNewUserGuideType] = [.ai, .editPDF, .note, .pageEdit, .convert, .ocr, .fillForms] for type in types { datas_.append(_initModel(type: type)) } } private func _initModel(type: KMNewUserGuideType) -> KMNewUserGuideModel { let model = KMNewUserGuideModel() model.type = type let maxWidth: CGFloat = 224 var listDespColor = NSColor(hex: "#42464D") if KMAppearance.isDarkMode() { listDespColor = .white } if type == .ai { #if VERSION_FREE model.name = NSLocalizedString("AI Credits Giveaway", comment: "") let attri = NSAttributedString(string: NSLocalizedString("Free 40 AI Credits for 7-day VIP Free Trial Users", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) #else model.name = NSLocalizedString("Edit with AI Robot", comment: "") let attri = NSAttributedString(string: NSLocalizedString("AI Translate, Summarize, Proofread, Rewrite", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) #endif model.imageName = "newUserGuideAITools" model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } else if type == .note { model.name = NSLocalizedString("Annotate PDF", comment: "") model.imageName = "newUserGuideAnnotation" let attri = NSAttributedString(string: NSLocalizedString("Add Annotations, Images, and Stamps to PDFs", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } else if type == .editPDF { model.name = NSLocalizedString("Edit PDF", comment: "") model.imageName = "newUserGuideEdit" let attri = NSAttributedString(string: NSLocalizedString("Edit PDF Text, Images, and Watermarks", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } else if type == .pageEdit { model.name = NSLocalizedString("Page Edit", comment: "") model.imageName = "KMImageNameNewUserGuidePageEdit" let attri = NSAttributedString(string: NSLocalizedString("Insert, Delete, Rotate, and Extract the Pages in PDFs", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } else if type == .convert { model.name = NSLocalizedString("PDF Converter", comment: "") model.imageName = "KMImageNameNewUserGuideConvert" let attri = NSAttributedString(string: NSLocalizedString("Convert to/from MS Office, Images, and More", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } else if type == .ocr { model.name = NSLocalizedString("OCR", comment: "") model.imageName = "KMImageNameNewUserGuideOCR" let attri = NSAttributedString(string: NSLocalizedString("Scanned PDFs to Editable & Searchable PDFs in 90+ Languages", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } else if type == .fillForms { model.name = NSLocalizedString("Create & Fill Forms", comment: "") model.imageName = "KMImageNameNewUserGuideFillForms" let attri = NSAttributedString(string: NSLocalizedString("Support to Create & Fill Interactive PDF Forms", comment: ""), attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = _calculateViewHeight(model: model, maxWidth: maxWidth) } return model } private func _calculateViewHeight(model: KMNewUserGuideModel, maxWidth: CGFloat) -> CGFloat { guard let attri = model.despAttri else { return defaultViewH_ } let size = attri.boundingRect(with: .init(width: maxWidth, height: CGFLOAT_MAX), options: .usesLineFragmentOrigin).size return (4+20+8+size.height+4)+4+4 } private func _trackEvent(type: KMNewUserGuideType) { if type == .ai { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_AI"], platform: .AppCenter) } else if type == .note { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_AnnotatePDF"], platform: .AppCenter) } else if type == .editPDF { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_EditPDF"], platform: .AppCenter) } else if type == .pageEdit { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_PageEditor"], platform: .AppCenter) } else if type == .convert { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_Convert"], platform: .AppCenter) } else if type == .ocr { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_OCR"], platform: .AppCenter) } else if type == .fillForms { trackEvent(eventName: "PUW", params: ["PUW_Btn" : "FunctionIntro_Form"], platform: .AppCenter) } } private func _fetchBrowserWindowC() -> KMBrowserWindowController? { if let win = NSApp.mainWindow as? CTBrowserWindow { return win.windowController as? KMBrowserWindowController } if let win = NSApp.keyWindow as? CTBrowserWindow { return win.windowController as? KMBrowserWindowController } for win in NSApp.windows { if win.isVisible == false { continue } guard let winC = win.windowController as? KMBrowserWindowController else { continue } if winC.browser == nil { continue } return winC } return nil } private func _showPopGuide() { if KMMemberInfo.shared.isLogin == false { // 未登陆 return } if KMMemberInfo.shared.isMemberAllFunction { // 有权益 return } guard let winC = _fetchBrowserWindowC() else { // 没找到有效的窗口 return } guard let itemV = winC.rightMessageVC?.discountItemView, itemV.isHidden == false else { return } guard let doc = winC.browser.activeTabContents() as? KMMainDocument else { return } if doc.isHome || doc.isNewTab { doc.homeViewController?.loadOpenFileFunctionGuide(.messageDiscount) return } doc.mainViewController?.loadOpenFileFunctionGuide(.messageDiscount) } override func interfaceThemeDidChanged(_ appearance: NSAppearance.Name) { super.interfaceThemeDidChanged(appearance) KMMainThreadExecute { var listDespColor = NSColor(hex: "#42464D") if KMAppearance.isDarkMode() { self.titleLabel.textColor = .white listDespColor = .white } else { self.titleLabel.textColor = NSColor(hex: "#002143") } let maxWidth: CGFloat = 224 for model in self.datas_ { if let data = model.despAttri?.string { let attri = NSAttributedString(string: data, attributes: [ .font : NSFont.SFProTextRegularFont(11), .foregroundColor : listDespColor ]) model.despAttri = attri model.viewH = self._calculateViewHeight(model: model, maxWidth: maxWidth) } } self.tableView.reloadData() } } // MARK: - Actions @objc func closeAction() { _showPopGuide() closeWindow() } // MARK: - public Methods public func openWindow() { self.showWindow(nil) self._showCenter(animate: true) self.window?.makeKeyAndOrderFront(nil) selectType(showType) saveShowRecord() trackEvent(eventName: "PUW", params: ["PUW_Exposure" : "PUW_FunctionIntro"], platform: .AppCenter) } public func selectType(_ type: KMNewUserGuideType) { var selectedModel: KMNewUserGuideModel? for model in datas_ { model.isSelected = (model.type == type) if model.isSelected { selectedModel = model } } KMMainThreadExecute { // self.tableView.reloadData() self.tableView.animator().reloadData() if let data = selectedModel?.imageName { self.displayView_.imageView.image = NSImage(named: data) self.displayView_.imageView.image?.size = NSMakeSize(480, 320) } } } public func closeWindow() { window?.close() } public func saveShowRecord() { KMDataManager.ud_set(true, forKey: hasShowKey_) } public func needShow() -> Bool { // 已经显示 if KMDataManager.ud_bool(forKey: hasShowKey_) { return false } #if VERSION_FREE if KMMemberInfo.shared.isMemberAllFunction { // 有权益(本地 或 账号) return false } #endif // 不是新用户 if KMMemberInfo.shared.isFreeAccount == false { return false } // 新用户首次显示 return true } private func _showCenter(animate: Bool){ guard let screenFrame = NSScreen.main?.frame else { return } var frame: CGRect = self.window!.frame frame.origin.y = (screenFrame.size.height-frame.size.height)*0.5 frame.origin.x = (screenFrame.size.width-frame.size.width)*0.5 self.window?.setFrame(frame, display: true, animate: animate) } } // MARK: - NSTableViewDelegate, NSTableViewDataSource extension KMNewUserGuideWindowController: NSTableViewDelegate, NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return datas_.count } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { let model = datas_.safe_element(for: row) as? KMNewUserGuideModel let isSelected = model?.isSelected ?? false if isSelected == false { return defaultViewH_ } return model?.viewH ?? defaultViewH_ } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { let rowView = KMTableRowView() rowView.selectionBackgroundColorBlock = { return .clear } return rowView } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { var cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CellId"), owner: self) as? KMNewUserGuideCellView if cell == nil { cell = KMNewUserGuideCellView() } cell?.contentInset = .init(top: 4, left: 0, bottom: 4, right: 0) guard let model = datas_.safe_element(for: row) as? KMNewUserGuideModel else { return cell } cell?.titleLabel.font = .SFProTextBoldFont(14) cell?.titleLabel.stringValue = model.name if let data = model.despAttri { cell?.subTitleLabel.attributedStringValue = data } let isSelected = model.isSelected cell?.subTitleLabel.isHidden = !isSelected cell?.leftLine.isHidden = !isSelected cell?.backgroundView.layer?.cornerRadius = 4 if KMAppearance.isDarkMode() { cell?.titleLabel.textColor = .white cell?.leftLine.layer?.backgroundColor = NSColor(hex: "#227AFF").cgColor if isSelected { cell?.backgroundView.layer?.backgroundColor = NSColor(hex: "#869297").withAlphaComponent(0.4).cgColor } else { cell?.backgroundView.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.8).cgColor } } else { cell?.titleLabel.textColor = NSColor(hex: "#42464D") cell?.leftLine.layer?.backgroundColor = NSColor(hex: "#4982E6").cgColor if isSelected { cell?.backgroundView.layer?.backgroundColor = NSColor(hex: "#DCF4FF").withAlphaComponent(0.4).cgColor } else { cell?.backgroundView.layer?.backgroundColor = .white } } return cell } func tableViewSelectionDidChange(_ notification: Notification) { if tableView.isEqual(to: notification.object) { let row = tableView.selectedRow guard let model = datas_.safe_element(for: row) as? KMNewUserGuideModel else { return } selectType(model.type) _trackEvent(type: model.type) } } }