// // FormatNoteViewController.swift // Note // // Created by fish on 2026/7/12. // import UIKit class FormatNoteViewController: UIViewController { private var textView: UITextView! private var iCloudButton: UIButton! private var noteButton: UIButton! private var doneButton: UIBarButtonItem! private var pasteButton: UIBarButtonItem! private var resetButton: UIBarButtonItem! private var rightBarButtonMode: RightBarButtonMode = .paste private var isCloudFormatted = false private var isNoteFormatted = false private enum RightBarButtonMode { case paste case reset } override func viewDidLoad() { super.viewDidLoad() title = "格式化笔记" navigationItem.largeTitleDisplayMode = .never view.backgroundColor = .systemGroupedBackground setupUI() setupKeyboardObservers() } deinit { NotificationCenter.default.removeObserver(self) } private func setupUI() { textView = UITextView() textView.translatesAutoresizingMaskIntoConstraints = false textView.backgroundColor = UIColor { traitCollection in traitCollection.userInterfaceStyle == .dark ? .secondarySystemBackground : .white } textView.layer.cornerRadius = 12 textView.layer.borderWidth = 0.5 textView.layer.borderColor = UIColor.separator.cgColor textView.layer.masksToBounds = true textView.font = UIFont.preferredFont(forTextStyle: .body) textView.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) textView.isEditable = true textView.delegate = self view.addSubview(textView) doneButton = UIBarButtonItem(title: "完成", style: .plain, target: self, action: #selector(didTapDoneButton)) pasteButton = UIBarButtonItem(title: "粘贴文案", style: .plain, target: self, action: #selector(didTapPasteButton)) resetButton = UIBarButtonItem(title: "重置", style: .plain, target: self, action: #selector(didTapResetButton)) updateRightBarButtonItem() iCloudButton = createBottomButton(title: "iCloud 格式化", color: .systemBlue) iCloudButton.addTarget(self, action: #selector(didTapiCloudButton), for: .touchUpInside) view.addSubview(iCloudButton) noteButton = createBottomButton(title: "Note 格式化", color: .systemGreen) noteButton.addTarget(self, action: #selector(didTapNoteButton), for: .touchUpInside) view.addSubview(noteButton) let safeArea = view.safeAreaLayoutGuide let buttonHeight: CGFloat = 52 let spacing: CGFloat = 16 NSLayoutConstraint.activate([ textView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: spacing), textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: spacing), textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -spacing), iCloudButton.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: spacing), iCloudButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: spacing), iCloudButton.trailingAnchor.constraint(equalTo: safeArea.centerXAnchor, constant: -spacing / 2), iCloudButton.heightAnchor.constraint(equalToConstant: buttonHeight), iCloudButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -spacing), noteButton.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: spacing), noteButton.leadingAnchor.constraint(equalTo: safeArea.centerXAnchor, constant: spacing / 2), noteButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -spacing), noteButton.heightAnchor.constraint(equalToConstant: buttonHeight), noteButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -spacing) ]) } private func createBottomButton(title: String, color: UIColor) -> UIButton { var config = UIButton.Configuration.filled() config.title = title config.baseBackgroundColor = color config.baseForegroundColor = .white config.cornerStyle = .medium config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) let button = UIButton(configuration: config) button.translatesAutoresizingMaskIntoConstraints = false return button } @objc private func didTapDoneButton() { textView.resignFirstResponder() } private func setupKeyboardObservers() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) } @objc private func keyboardWillShow(_ notification: Notification) { guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } let keyboardFrameInView = view.convert(keyboardFrame, from: nil) let intersection = keyboardFrameInView.intersection(view.bounds) let keyboardHeight = intersection.height let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25 UIView.animate(withDuration: duration, delay: 0, options: .beginFromCurrentState) { self.textView.contentInset.bottom = keyboardHeight self.textView.verticalScrollIndicatorInsets.bottom = keyboardHeight } } @objc private func keyboardWillHide(_ notification: Notification) { let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25 UIView.animate(withDuration: duration, delay: 0, options: .beginFromCurrentState) { self.textView.contentInset.bottom = 0 self.textView.verticalScrollIndicatorInsets.bottom = 0 } } @objc private func didTapPasteButton() { if let string = UIPasteboard.general.string { textView.text = string } } private func trimmedText() -> String { return textView.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } private func showEmptyAlert() { let alert = UIAlertController(title: "提示", message: "请输入内容后再进行格式化", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default)) present(alert, animated: true) } @objc private func didTapResetButton() { textView.text = "" rightBarButtonMode = .paste isCloudFormatted = false isNoteFormatted = false iCloudButton.setTitle("iCloud 格式化", for: .normal) noteButton.setTitle("Note 格式化", for: .normal) updateRightBarButtonItem() } private func updateRightBarButtonItem() { if textView.isFirstResponder { navigationItem.rightBarButtonItem = doneButton } else { switch rightBarButtonMode { case .paste: navigationItem.rightBarButtonItem = pasteButton case .reset: navigationItem.rightBarButtonItem = resetButton } } } @objc private func didTapiCloudButton() { guard !trimmedText().isEmpty else { showEmptyAlert() return } if isCloudFormatted { saveToFiles() } else { textView.text = formatForCloud(textView.text) isCloudFormatted = true iCloudButton.setTitle("保存到 iCloud", for: .normal) rightBarButtonMode = .reset updateRightBarButtonItem() } } @objc private func didTapNoteButton() { guard !trimmedText().isEmpty else { showEmptyAlert() return } if isNoteFormatted { copyAndOpenNotes() } else { textView.text = formatForNote(textView.text) isNoteFormatted = true noteButton.setTitle("复制并打开备忘录", for: .normal) rightBarButtonMode = .reset updateRightBarButtonItem() } } private func copyAndOpenNotes() { UIPasteboard.general.string = textView.text guard let url = URL(string: "mobilenotes://") else { return } guard UIApplication.shared.canOpenURL(url) else { return } UIApplication.shared.open(url, options: [:], completionHandler: nil) } private func formatForCloud(_ text: String) -> String { var content = text content = content.replacingOccurrences(of: " ", with: "") content = content.replacingOccurrences(of: " ", with: "") content = content.replacingOccurrences(of: "\u{00A0}", with: "") content = content.replacingOccurrences(of: "# ", with: "") content = content.replacingOccurrences(of: "#", with: "") content = content.replacingOccurrences(of: "!", with: "。") content = content.replacingOccurrences(of: "!", with: "。") content = content.replacingOccurrences(of: ";", with: "。") content = content.replacingOccurrences(of: ";", with: "。") content = content.replacingOccurrences(of: ".", with: "。") content = content.replacingOccurrences(of: "(", with: "(") content = content.replacingOccurrences(of: ")", with: ")") content = content.replacingOccurrences(of: "?", with: "?") content = content.replacingOccurrences(of: ",", with: ",") content = content.replacingOccurrences(of: ":", with: ":") let numberReplacements: [(String, String)] = [ ("20、", "二十、"), ("19、", "十九、"), ("18、", "十八、"), ("17、", "十七、"), ("16、", "十六、"), ("15、", "十五、"), ("14、", "十四、"), ("13、", "十三、"), ("12、", "十二、"), ("11、", "十一、"), ("10、", "十、"), ("9、", "九、"), ("8、", "八、"), ("7、", "七、"), ("6、", "六、"), ("5、", "五、"), ("4、", "四、"), ("3、", "三、"), ("2、", "二、"), ("1、", "一、") ] for (old, new) in numberReplacements { content = content.replacingOccurrences(of: old, with: new) } content = content.replacingOccurrences(of: "。", with: "。\n") content = content.replacingOccurrences(of: "?", with: "?\n") content = removeEmptyLines(content) content = content.replacingOccurrences(of: ":\\s*", with: ":\n\n", options: .regularExpression) content = content.replacingOccurrences(of: "。", with: "。\n") content = content.replacingOccurrences(of: "?", with: "?\n") content = content.replacingOccurrences(of: ":\n\n", with: ":") content = content.replacingOccurrences(of: ":一、", with: ":\n\n一、") content = content.replacingOccurrences(of: " ", with: "") content = content.replacingOccurrences(of: "---", with: "---\n") content = content.replacingOccurrences(of: "日星期", with: "日 星期") content = "# " + content content = insertEmptyLineAfterFirstLine(content) return content } private func formatForNote(_ text: String) -> String { var content = text content = content.replacingOccurrences(of: "# ", with: "") content = content.replacingOccurrences(of: "#", with: "") content = content.replacingOccurrences(of: "日星期", with: "日 星期") // 第一行固定为标题 let allLines = removeEmptyLines(content).components(separatedBy: .newlines) guard let title = allLines.first else { return content } // 剩余内容按 --- 分段 let bodyLines = Array(allLines.dropFirst()) var groups: [[String]] = [] var currentGroup: [String] = [] for line in bodyLines { if line == "---" { if !currentGroup.isEmpty { groups.append(currentGroup) currentGroup = [] } } else { currentGroup.append(line) } } if !currentGroup.isEmpty { groups.append(currentGroup) } var result = title for group in groups { result += "\n\n\n" + group.joined(separator: "\n") } return result.trimmingCharacters(in: .whitespacesAndNewlines) } private func saveToFiles() { let fileName = generateFileName() let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) do { try textView.text.write(to: fileURL, atomically: true, encoding: .utf8) let documentPicker = UIDocumentPickerViewController(forExporting: [fileURL], asCopy: true) documentPicker.delegate = self present(documentPicker, animated: true) } catch { print("Failed to write file: \(error)") } } private func generateFileName() -> String { let text = textView.text ?? "" let firstLine = text.components(separatedBy: .newlines).first ?? "" let trimmed = firstLine.trimmingCharacters(in: .whitespaces) let withoutHash = trimmed.replacingOccurrences(of: "^#\\s*", with: "", options: .regularExpression) let cleanName = withoutHash.trimmingCharacters(in: .whitespaces) if cleanName.isEmpty { return "formatted_note_\(Int(Date().timeIntervalSince1970)).md" } return cleanName + ".md" } private func removeEmptyLines(_ text: String) -> String { let lines = text.components(separatedBy: .newlines) let nonEmptyLines = lines.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } return nonEmptyLines.joined(separator: "\n") } private func insertEmptyLineAfterFirstLine(_ text: String) -> String { var lines = text.components(separatedBy: .newlines) if !lines.isEmpty { lines.insert("", at: 1) } return lines.joined(separator: "\n") } } extension FormatNoteViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { // File saved to selected location } func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { // User cancelled } } extension FormatNoteViewController: UITextViewDelegate { func textViewDidBeginEditing(_ textView: UITextView) { updateRightBarButtonItem() } func textViewDidEndEditing(_ textView: UITextView) { updateRightBarButtonItem() } }