// // KMUserFeekbackWindowController.swift // PDF Reader Pro Edition // // Created by tangchao on 2024/7/11. // import Cocoa @objcMembers class KMUserFeekbackWindowController: NSWindowController { static let shared = KMUserFeekbackWindowController(windowNibName: "KMUserFeekbackWindowController") @IBOutlet weak var emailBox: NSBox! @IBOutlet weak var typeBox: NSBox! @IBOutlet weak var despBox: NSBox! @IBOutlet weak var listHeaderBox: NSBox! @IBOutlet weak var listBox: NSBox! @IBOutlet weak var comfirmButton: NSButton! @IBOutlet weak var cancelButton: NSButton! private var emailItemView_: KMUserFbEmailItemView? private var typeItemView_: KMUserFbTypeItemView? private var despItemView_: KMUserFbDespItemView? private var listHeaderItemView_: KMUserFbListHeaderItemView? private var listItemView_: KMUserListItemView? typealias KMRequestServerComplete = (_ wrapper: ResultWrapper) -> Void private var filePaths_: [String] = [] private var logPath_: String? private var popover_: NSPopover? var typeString: String = "" @IBOutlet weak var listHeaderItemHConst: NSLayoutConstraint! private var fileFormats_: [String] = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "psd", "svg", "pdf", "mp4", "mov", "avi", "mkv", "wmv", "flv", "mpg", "3gp", "doc", "docx", "xls" ,"xlsx", "ppt", "pptx", "txt", "rtf", "nfo"] private var fileFormatsString = "jpg, jpeg, png, gif, bmp, tiff, tif, psd, svg, pdf, mp4, mov, avi, mkv, wmv, flv, mpg, 3gp, doc, docx, xls,xlsx, ppt, pptx, txt, rtf, nfo" deinit { Swift.debugPrint("KMUserFeekbackWindowController deinit.") DistributedNotificationCenter.default().removeObserver(self) } override func windowDidLoad() { super.windowDidLoad() self._initDefaultValue() self._initSubViews() self.typeItemView_?.comBoBox.selectItem(at: 0) self.updateViewColor() DistributedNotificationCenter.default().addObserver(self, selector: #selector(themeChanged), name: NSApplication.interfaceThemeChangedNotification, object: nil) } private func _initDefaultValue() { if let btn = self.window?.standardWindowButton(.miniaturizeButton) { btn.isHidden = true } if let btn = self.window?.standardWindowButton(.zoomButton) { btn.isHidden = true } self.window?.title = NSLocalizedString("Feedback for PDF Reader Pro", comment: "") self.emailBox.borderWidth = 0 self.typeBox.borderWidth = 0 self.despBox.borderWidth = 0 self.listHeaderBox.borderWidth = 0 self.listBox.borderWidth = 0 #if !VERSION_DMG self.listHeaderItemHConst.constant = 44 #endif let email = VerificationManager.default()?.email ?? "" self.emailItemView_?.textfiled.stringValue = email self.emailItemView_?.textfiled.nextResponder = self.despItemView_?.textView self.cancelButton.title = NSLocalizedString("Cancel", comment: "") self.comfirmButton.title = NSLocalizedString("Submit", comment: "") self.cancelButton.target = self self.cancelButton.action = #selector(_cancelButtonAction) self.comfirmButton.target = self self.comfirmButton.action = #selector(_comfirmButtonAction) } private func _initSubViews() { self.emailItemView_ = KMUserFbEmailItemView.createFromNib() self.emailBox.contentView = self.emailItemView_ self.typeItemView_ = KMUserFbTypeItemView.createFromNib() self.typeBox.contentView = self.typeItemView_ self.despItemView_ = KMUserFbDespItemView.createFromNib() self.despBox.contentView = self.despItemView_ self.listHeaderItemView_ = KMUserFbListHeaderItemView.createFromNib() self.listHeaderBox.contentView = self.listHeaderItemView_ self.listItemView_ = KMUserListItemView.createFromNib() self.listBox.contentView = self.listItemView_ self.listItemView_?.hiddenTip() self.listHeaderItemView_?.helpHoverCallback = { [weak self] action in if action.rawValue == 0 { // enter self?._showHelpPopover((self?.listHeaderItemView_?.helpButton)!) } else if action.rawValue == 2 { // exit self?._hiddenHelpPopover() } } self.listHeaderItemView_?.itemClick = { [weak self] idx in if idx == 1 { // 获取日志文件 } else if idx == 3 { // 新增文件 let panel = NSOpenPanel() // panel.message = self?.fileFormatsString panel.allowedFileTypes = self?.fileFormats_ panel.allowsMultipleSelection = true panel.beginSheetModal(for: self!.window!) { resp in if resp == .cancel { return } for url in panel.urls { self?.filePaths_.append(url.path) } self?._updateListData() } } } self.listItemView_?.itemClick = { [weak self] idx, param in if let cnt = self?.filePaths_.count, idx < cnt { self?.filePaths_.remove(at: idx) self?._updateListData() } } self.listItemView_?.validateDropCallback = { [weak self] info, row, dropOperation in let datas = self?.filePaths_.count ?? 0 if datas >= 10 { return NSDragOperation(rawValue: 0) } return .generic } self.listItemView_?.acceptDropCallback = { [weak self] info, row, dropOperation in let pborad = info.draggingPasteboard guard let _ = pborad.availableType(from: [.fileURL]) else { return false } guard let items = pborad.pasteboardItems else { return false } for item in items { let string = item.propertyList(forType: .fileURL) as? String ?? "" if let path = URL(string: string)?.path { let contains = self?._fileFormatIsContains(URL(string: string)!.pathExtension) ?? false if contains { self?.filePaths_.append(path) } } } self?._updateListData() return true } } override func showWindow(_ sender: Any?) { super.showWindow(sender) self.typeItemView_?.comBoBox.stringValue = self.typeString } func updateViewColor() { if KMAppearance.isDarkMode() { self.window?.backgroundColor = NSColor(red: 40/255.0, green: 40/255.0, blue: 40/255.0, alpha: 1) self.emailItemView_?.box.fillColor = NSColor(red: 110/255.0, green: 109/255.0, blue: 112/255.0, alpha: 1) self.typeItemView_?.comBoBox.backgroundColor = NSColor(red: 110/255.0, green: 109/255.0, blue: 112/255.0, alpha: 1) self.despItemView_?.box.fillColor = NSColor(red: 110/255.0, green: 109/255.0, blue: 112/255.0, alpha: 1) self.listItemView_?.tipBox.fillColor = NSColor(red: 231/255.0, green: 56/255.0, blue: 91/255.0, alpha: 1) } else { self.window?.backgroundColor = .white self.emailItemView_?.box.fillColor = .white self.typeItemView_?.comBoBox.backgroundColor = .white self.despItemView_?.box.fillColor = .white self.listItemView_?.tipBox.fillColor = NSColor.km_init(hex: "#FEE4EC") } } // MARK: - Noti Methods @objc func themeChanged(_ notification: Notification) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.updateViewColor() } } // MARK: - Private Methods @objc private func _cancelButtonAction() { self.window?.orderOut(nil) } @objc private func _comfirmButtonAction() { #if VERSION_DMG if let data = self.listHeaderItemView_?.checkButton.state, data == .on { let fileaccess = AppSandboxFileAccess() // /Users/kdanmobile/Library/Logs/DiagnosticReports let url = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first let coms = url?.pathComponents ?? [] var filePath: String = "" for (i, com) in coms.enumerated() { if i > 2 { break } else { filePath.append("/\(com)") } } let cmd = filePath.appending("/Library/Logs/DiagnosticReports/") // /Library/Logs/DiagnosticReports/ // fileaccess?.accessFilePath(cmd, persistPermission: true, with: { // // }) let logString = self._getLog() if logString.isEmpty == false { let bundleIdentifier = Bundle.main.bundleIdentifier ?? "PDF Reader Pro" if let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.appendingPathComponent(bundleIdentifier) { if FileManager.default.fileExists(atPath: url.path) == false { try?FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) } let fileUrl = url.appendingPathComponent("Log.txt") try?logString.write(to: fileUrl, atomically: true, encoding: .utf8) self.logPath_ = fileUrl.path } } } else { self.logPath_ = nil } #endif self.window?.makeFirstResponder(nil) let state = self._isConnectionAvailable() if !state { self._showHud(msg: NSLocalizedString("Please make sure your internet connection is available.", comment: "")) return } guard let emailString = self.emailItemView_?.string(), emailString.isEmpty == false else { // 邮箱为空 self.emailItemView_?.showTip(label: NSLocalizedString("Please enter your email", comment: "")) return } if KMValidateEmail(email: emailString) == false { self.emailItemView_?.showTip(label: NSLocalizedString("Please enter valid email", comment: "")) return } guard let despString = self.despItemView_?.string(), despString.isEmpty == false else { // 描述信息为空 self.despItemView_?.showTip() return } self.window?.contentView?.beginLoading() self.comfirmButton.isEnabled = false var filePaths: [String] = self.filePaths_ #if VERSION_DMG if let data = self.listHeaderItemView_?.checkButton.state, data == .on { if let filePath = self.logPath_, filePath.isEmpty == false { filePaths.append(filePath) } } #endif self._feekbackAction(filePaths: filePaths) { [weak self] wrapper in self?.window?.contentView?.endLoading() self?.comfirmButton.isEnabled = true if wrapper.success { self?._showHud(msg: NSLocalizedString("Thank you for your feedback! Customer service will feedback to your email within 1 working day", comment: "")) DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { self?.despItemView_?.textView.string = "" self?.despItemView_?.textView.placeholderLabel.isHidden = false; self?.filePaths_.removeAll() self?._updateListData() if let data = self?.logPath_, data.isEmpty == false { if FileManager.default.fileExists(atPath: data) { try?FileManager.default.removeItem(atPath: data) } } self?.logPath_ = nil self?.window?.orderOut(nil) } } else { self?._showHud(msg: wrapper.content) } } } private func _showHelpPopover(_ sender: NSButton) { if self.popover_ != nil { return } let vc = KMUserFbHelpPopController() vc.formatSting = self.fileFormatsString let popover = NSPopover() popover.contentViewController = vc popover.animates = true popover.behavior = .semitransient popover.delegate = self popover.setValue(true, forKey: "shouldHideAnchor") self.popover_ = popover vc.isDrak = KMAppearance.isDarkMode() popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxX) } private func _hiddenHelpPopover() { self.popover_?.close() } private func _updateListData() { self.listItemView_?.hiddenTip() var datas: [KMUserFbListModel] = [] let maxSize: Double = 20 * 1024 * 1024 var fileSize: Double = 0 var filePaths: [String] = [] var showFileSizeLimit = false var showFileCountLimit = false for (i, fileP) in self.filePaths_.enumerated() { let model = KMUserFbListModel() model.filePath = fileP let url = URL(fileURLWithPath: fileP) model.fileName = url.lastPathComponent let attri = try?FileManager.default.attributesOfItem(atPath: fileP) model.fileSize = attri?[FileAttributeKey.size] as? Double ?? 0 // fileSize += model.fileSize fileSize = model.fileSize if fileSize >= maxSize { showFileSizeLimit = true continue } if i >= 10 { showFileCountLimit = true break } model.fileSizeString = self.fileSizeString(model.fileSize) ?? "" datas.append(model) filePaths.append(fileP) } self.filePaths_ = filePaths if showFileCountLimit { self.listItemView_?.showTip(tip: NSLocalizedString("Add failed: upload up to 10 files", comment: "")) } else if showFileSizeLimit { self.listItemView_?.showTip(tip: NSLocalizedString("Add failed: attachment size cannot exceed 20M", comment: "")) } if datas.count >= 10 { self.listHeaderItemView_?.addButton.isEnabled = false } else { self.listHeaderItemView_?.addButton.isEnabled = true } self.listItemView_?.datas = datas } private func _showHud(msg: String) { if let data = self.window?.contentView { // _ = CustomAlertView(message: msg, from: data, with: .black) CustomAlertView.alertView(message: msg, fromView: data, withStyle: .black) } } #if VERSION_DMG private func _getLog() -> String { let url = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first let coms = url?.pathComponents ?? [] var filePath: String = "" for (i, com) in coms.enumerated() { if i > 2 { break } else { filePath.append("/\(com)") } } // ls /Library/Logs/DiagnosticReports/PDF* let cmd = "ls \(filePath)/Library/Logs/DiagnosticReports/PDF*" let filestrings = _runShellCommand(cmd) let files = filestrings.components(separatedBy: "\n") var results: [String] = [] for string in files { if string.contains("PDF Reader Pro") { var tempS = string // tempS.replace(" ", with: "\\ ") // tempS.replacingCharacters(in: " ", with: "\\ ") // let ss = tempS.replacing(" ", with: "\\ ") let ss = tempS.replacingOccurrences(of: " ", with: "\\ ") results.append(ss) } } if results.isEmpty { let cmd = "ls \(filePath)/Library/Logs/DiagnosticReports/Retired/PDF*" let filestrings = _runShellCommand(cmd) let files = filestrings.components(separatedBy: "\n") for string in files { if string.contains("PDF Reader Pro") { var tempS = string // tempS.replace(" ", with: "\\ ") // tempS.replacingCharacters(in: " ", with: "\\ ") // let ss = tempS.replacing(" ", with: "\\ ") let ss = tempS.replacingOccurrences(of: " ", with: "\\ ") results.append(ss) } } } if let file = results.last { let cmd = "cat \(file)" // cat /Library/Logs/DiagnosticReports/PDF* let logs = _runShellCommand(cmd) return logs } return "" } #endif private func _runShellCommand(_ command: String) -> String { let task = Process() let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.arguments = ["-c", command] task.launchPath = "/bin/bash" do { try task.run() } catch { return "Error running command: \(error)" } let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" return output } func test() { var convertString = "TARGET_BUNDLE_IDENTIFIER=\"com.brother.pdfreaderprofree.mac\"\n" convertString = convertString.appendingFormat("SANDBOX_ROOT=$(osascript -e 'tell application \"System Events\" to get the path of the home folder of (system info)')\n") convertString = convertString.appendingFormat("CONTAINER_LOG_PATH=$(osascript < String { let fileSize = fSize / 1024 let size = fileSize >= 1024 ? (fileSize < 1048576 ? fileSize/1024 : fileSize/1048576.0) : fileSize let unit = fileSize >= 1024 ? (fileSize < 1048576 ? "M" : "G") : "K" return String(format: "%0.1f %@", size, unit) } private func _feekbackAction(filePaths: [String], complete: @escaping KMRequestServerComplete) { let urlString = kVerificationServer + "/api/feedback/feedback" var fileDatas: [Data] = [] for filePath in filePaths { if let fileData = FileManager.default.contents(atPath: filePath) { fileDatas.append(fileData) } } let (major, minor, bugFix) = KMTools.getSystemVersion() let versionInfoString = "\(KMTools.getRawSystemInfo()) - \(major).\(minor).\(bugFix)" let appVersion = KMTools.getAppVersion() let appName = KMTools.getAppNameForSupportEmail() let typeString = (self.typeItemView_?.string() ?? "") + ";" + appName + " - " + appVersion + ";" + versionInfoString let uuid = GetHardwareUUID() let params: [String: Any] = ["email": self.emailItemView_?.string() ?? "", "title": typeString, "content": self.despItemView_?.string() ?? "", "unique_sn" : uuid!, "files[]" : fileDatas] KMAIRequestServer.requestServer.uploadFile(urlString: urlString, params: params) { formData in for i in 0 ..< fileDatas.count { let path = filePaths[i] let fileURL = URL(fileURLWithPath: path) try? formData.appendPart(withFileURL: fileURL, name: "files[]", fileName: fileURL.lastPathComponent, mimeType: "application/octet-stream") } } requestSerializer: { requestSerializer in // requestSerializer.setValue("Apifox/1.0.0 (https://www.apifox.cn)", forHTTPHeaderField: "User-Agent") } completion: { task, responseObject, error in if responseObject != nil { let data: NSDictionary = responseObject as? NSDictionary ?? [:] // var code: String = responseObject!["code"] as? String ?? "06005" let code = data["code"] as? Int ?? 06005 if code == 06005 { // let tempCode: Int = responseObject!["code"] as? Int ?? 0 // if tempCode == 501 { // code = "501" // } } let message: String = data["message"] as? String ?? "unknown error" let error = NSError(domain: message, code: code) if code == 200 { let wrapper = ResultWrapper(success: true, resultData: data) // wrapper.content = data["fileKey"] as! String complete(wrapper) } else { let wrapper = ResultWrapper(success: false, resultData: data) if code == 501 { wrapper.content = "501" } else { wrapper.content = message } complete(wrapper) } } else { complete(ResultWrapper(success: false, content: "unknown error")) } } } private func _isConnectionAvailable() -> Bool { if Reachability.forInternetConnection().currentReachabilityStatus().rawValue == 0 { return false } return true } private func _fileFormatIsContains(_ exn: String) -> Bool { let _exn = exn.lowercased() return self.fileFormats_.contains(_exn) } } extension KMUserFeekbackWindowController: NSPopoverDelegate { func popoverWillClose(_ notification: Notification) { if let data = self.popover_?.isEqual(to: notification.object), data { self.popover_ = nil } } }