KMUserFeekbackWindowController.swift 23 KB

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