325 lines
14 KiB
Swift
325 lines
14 KiB
Swift
//
|
||
// 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()
|
||
}
|
||
|
||
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()
|
||
}
|
||
|
||
@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: "")
|
||
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 paragraphs = content.components(separatedBy: .newlines)
|
||
.split(separator: "", omittingEmptySubsequences: false)
|
||
.map { $0.joined(separator: "\n") }
|
||
|
||
// 每个段落内部按句换行,段落之间保留两个空行
|
||
let formattedParagraphs = paragraphs.map { paragraph -> String in
|
||
var p = paragraph
|
||
p = p.replacingOccurrences(of: "。", with: "。\n")
|
||
p = p.replacingOccurrences(of: "?", with: "?\n")
|
||
p = removeEmptyLines(p)
|
||
return p
|
||
}
|
||
|
||
return formattedParagraphs.joined(separator: "\n\n\n")
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|