KMUserFeekbackWindowController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. //
  2. // KMUserFeekbackWindowController.swift
  3. // PDF Reader Pro Edition
  4. //
  5. // Created by tangchao on 2024/7/11.
  6. //
  7. import Cocoa
  8. @objcMembers class KMUserFeekbackWindowController: NSWindowController {
  9. static let shared = KMUserFeekbackWindowController(windowNibName: "KMUserFeekbackWindowController")
  10. @IBOutlet weak var emailBox: NSBox!
  11. @IBOutlet weak var typeBox: NSBox!
  12. @IBOutlet weak var despBox: NSBox!
  13. @IBOutlet weak var listHeaderBox: NSBox!
  14. @IBOutlet weak var listBox: NSBox!
  15. @IBOutlet weak var comfirmButton: NSButton!
  16. @IBOutlet weak var cancelButton: NSButton!
  17. private var emailItemView_: KMUserFbEmailItemView?
  18. private var typeItemView_: KMUserFbTypeItemView?
  19. private var despItemView_: KMUserFbDespItemView?
  20. private var listHeaderItemView_: KMUserFbListHeaderItemView?
  21. private var listItemView_: KMUserListItemView?
  22. typealias KMRequestServerComplete = (_ wrapper: ResultWrapper) -> Void
  23. private var filePaths_: [String] = []
  24. private var logPath_: String?
  25. private var popover_: NSPopover?
  26. var typeString: String = ""
  27. deinit {
  28. Swift.debugPrint("KMUserFeekbackWindowController deinit.")
  29. }
  30. override func windowDidLoad() {
  31. super.windowDidLoad()
  32. self._initDefaultValue()
  33. self._initSubViews()
  34. self.typeItemView_?.comBoBox.selectItem(at: 0)
  35. }
  36. private func _initDefaultValue() {
  37. if let btn = self.window?.standardWindowButton(.miniaturizeButton) {
  38. btn.isHidden = true
  39. }
  40. if let btn = self.window?.standardWindowButton(.zoomButton) {
  41. btn.isHidden = true
  42. }
  43. self.window?.title = NSLocalizedString("Feedback for PDF Reader Pro", comment: "")
  44. self.window?.backgroundColor = NSColor.white
  45. self.emailBox.borderWidth = 0
  46. self.typeBox.borderWidth = 0
  47. self.despBox.borderWidth = 0
  48. self.listHeaderBox.borderWidth = 0
  49. self.listBox.borderWidth = 0
  50. let email = VerificationManager.default()?.email ?? ""
  51. self.emailItemView_?.textfiled.stringValue = email
  52. self.emailItemView_?.textfiled.nextResponder = self.despItemView_?.textView
  53. self.cancelButton.title = NSLocalizedString("Cancel", comment: "")
  54. self.comfirmButton.title = NSLocalizedString("Submit", comment: "")
  55. self.cancelButton.target = self
  56. self.cancelButton.action = #selector(_cancelButtonAction)
  57. self.comfirmButton.target = self
  58. self.comfirmButton.action = #selector(_comfirmButtonAction)
  59. }
  60. private func _initSubViews() {
  61. self.emailItemView_ = KMUserFbEmailItemView.createFromNib()
  62. self.emailBox.contentView = self.emailItemView_
  63. self.typeItemView_ = KMUserFbTypeItemView.createFromNib()
  64. self.typeBox.contentView = self.typeItemView_
  65. self.despItemView_ = KMUserFbDespItemView.createFromNib()
  66. self.despBox.contentView = self.despItemView_
  67. self.listHeaderItemView_ = KMUserFbListHeaderItemView.createFromNib()
  68. self.listHeaderBox.contentView = self.listHeaderItemView_
  69. self.listItemView_ = KMUserListItemView.createFromNib()
  70. self.listBox.contentView = self.listItemView_
  71. self.listItemView_?.hiddenTip()
  72. self.listHeaderItemView_?.helpHoverCallback = { [weak self] action in
  73. if action.rawValue == 0 { // enter
  74. self?._showHelpPopover((self?.listHeaderItemView_?.helpButton)!)
  75. } else if action.rawValue == 2 { // exit
  76. self?._hiddenHelpPopover()
  77. }
  78. }
  79. self.listHeaderItemView_?.itemClick = { [weak self] idx in
  80. if idx == 1 { // 获取日志文件
  81. } else if idx == 3 { // 新增文件
  82. let panel = NSOpenPanel()
  83. panel.allowsMultipleSelection = true
  84. panel.beginSheetModal(for: self!.window!) { resp in
  85. if resp == .cancel {
  86. return
  87. }
  88. for url in panel.urls {
  89. self?.filePaths_.append(url.path)
  90. }
  91. self?._updateListData()
  92. }
  93. }
  94. }
  95. self.listItemView_?.itemClick = { [weak self] idx, param in
  96. if let cnt = self?.filePaths_.count, idx < cnt {
  97. self?.filePaths_.remove(at: idx)
  98. self?._updateListData()
  99. }
  100. }
  101. self.listItemView_?.validateDropCallback = { [weak self] info, row, dropOperation in
  102. let datas = self?.filePaths_.count ?? 0
  103. if datas >= 10 {
  104. return NSDragOperation(rawValue: 0)
  105. }
  106. return .generic
  107. }
  108. self.listItemView_?.acceptDropCallback = { [weak self] info, row, dropOperation in
  109. let pborad = info.draggingPasteboard
  110. guard let _ = pborad.availableType(from: [.fileURL]) else {
  111. return false
  112. }
  113. guard let items = pborad.pasteboardItems else {
  114. return false
  115. }
  116. for item in items {
  117. let string = item.propertyList(forType: .fileURL) as? String ?? ""
  118. if let path = URL(string: string)?.path {
  119. self?.filePaths_.append(path)
  120. }
  121. }
  122. self?._updateListData()
  123. return true
  124. }
  125. }
  126. override func showWindow(_ sender: Any?) {
  127. super.showWindow(sender)
  128. self.typeItemView_?.comBoBox.stringValue = self.typeString
  129. }
  130. // MARK: - Private Methods
  131. @objc private func _cancelButtonAction() {
  132. self.window?.orderOut(nil)
  133. }
  134. @objc private func _comfirmButtonAction() {
  135. if let data = self.listHeaderItemView_?.checkButton.state, data == .on {
  136. let fileaccess = AppSandboxFileAccess()
  137. // /Users/kdanmobile/Library/Logs/DiagnosticReports
  138. let url = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
  139. let coms = url?.pathComponents ?? []
  140. var filePath: String = ""
  141. for (i, com) in coms.enumerated() {
  142. if i > 2 {
  143. break
  144. } else {
  145. filePath.append("/\(com)")
  146. }
  147. }
  148. let cmd = filePath.appending("/Library/Logs/DiagnosticReports/")
  149. // /Library/Logs/DiagnosticReports/
  150. fileaccess?.accessFilePath(cmd, persistPermission: true, with: {
  151. })
  152. let logString = self._getLog()
  153. if logString.isEmpty == false {
  154. let bundleIdentifier = Bundle.main.bundleIdentifier ?? "PDF Reader Pro"
  155. if let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.appendingPathComponent(bundleIdentifier) {
  156. if FileManager.default.fileExists(atPath: url.path) == false {
  157. try?FileManager.default.createDirectory(at: url, withIntermediateDirectories: false)
  158. }
  159. let fileUrl = url.appendingPathComponent("Log.txt")
  160. try?logString.write(to: fileUrl, atomically: true, encoding: .utf8)
  161. self.logPath_ = fileUrl.path
  162. }
  163. }
  164. } else {
  165. self.logPath_ = nil
  166. }
  167. self.window?.makeFirstResponder(nil)
  168. let state = self._isConnectionAvailable()
  169. if !state {
  170. self._showHud(msg: NSLocalizedString("Please make sure your internet connection is available.", comment: ""))
  171. return
  172. }
  173. guard let emailString = self.emailItemView_?.string(), emailString.isEmpty == false else {
  174. // 邮箱为空
  175. self.emailItemView_?.showTip(label: NSLocalizedString("请填写Email", comment: ""))
  176. return
  177. }
  178. if KMValidateEmail(email: emailString) == false {
  179. self.emailItemView_?.showTip(label: NSLocalizedString("Please enter valid email", comment: ""))
  180. return
  181. }
  182. guard let despString = self.despItemView_?.string(), despString.isEmpty == false else {
  183. // 描述信息为空
  184. self.despItemView_?.showTip()
  185. return
  186. }
  187. self.window?.contentView?.beginLoading()
  188. self.comfirmButton.isEnabled = false
  189. var filePaths: [String] = self.filePaths_
  190. if let data = self.listHeaderItemView_?.checkButton.state, data == .on {
  191. if let filePath = self.logPath_, filePath.isEmpty == false {
  192. filePaths.append(filePath)
  193. }
  194. }
  195. self._feekbackAction(filePaths: filePaths) { [weak self] wrapper in
  196. self?.window?.contentView?.endLoading()
  197. self?.comfirmButton.isEnabled = true
  198. if wrapper.success {
  199. self?._showHud(msg: NSLocalizedString("感谢您的反馈!\n客服将在1个工作日内反馈至您的邮箱", comment: ""))
  200. DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
  201. self?.despItemView_?.textView.string = ""
  202. self?.despItemView_?.textView.placeholderLabel.isHidden = false;
  203. self?.filePaths_.removeAll()
  204. self?._updateListData()
  205. if let data = self?.logPath_, data.isEmpty == false {
  206. if FileManager.default.fileExists(atPath: data) {
  207. try?FileManager.default.removeItem(atPath: data)
  208. }
  209. }
  210. self?.logPath_ = nil
  211. self?.window?.orderOut(nil)
  212. }
  213. } else {
  214. self?._showHud(msg: wrapper.content)
  215. }
  216. }
  217. }
  218. private func _showHelpPopover(_ sender: NSButton) {
  219. if self.popover_ != nil {
  220. return
  221. }
  222. let vc = KMUserFbHelpPopController()
  223. let popover = NSPopover()
  224. popover.contentViewController = vc
  225. popover.animates = true
  226. popover.behavior = .semitransient
  227. popover.delegate = self
  228. popover.setValue(true, forKey: "shouldHideAnchor")
  229. self.popover_ = popover
  230. popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxX)
  231. }
  232. private func _hiddenHelpPopover() {
  233. self.popover_?.close()
  234. }
  235. private func _updateListData() {
  236. self.listItemView_?.hiddenTip()
  237. var datas: [KMUserFbListModel] = []
  238. let maxSize: Float = 20 * 1024 * 1024
  239. var fileSize: Float = 0
  240. var filePaths: [String] = []
  241. for (i, fileP) in self.filePaths_.enumerated() {
  242. let model = KMUserFbListModel()
  243. model.filePath = fileP
  244. let url = URL(fileURLWithPath: fileP)
  245. model.fileName = url.lastPathComponent
  246. let attri = try?FileManager.default.attributesOfItem(atPath: fileP)
  247. model.fileSize = attri?[FileAttributeKey.size] as? Float ?? 0
  248. fileSize += model.fileSize
  249. if fileSize >= maxSize {
  250. self.listItemView_?.showTip()
  251. break
  252. }
  253. if i >= 10 {
  254. break
  255. }
  256. model.fileSizeString = self.fileSizeString(model.fileSize) ?? ""
  257. datas.append(model)
  258. filePaths.append(fileP)
  259. }
  260. self.filePaths_ = filePaths
  261. if datas.count >= 10 {
  262. self.listHeaderItemView_?.addButton.isEnabled = false
  263. } else {
  264. self.listHeaderItemView_?.addButton.isEnabled = true
  265. }
  266. self.listItemView_?.datas = datas
  267. }
  268. private func _showHud(msg: String) {
  269. if let data = self.window?.contentView {
  270. // _ = CustomAlertView(message: msg, from: data, with: .black)
  271. CustomAlertView.alertView(message: msg, fromView: data, withStyle: .black)
  272. }
  273. }
  274. private func _getLog() -> String {
  275. let url = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
  276. let coms = url?.pathComponents ?? []
  277. var filePath: String = ""
  278. for (i, com) in coms.enumerated() {
  279. if i > 2 {
  280. break
  281. } else {
  282. filePath.append("/\(com)")
  283. }
  284. }
  285. // ls /Library/Logs/DiagnosticReports/PDF*
  286. let cmd = "ls \(filePath)/Library/Logs/DiagnosticReports/PDF*"
  287. let filestrings = _runShellCommand(cmd)
  288. let files = filestrings.components(separatedBy: "\n")
  289. var results: [String] = []
  290. for string in files {
  291. if string.contains("PDF Reader Pro") {
  292. var tempS = string
  293. // tempS.replace(" ", with: "\\ ")
  294. // tempS.replacingCharacters(in: " ", with: "\\ ")
  295. // let ss = tempS.replacing(" ", with: "\\ ")
  296. let ss = tempS.replacingOccurrences(of: " ", with: "\\ ")
  297. results.append(ss)
  298. }
  299. }
  300. if let file = results.last {
  301. let cmd = "cat \(file)"
  302. // cat /Library/Logs/DiagnosticReports/PDF*
  303. let logs = _runShellCommand(cmd)
  304. return logs
  305. }
  306. return ""
  307. }
  308. private func _runShellCommand(_ command: String) -> String {
  309. let task = Process()
  310. let pipe = Pipe()
  311. task.standardOutput = pipe
  312. task.standardError = pipe
  313. task.arguments = ["-c", command]
  314. task.launchPath = "/bin/bash"
  315. do {
  316. try task.run()
  317. } catch {
  318. return "Error running command: \(error)"
  319. }
  320. let data = pipe.fileHandleForReading.readDataToEndOfFile()
  321. let output = String(data: data, encoding: .utf8) ?? ""
  322. return output
  323. }
  324. func test() {
  325. var convertString = "TARGET_BUNDLE_IDENTIFIER=\"com.brother.pdfreaderprofree.mac\"\n"
  326. convertString = convertString.appendingFormat("SANDBOX_ROOT=$(osascript -e 'tell application \"System Events\" to get the path of the home folder of (system info)')\n")
  327. convertString = convertString.appendingFormat("CONTAINER_LOG_PATH=$(osascript <<EOF\n")
  328. convertString.append("tell application \"System Events\"\n")
  329. // convertString.append("set isRun to running\n")
  330. convertString.append("set container_path to POSIX path of (the home folder as text) & \"Library:Containers:\" & \"$TARGET_BUNDLE_IDENTIFIER\"\n")
  331. convertString.append("set log_path to container_path & \":Logs:\" & \"Console.app-History\"\n")
  332. convertString.append("return log_path\n")
  333. convertString.append("end tell\n")
  334. convertString.append("EOF)\n")
  335. convertString.append("echo \"日志文件路径: $CONTAINER_LOG_PATH\"\n")
  336. var dic: NSDictionary?
  337. let docScript = NSAppleScript(source: convertString)
  338. docScript?.executeAndReturnError(&dic)
  339. }
  340. func fileSizeString(_ fSize: Float) -> String {
  341. let fileSize = fSize / 1024
  342. let size = fileSize >= 1024 ? (fileSize < 1048576 ? fileSize/1024 : fileSize/1048576.0) : fileSize
  343. let unit = fileSize >= 1024 ? (fileSize < 1048576 ? "M" : "G") : "K"
  344. return String(format: "%0.1f %@", size, unit)
  345. }
  346. private func _feekbackAction(filePaths: [String], complete: @escaping KMRequestServerComplete) {
  347. let urlString = kVerificationServer + "/api/feedback/feedback"
  348. var fileDatas: [Data] = []
  349. for filePath in filePaths {
  350. if let fileData = FileManager.default.contents(atPath: filePath) {
  351. fileDatas.append(fileData)
  352. }
  353. }
  354. let uuid = GetHardwareUUID()
  355. let params: [String: Any] = ["email": self.emailItemView_?.string() ?? "",
  356. "title": self.typeItemView_?.string() ?? "",
  357. "content": self.despItemView_?.string() ?? "",
  358. "unique_sn" : uuid!,
  359. "files[]" : fileDatas]
  360. KMAIRequestServer.requestServer.uploadFile(urlString: urlString, params: params) { formData in
  361. for i in 0 ..< fileDatas.count {
  362. let path = filePaths[i]
  363. let fileURL = URL(fileURLWithPath: path)
  364. try? formData.appendPart(withFileURL: fileURL, name: "files[]", fileName: fileURL.lastPathComponent, mimeType: "application/octet-stream")
  365. }
  366. } requestSerializer: { requestSerializer in
  367. requestSerializer.setValue("Apifox/1.0.0 (https://www.apifox.cn)", forHTTPHeaderField: "User-Agent")
  368. } completion: { task, responseObject, error in
  369. if responseObject != nil {
  370. let data: NSDictionary = responseObject as? NSDictionary ?? [:]
  371. // var code: String = responseObject!["code"] as? String ?? "06005"
  372. let code = data["code"] as? Int ?? 06005
  373. if code == 06005 {
  374. // let tempCode: Int = responseObject!["code"] as? Int ?? 0
  375. // if tempCode == 501 {
  376. // code = "501"
  377. // }
  378. }
  379. let message: String = data["message"] as? String ?? "unknown error"
  380. let error = NSError(domain: message, code: code)
  381. if code == 200 {
  382. let wrapper = ResultWrapper(success: true, resultData: data)
  383. // wrapper.content = data["fileKey"] as! String
  384. complete(wrapper)
  385. } else {
  386. let wrapper = ResultWrapper(success: false, resultData: data)
  387. if code == 501 {
  388. wrapper.content = "501"
  389. } else {
  390. wrapper.content = message
  391. }
  392. complete(wrapper)
  393. }
  394. } else {
  395. complete(ResultWrapper(success: false, content: "unknown error"))
  396. }
  397. }
  398. }
  399. private func _isConnectionAvailable() -> Bool {
  400. if Reachability.forInternetConnection().currentReachabilityStatus().rawValue == 0 {
  401. return false
  402. }
  403. return true
  404. }
  405. }
  406. extension KMUserFeekbackWindowController: NSPopoverDelegate {
  407. func popoverWillClose(_ notification: Notification) {
  408. if let data = self.popover_?.isEqual(to: notification.object), data {
  409. self.popover_ = nil
  410. }
  411. }
  412. }