1a525c763e
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
314 lines
12 KiB
Swift
314 lines
12 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 floatingDoneButton: UIButton!
|
|
private var floatingDoneButtonBottomConstraint: NSLayoutConstraint!
|
|
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
|
|
var initialText: String?
|
|
|
|
private enum RightBarButtonMode {
|
|
case paste
|
|
case reset
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
title = "格式化笔记"
|
|
navigationItem.largeTitleDisplayMode = .never
|
|
view.backgroundColor = .systemGroupedBackground
|
|
setupUI()
|
|
if let initialText = initialText {
|
|
textView.text = initialText
|
|
}
|
|
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)
|
|
|
|
var doneConfig = UIButton.Configuration.glass()
|
|
doneConfig.title = "完成"
|
|
doneConfig.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
|
var outgoing = incoming
|
|
outgoing.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
|
return outgoing
|
|
}
|
|
doneConfig.baseForegroundColor = .label
|
|
doneConfig.cornerStyle = .capsule
|
|
doneConfig.contentInsets = NSDirectionalEdgeInsets(top: 15, leading: 22, bottom: 15, trailing: 22)
|
|
floatingDoneButton = UIButton(configuration: doneConfig)
|
|
floatingDoneButton.translatesAutoresizingMaskIntoConstraints = false
|
|
floatingDoneButton.alpha = 0
|
|
floatingDoneButton.addTarget(self, action: #selector(didTapFloatingDoneButton), for: .touchUpInside)
|
|
view.addSubview(floatingDoneButton)
|
|
|
|
let safeArea = view.safeAreaLayoutGuide
|
|
let buttonHeight: CGFloat = 52
|
|
let spacing: CGFloat = 16
|
|
|
|
floatingDoneButtonBottomConstraint = floatingDoneButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -spacing)
|
|
|
|
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),
|
|
|
|
floatingDoneButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -spacing),
|
|
floatingDoneButtonBottomConstraint
|
|
])
|
|
}
|
|
|
|
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 didTapFloatingDoneButton() {
|
|
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
|
|
self.floatingDoneButtonBottomConstraint.constant = -(keyboardHeight + 8)
|
|
self.floatingDoneButton.alpha = 1
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
@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
|
|
self.floatingDoneButtonBottomConstraint.constant = -16
|
|
self.floatingDoneButton.alpha = 0
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
@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() {
|
|
let alert = UIAlertController(
|
|
title: "确认重置",
|
|
message: "确定要清空当前内容吗?",
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
alert.addAction(UIAlertAction(title: "重置", style: .destructive) { [weak self] _ in
|
|
self?.performReset()
|
|
})
|
|
present(alert, animated: true)
|
|
}
|
|
|
|
private func performReset() {
|
|
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 = TextFormatter.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 = TextFormatter.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 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"
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|