diff --git a/Note/FormatLogViewController.swift b/Note/FormatLogViewController.swift index 064e83a..d32d536 100644 --- a/Note/FormatLogViewController.swift +++ b/Note/FormatLogViewController.swift @@ -18,6 +18,7 @@ class FormatLogViewController: UIViewController { private var resetButton: UIBarButtonItem! private var isFormatted = false private var rightBarButtonMode: RightBarButtonMode = .paste + var initialText: String? private enum RightBarButtonMode { case paste @@ -30,6 +31,13 @@ class FormatLogViewController: UIViewController { navigationItem.largeTitleDisplayMode = .never view.backgroundColor = .systemGroupedBackground setupUI() + if let initialText = initialText { + textView.text = initialText + isFormatted = true + formatButton.setTitle("保存到 iCloud", for: .normal) + rightBarButtonMode = .reset + updateRightBarButtonItem() + } setupKeyboardObservers() } @@ -151,7 +159,7 @@ class FormatLogViewController: UIViewController { if isFormatted { saveToFiles() } else { - textView.text = formatLog(textView.text) + textView.text = TextFormatter.formatLog(textView.text) isFormatted = true formatButton.setTitle("保存到 iCloud", for: .normal) rightBarButtonMode = .reset @@ -222,28 +230,6 @@ class FormatLogViewController: UIViewController { } return cleanName + ".md" } - - private func formatLog(_ 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 - content = content.replacingOccurrences(of: "一、生活方面", with: "## 一、生活方面") - content = content.replacingOccurrences(of: "二、交易方面", with: "## 二、交易方面") - content = content.replacingOccurrences(of: "三、工作方面", with: "## 三、工作方面") - - return content - } } extension FormatLogViewController: UITextViewDelegate { diff --git a/Note/FormatNoteViewController.swift b/Note/FormatNoteViewController.swift index 73bf468..782bdd0 100644 --- a/Note/FormatNoteViewController.swift +++ b/Note/FormatNoteViewController.swift @@ -20,6 +20,7 @@ class FormatNoteViewController: UIViewController { private var rightBarButtonMode: RightBarButtonMode = .paste private var isCloudFormatted = false private var isNoteFormatted = false + var initialText: String? private enum RightBarButtonMode { case paste @@ -32,6 +33,13 @@ class FormatNoteViewController: UIViewController { navigationItem.largeTitleDisplayMode = .never view.backgroundColor = .systemGroupedBackground setupUI() + if let initialText = initialText { + textView.text = initialText + isNoteFormatted = true + noteButton.setTitle("复制并打开备忘录", for: .normal) + rightBarButtonMode = .reset + updateRightBarButtonItem() + } setupKeyboardObservers() } @@ -214,7 +222,7 @@ class FormatNoteViewController: UIViewController { if isCloudFormatted { saveToFiles() } else { - textView.text = formatForCloud(textView.text) + textView.text = TextFormatter.formatForCloud(textView.text) isCloudFormatted = true iCloudButton.setTitle("保存到 iCloud", for: .normal) rightBarButtonMode = .reset @@ -230,7 +238,7 @@ class FormatNoteViewController: UIViewController { if isNoteFormatted { copyAndOpenNotes() } else { - textView.text = formatForNote(textView.text) + textView.text = TextFormatter.formatForNote(textView.text) isNoteFormatted = true noteButton.setTitle("复制并打开备忘录", for: .normal) rightBarButtonMode = .reset @@ -245,86 +253,6 @@ class FormatNoteViewController: UIViewController { 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) @@ -351,20 +279,6 @@ class FormatNoteViewController: UIViewController { } 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 { diff --git a/Note/Info.plist b/Note/Info.plist index d719f0f..a45e695 100644 --- a/Note/Info.plist +++ b/Note/Info.plist @@ -9,6 +9,31 @@ mobilenotes shareddocuments + CFBundleDocumentTypes + + + CFBundleTypeName + Markdown File + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + net.daringfireball.markdown + public.plain-text + public.data + + CFBundleTypeExtensions + + md + markdown + txt + + + + LSSupportsOpeningDocumentsInPlace + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Note/PreviewViewController.swift b/Note/PreviewViewController.swift new file mode 100644 index 0000000..4d9560e --- /dev/null +++ b/Note/PreviewViewController.swift @@ -0,0 +1,99 @@ +// +// PreviewViewController.swift +// Note +// +// Created by fish on 2026/7/13. +// + +import UIKit + +class PreviewViewController: UIViewController { + + private var textView: UITextView! + private var logButton: UIButton! + private var noteButton: UIButton! + + var initialText: String? + var fileName: String? + + 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 = false + textView.isSelectable = true + textView.text = initialText + view.addSubview(textView) + + logButton = createBottomButton(title: "处理日志", color: .systemBlue) + logButton.addTarget(self, action: #selector(didTapLogButton), for: .touchUpInside) + view.addSubview(logButton) + + 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), + + logButton.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: spacing), + logButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: spacing), + logButton.trailingAnchor.constraint(equalTo: safeArea.centerXAnchor, constant: -spacing / 2), + logButton.heightAnchor.constraint(equalToConstant: buttonHeight), + logButton.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 didTapLogButton() { + let viewController = FormatLogViewController() + viewController.initialText = TextFormatter.formatLog(initialText ?? "") + navigationController?.pushViewController(viewController, animated: true) + } + + @objc private func didTapNoteButton() { + let viewController = FormatNoteViewController() + viewController.initialText = TextFormatter.formatForNote(initialText ?? "") + navigationController?.pushViewController(viewController, animated: true) + } +} diff --git a/Note/SceneDelegate.swift b/Note/SceneDelegate.swift index f40f6c0..7183a4f 100644 --- a/Note/SceneDelegate.swift +++ b/Note/SceneDelegate.swift @@ -20,6 +20,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let navigationController = UINavigationController(rootViewController: rootViewController) window?.rootViewController = navigationController window?.makeKeyAndVisible() + + if let url = connectionOptions.urlContexts.first?.url { + handleSharedFile(url) + } } func sceneDidDisconnect(_ scene: UIScene) { @@ -50,6 +54,38 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + handleSharedFile(url) + } + + private func handleSharedFile(_ url: URL) { + guard url.isFileURL, + let navigationController = window?.rootViewController as? UINavigationController else { + return + } + + let shouldStopAccessing = url.startAccessingSecurityScopedResource() + defer { + if shouldStopAccessing { + url.stopAccessingSecurityScopedResource() + } + } + + do { + let content = try String(contentsOf: url, encoding: .utf8) + DispatchQueue.main.async { + let viewController = PreviewViewController() + viewController.initialText = content + viewController.fileName = url.lastPathComponent + viewController.title = url.lastPathComponent + navigationController.pushViewController(viewController, animated: true) + } + } catch { + print("Failed to read shared file: \(error)") + } + } + } diff --git a/Note/TextFormatter.swift b/Note/TextFormatter.swift new file mode 100644 index 0000000..aecb99a --- /dev/null +++ b/Note/TextFormatter.swift @@ -0,0 +1,127 @@ +// +// TextFormatter.swift +// Note +// +// Created by fish on 2026/7/13. +// + +import Foundation + +enum TextFormatter { + + static func formatLog(_ 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 + content = content.replacingOccurrences(of: "一、生活方面", with: "## 一、生活方面") + content = content.replacingOccurrences(of: "二、交易方面", with: "## 二、交易方面") + content = content.replacingOccurrences(of: "三、工作方面", with: "## 三、工作方面") + + return content + } + + static 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 + } + + static 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) + } + + static 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") + } + + static func insertEmptyLineAfterFirstLine(_ text: String) -> String { + var lines = text.components(separatedBy: .newlines) + if !lines.isEmpty { + lines.insert("", at: 1) + } + return lines.joined(separator: "\n") + } +}