diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index bf14a13581..0bc89e3d2b 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -326,9 +326,13 @@ ); inputFileListPaths = ( ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n"; @@ -521,7 +525,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -559,7 +563,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 63f227d74d..54b8df9334 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,35 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apollographql/apollo-ios", "state" : { - "revision" : "39fea7617346c0731be25f61afd537e7032fb562", - "version" : "1.22.0" + "revision" : "9aa748d6f0526a744d49d59a2383dc7fdf9d645b", + "version" : "1.18.0" } }, { - "identity" : "chidorimenu", + "identity" : "sqlite.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/Lakr233/ChidoriMenu", + "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "3bb4323fe0f7f8f435d15656c3eeffcbb7c9c605", - "version" : "3.0.0" - } - }, - { - "identity" : "splash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/Splash", - "state" : { - "revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8", - "version" : "0.16.0" - } - }, - { - "identity" : "swift-cmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-cmark", - "state" : { - "revision" : "b022b08312decdc46585e0b3440d97f6f22ef703", - "version" : "0.6.0" + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" } }, { diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0763ea436..8e0a0f72ba 100644 --- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,35 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apollographql/apollo-ios.git", "state" : { - "revision" : "39fea7617346c0731be25f61afd537e7032fb562", - "version" : "1.22.0" + "revision" : "9aa748d6f0526a744d49d59a2383dc7fdf9d645b", + "version" : "1.18.0" } }, { - "identity" : "chidorimenu", + "identity" : "sqlite.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/Lakr233/ChidoriMenu", + "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "3bb4323fe0f7f8f435d15656c3eeffcbb7c9c605", - "version" : "3.0.0" - } - }, - { - "identity" : "splash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/Splash", - "state" : { - "revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8", - "version" : "0.16.0" - } - }, - { - "identity" : "swift-cmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-cmark", - "state" : { - "revision" : "b022b08312decdc46585e0b3440d97f6f22ef703", - "version" : "0.6.0" + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" } }, { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index 1f7fef2c54..e3f6ababcb 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -7,26 +7,20 @@ let package = Package( name: "Intelligents", defaultLocalization: "en", platforms: [ - .iOS(.v15), - .macCatalyst(.v15), + .iOS(.v17), ], products: [ .library(name: "Intelligents", targets: ["Intelligents"]), ], dependencies: [ .package(path: "../AffineGraphQL"), - .package(path: "../MarkdownView"), - .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.22.0"), + .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.18.0"), .package(url: "https://github.com/LaunchDarkly/swift-eventsource.git", from: "3.3.0"), .package(url: "https://github.com/apple/swift-collections", from: "1.2.0"), - .package(url: "https://github.com/Lakr233/ChidoriMenu", from: "3.0.0"), ], targets: [ .target(name: "Intelligents", dependencies: [ "AffineGraphQL", - "ChidoriMenu", - "MarkdownView", - "ChidoriMenu", .product(name: "Apollo", package: "apollo-ios"), .product(name: "LDSwiftEventSource", package: "swift-eventsource"), .product(name: "OrderedCollections", package: "swift-collections"), diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Constant/Constant.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Constant/Constant.swift deleted file mode 100644 index e5f639f6f6..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Constant/Constant.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Constant.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -enum Constant { - static let affineTabbarHeight: CGFloat = 44 - static let affineTintColor: UIColor = .init(red: 30 / 255, green: 150 / 255, blue: 235 / 255, alpha: 1.0) - - static var affineUpstreamURL = URL(string: "https://app.affine.pro/")! -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Constant/UnableTo.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Constant/UnableTo.swift deleted file mode 100644 index bb7c809436..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Constant/UnableTo.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// UnableTo.swift -// Intelligents -// -// Created by 秋星桥 on 4/1/25. -// - -import Foundation - -private let domain = "Intelligents" - -enum UnableTo { - static let identifyDocumentOrWorkspace = - NSError( - domain: domain, - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Unable to identify the document or workspace"] - ) - - static let createSession = NSError( - domain: domain, - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Unable to create a session"] - ) - - static let createMessage = NSError( - domain: domain, - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Unable to create a message"] - ) - - static let compressImage = NSError( - domain: domain, - code: -1, - userInfo: [ - NSLocalizedDescriptionKey: "Failed to compress image data", - ] - ) - - static let clearHistory = NSError( - domain: domain, - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Unable to clear history"] - ) -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Model/Chat.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Model/Chat.swift deleted file mode 100644 index 85fba639b6..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Model/Chat.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Chat.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import Foundation - -struct Chat: Codable { - enum ParticipantType: String, Codable, Equatable { - case user - case bot - } - - var participant: ParticipantType - - typealias MarkdownDocument = String - var content: MarkdownDocument - var date: Date - - init(participant: ParticipantType, content: MarkdownDocument, date: Date = .init()) { - self.participant = participant - self.content = content - self.date = date - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Model/Prompt.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Model/Prompt.swift deleted file mode 100644 index 6bbac1b907..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Backend/Model/Prompt.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Prompt.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/26. -// - -import Foundation - -enum Prompt: String { - #if DEBUG - case debug_action_dalle3 = "debug:action:dalle3" - case debug_action_fal_sd15 = "debug:action:fal-sd15" - case debug_action_fal_upscaler = "debug:action:fal-upscaler" - case debug_action_fal_remove_bg = "debug:action:fal-remove-bg" - case debug_action_fal_face_to_sticker = "debug:action:fal-face-to-sticker" - #endif - - case general_Chat_With_AFFiNE_AI = "Chat With AFFiNE AI" - case general_Summary = "Summary" - case general_Generate_a_caption = "Generate a caption" - case general_Summary_the_webpage = "Summary the webpage" - case general_Explain_this = "Explain this" - case general_Explain_this_image = "Explain this image" - case general_Explain_this_code = "Explain this code" - case general_Translate_to = "Translate to" - case general_Write_an_article_about_this = "Write an article about this" - case general_Write_a_twitter_about_this = "Write a twitter about this" - case general_Write_a_poem_about_this = "Write a poem about this" - case general_Write_a_blog_post_about_this = "Write a blog post about this" - case general_Write_outline = "Write outline" - case general_Change_tone_to = "Change tone to" - case general_Brainstorm_ideas_about_this = "Brainstorm ideas about this" - case general_Expand_mind_map = "Expand mind map" - case general_Improve_writing_for_it = "Improve writing for it" - case general_Improve_grammar_for_it = "Improve grammar for it" - case general_Fix_spelling_for_it = "Fix spelling for it" - case general_Find_action_items_from_it = "Find action items from it" - case general_Check_code_error = "Check code error" - case general_Create_headings = "Create headings" - case general_Make_it_real = "Make it real" - case general_Make_it_real_with_text = "Make it real with text" - case general_Make_it_longer = "Make it longer" - case general_Make_it_shorter = "Make it shorter" - case general_Continue_writing = "Continue writing" - - case workflow_presentation = "workflow:presentation" - case workflow_brainstorm = "workflow:brainstorm" - case workflow_image_sketch = "workflow:image-sketch" - case workflow_image_clay = "workflow:image-clay" - case workflow_image_anime = "workflow:image-anime" - case workflow_image_pixel = "workflow:image-pixel" -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+EventHandler.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+EventHandler.swift deleted file mode 100644 index f7ee027bb7..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+EventHandler.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Ext+EventHandler.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/26. -// - -import Foundation -import LDSwiftEventSource - -class BlockEventHandler: EventHandler { - var onOpenedBlock: (() -> Void)? - var onClosedBlock: (() -> Void)? - var onMessageBlock: ((String, LDSwiftEventSource.MessageEvent) -> Void)? - var onCommentBlock: ((String) -> Void)? - var onErrorBlock: ((Error) -> Void)? - - public func onOpened() { - onOpenedBlock?() - } - - public func onClosed() { - onClosedBlock?() - } - - public func onMessage(eventType: String, messageEvent: LDSwiftEventSource.MessageEvent) { - onMessageBlock?(eventType, messageEvent) - } - - public func onComment(comment: String) { - onCommentBlock?(comment) - } - - public func onError(error: any Error) { - onErrorBlock?(error) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift deleted file mode 100644 index 7feddb601e..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Ext+String.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import Foundation - -extension String { - func localized() -> String { - let ans = NSLocalizedString(self, bundle: Bundle.module, comment: "") - guard !ans.isEmpty else { - assertionFailure() - return self - } - return ans - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIColor.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIColor.swift deleted file mode 100644 index 932f5f3ebf..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIColor.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Ext+UIColor.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/13. -// - -import UIKit - -extension UIColor { - static var accent: UIColor { - Constant.affineTintColor - } - - convenience init(light: UIColor, dark: UIColor) { - self.init(dynamicProvider: { traitCollection in - switch traitCollection.userInterfaceStyle { - case .light: - light - case .dark: - dark - default: - light - } - }) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIFont.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIFont.swift deleted file mode 100644 index b94160a5c2..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIFont.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Ext+UIFont.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/21. -// - -import UIKit - -extension UIFont { - static func preferredFont(for style: TextStyle, weight: Weight, italic: Bool = false) -> UIFont { - // Get the style's default pointSize - let traits = UITraitCollection(preferredContentSizeCategory: .large) - let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits) - - // Get the font at the default size and preferred weight - var font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight) - if italic == true { - font = font.with([.traitItalic]) - } - - // Setup the font to be auto-scalable - let metrics = UIFontMetrics(forTextStyle: style) - return metrics.scaledFont(for: font) - } - - private func with(_ traits: UIFontDescriptor.SymbolicTraits...) -> UIFont { - guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits).union(fontDescriptor.symbolicTraits)) else { - return self - } - return UIFont(descriptor: descriptor, size: 0) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift deleted file mode 100644 index 1da7e3b992..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Ext+UIView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -extension UIView { - var parentViewController: UIViewController? { - var responder: UIResponder? = self - while responder != nil { - if let responder = responder as? UIViewController { - return responder - } - responder = responder?.next - } - return nil - } - - func removeEveryAutoResizingMasks() { - var views: [UIView] = [self] - while let view = views.first { - views.removeFirst() - view.translatesAutoresizingMaskIntoConstraints = false - view.subviews.forEach { views.append($0) } - } - } - - #if DEBUG - func debugFrame() { - layer.borderWidth = 1 - layer.borderColor = [ - UIColor.red, - .green, - .blue, - .yellow, - .cyan, - .magenta, - .orange, - ].map(\.cgColor).randomElement() - subviews.forEach { $0.debugFrame() } - } - #endif -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift deleted file mode 100644 index eb58f6849f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// Ext+UIViewController.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -public extension UIViewController { - func presentIntoCurrentContext(withTargetController targetController: UIViewController, animated: Bool = true) { - if let nav = self as? UINavigationController { - nav.pushViewController(targetController, animated: animated) - } else if let nav = navigationController { - nav.pushViewController(targetController, animated: animated) - } else { - present(targetController, animated: animated, completion: nil) - } - } - - func dismissInContext() { - if let nav = navigationController { - nav.popViewController(animated: true) - } else { - dismiss(animated: true, completion: nil) - } - } - - func hideKeyboardWhenTappedAround() { - let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard)) - tap.cancelsTouchesInView = false - view.addGestureRecognizer(tap) - } - - @objc func dismissKeyboard() { - view.endEditing(true) - } - - func presentError(_ error: Error, onDismiss: @escaping () -> Void = {}) { - DispatchQueue.main.async { [self] in - let alert = UIAlertController( - title: "Error".localized(), - message: error.localizedDescription, - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "OK".localized(), style: .default) { _ in - onDismiss() - }) - present(alert, animated: true) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift deleted file mode 100644 index 1959304e7e..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Ext+print.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import Foundation - -public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { - #if DEBUG - Swift.print(items, separator: separator, terminator: terminator) - #endif -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/AttachmentBannerView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/AttachmentBannerView.swift deleted file mode 100644 index e659ff48dd..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/AttachmentBannerView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// AttachmentBannerView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -private let attachmentSize: CGFloat = 100 -private let attachmentSpacing: CGFloat = 16 - -class AttachmentBannerView: UIScrollView { - var readAttachments: (() -> ([UIImage]))? - var onAttachmentsDelete: ((Int) -> Void)? - var attachments: [UIImage] { - get { readAttachments?() ?? [] } - set { assertionFailure() } - } - - override var intrinsicContentSize: CGSize { - if attachments.isEmpty { return .zero } - return .init( - width: (attachmentSize + attachmentSize) * CGFloat(attachments.count) - - attachmentSpacing, - height: attachmentSize - ) - } - - let stackView = UIStackView() - - init() { - super.init(frame: .zero) - - translatesAutoresizingMaskIntoConstraints = false - - showsHorizontalScrollIndicator = false - showsVerticalScrollIndicator = false - - stackView.axis = .horizontal - stackView.spacing = attachmentSpacing - stackView.alignment = .center - stackView.distribution = .fill - stackView.translatesAutoresizingMaskIntoConstraints = false - addSubview(stackView) - [ - stackView.topAnchor.constraint(equalTo: topAnchor), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), - stackView.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - - rebuildViews() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - var reusableViews = [AttachmentPreviewView]() - - func rebuildViews() { - let attachments = attachments - - if reusableViews.count > attachments.count { - for index in attachments.count ..< reusableViews.count { - reusableViews[index].removeFromSuperview() - } - reusableViews.removeLast(reusableViews.count - attachments.count) - } - if reusableViews.count < attachments.count { - for _ in reusableViews.count ..< attachments.count { - let view = AttachmentPreviewView() - view.alpha = 0 - reusableViews.append(view) - } - } - - assert(reusableViews.count == attachments.count) - - for (index, attachment) in attachments.enumerated() { - let view = reusableViews[index] - view.imageView.image = attachment - stackView.addArrangedSubview(view) - view.deleteButtonAction = { [weak self] in - self?.onAttachmentsDelete?(index) - } - } - - invalidateIntrinsicContentSize() - contentSize = intrinsicContentSize - UIView.performWithoutAnimation { - self.layoutIfNeeded() - } - - UIView.animate(withDuration: 0.3) { - for view in self.reusableViews { - view.alpha = 1 - } - } - } -} - -extension AttachmentBannerView { - class AttachmentPreviewView: UIView { - let imageView = UIImageView() - let deleteButton = UIButton() - - var deleteButtonAction: (() -> Void)? - - override var intrinsicContentSize: CGSize { - .init(width: attachmentSize, height: attachmentSize) - } - - init() { - super.init(frame: .zero) - addSubview(imageView) - addSubview(deleteButton) - - layer.cornerRadius = 8 - clipsToBounds = true - - imageView.contentMode = .scaleAspectFill - imageView.clipsToBounds = true - imageView.translatesAutoresizingMaskIntoConstraints = false - [ - imageView.topAnchor.constraint(equalTo: topAnchor), - imageView.leadingAnchor.constraint(equalTo: leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: trailingAnchor), - imageView.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - - deleteButton.setImage(.init(named: "close", in: .module, with: nil), for: .normal) - deleteButton.imageView?.contentMode = .scaleAspectFit - deleteButton.tintColor = .white - deleteButton.translatesAutoresizingMaskIntoConstraints = false - [ - deleteButton.topAnchor.constraint(equalTo: topAnchor, constant: 4), - deleteButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), - deleteButton.widthAnchor.constraint(equalToConstant: 32), - deleteButton.heightAnchor.constraint(equalToConstant: 32), - ].forEach { $0.isActive = true } - - deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) - - [ - widthAnchor.constraint(equalToConstant: attachmentSize), - heightAnchor.constraint(equalToConstant: attachmentSize), - ].forEach { $0.isActive = true } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - @objc func deleteButtonTapped() { - deleteButtonAction?() - deleteButtonAction = nil - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+Camera.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+Camera.swift deleted file mode 100644 index 54babfff8c..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+Camera.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// InputEditView+Camera.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/6. -// - -import AVKit -import UIKit - -extension InputEditView: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - @objc func takePhoto() { - AVCaptureDevice.requestAccess(for: .video) { _ in - DispatchQueue.main.async { - let ctrl = UIImagePickerController() - ctrl.allowsEditing = false - ctrl.sourceType = .camera - ctrl.mediaTypes = [UTType.movie.identifier, UTType.image.identifier] - ctrl.cameraCaptureMode = .photo - ctrl.delegate = self - self.parentViewController?.present(ctrl, animated: true) - } - } - } - - private func processJPEGImageData(_ image: UIImage) throws -> Data? { - guard let data = image.jpegData(compressionQuality: 0.75) else { - throw UnableTo.compressImage - } - return data - } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - picker.dismiss(animated: true) { - var itemUrl: URL? - - if itemUrl == nil, - let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage - { - let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("Camera") - try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - let tempFile = tempDir - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("jpeg") - try? self.processJPEGImageData(image)?.write(to: tempFile) - itemUrl = tempFile - } - if itemUrl == nil, - let url = info[.mediaURL] as? URL - { - itemUrl = url - } - - guard let url = itemUrl, FileManager.default.fileExists(atPath: url.path) else { - return - } - guard let image = UIImage(contentsOfFile: url.path) else { return } - try? FileManager.default.removeItem(at: url) - self.viewModel.attachments.append(image) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+Photo.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+Photo.swift deleted file mode 100644 index 73fca18994..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+Photo.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// InputEditView+Photo.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/6. -// - -import PhotosUI -import UIKit - -extension InputEditView: PHPickerViewControllerDelegate { - @objc func selectPhoto() { - var config = PHPickerConfiguration(photoLibrary: .shared()) - config.filter = .images - config.selectionLimit = 9 - let picker = PHPickerViewController(configuration: config) - picker.modalPresentationStyle = .formSheet - picker.delegate = self - parentViewController?.present(picker, animated: true, completion: nil) - } - - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.dismiss(animated: true) - loadPNG(from: results) - } - - private func loadPNG(from results: [PHPickerResult]) { - for result in results { - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in - if let image = image as? UIImage { - DispatchQueue.main.async { - self?.viewModel.attachments.append(image) - } - } - } - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift deleted file mode 100644 index 9634cff633..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// InputEditView+ViewModel.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import Combine -import UIKit - -extension InputEditView { - class ViewModel: ObservableObject { - var cancellables: Set = [] - - @Published var text: String = "" - @Published var attachments: [UIImage] = [] - - init() {} - - deinit { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - } - - func reset() { - text = "" - attachments = [] - } - - func duplicate() -> ViewModel { - let ans = ViewModel() - ans.text = text - ans.attachments = attachments - return ans - } - } -} - -extension InputEditView.ViewModel: Hashable, Equatable { - func hash(into hasher: inout Hasher) { - hasher.combine(text) - hasher.combine(attachments) - } - - static func == (lhs: InputEditView.ViewModel, rhs: InputEditView.ViewModel) -> Bool { - lhs.hashValue == rhs.hashValue - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView.swift deleted file mode 100644 index 27fe21a14c..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/InputEditView.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// InputEditView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import Combine -import UIKit - -class InputEditView: UIView { - let mainStack = UIStackView() - let attachmentsEditor = AttachmentBannerView() - let textEditor = PlainTextEditView() - let placeholderLabel = UILabel() - let controlBanner = TextEditControlBanner() - - let viewModel = ViewModel() - var placeholderText: String = "" { - didSet { - placeholderLabel.text = placeholderText - } - } - - var submitAction: (() -> Void) = {} - - init() { - super.init(frame: .zero) - - addSubview(mainStack) - mainStack.translatesAutoresizingMaskIntoConstraints = false - mainStack.axis = .vertical - mainStack.spacing = 16 - mainStack.alignment = .fill - mainStack.distribution = .equalSpacing - [ - mainStack.topAnchor.constraint(equalTo: topAnchor), - mainStack.leadingAnchor.constraint(equalTo: leadingAnchor), - mainStack.trailingAnchor.constraint(equalTo: trailingAnchor), - mainStack.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - - textEditor.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).isActive = true - - [ - attachmentsEditor, textEditor, controlBanner, - ].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - mainStack.addArrangedSubview($0) - [ - $0.leadingAnchor.constraint(equalTo: mainStack.leadingAnchor), - $0.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor), - ].forEach { $0.isActive = true } - } - - attachmentsEditor.readAttachments = { [weak self] in - self?.viewModel.attachments ?? [] - } - attachmentsEditor.onAttachmentsDelete = { [weak self] index in - self?.viewModel.attachments.remove(at: index) - } - - controlBanner.cameraButton.addTarget( - self, - action: #selector(takePhoto), - for: .touchUpInside - ) - controlBanner.photoButton.addTarget( - self, - action: #selector(selectPhoto), - for: .touchUpInside - ) - - textEditor.returnKeyType = .send - textEditor.addSubview(placeholderLabel) - placeholderLabel.textColor = .label.withAlphaComponent(0.25) - placeholderLabel.font = textEditor.font - placeholderLabel.translatesAutoresizingMaskIntoConstraints = false - [ - placeholderLabel.leadingAnchor.constraint(equalTo: textEditor.leadingAnchor, constant: 2), - placeholderLabel.trailingAnchor.constraint(equalTo: textEditor.trailingAnchor, constant: -2), - placeholderLabel.topAnchor.constraint(equalTo: textEditor.topAnchor, constant: 0), - ].forEach { $0.isActive = true } - - viewModel.objectWillChange - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.updateValues() - } - .store(in: &viewModel.cancellables) - - updateValues() - - textEditor.textDidChange = { [weak self] text in - self?.viewModel.text = text - self?.updatePlaceholderVisibility() - } - - textEditor.textDidReturn = { [weak self] in - self?.submitAction() - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - func updatePlaceholderVisibility() { - let visible = viewModel.text.isEmpty && !textEditor.isFirstResponder - UIView.animate(withDuration: 0.25) { - self.placeholderLabel.alpha = visible ? 1 : 0 - } - } - - func updateValues() { - UIView.animate( - withDuration: 0.5, - delay: 0, - usingSpringWithDamping: 1.0, - initialSpringVelocity: 0.8 - ) { [self] in - if textEditor.text != viewModel.text { - textEditor.text = viewModel.text - } - attachmentsEditor.rebuildViews() - parentViewController?.view.layoutIfNeeded() - updatePlaceholderVisibility() - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/PlainTextEditView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/PlainTextEditView.swift deleted file mode 100644 index 181161a3b1..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/PlainTextEditView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// PlainTextEditView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -class PlainTextEditView: UITextView, UITextViewDelegate { - var textDidChange: ((String) -> Void) = { _ in } - var textDidReturn: (() -> Void) = {} - - init() { - super.init(frame: .zero, textContainer: nil) - - delegate = self - tintColor = .accent - - linkTextAttributes = [:] - showsVerticalScrollIndicator = false - showsHorizontalScrollIndicator = false - textContainer.lineFragmentPadding = .zero - textAlignment = .natural - backgroundColor = .clear - textContainerInset = .zero - textContainer.lineBreakMode = .byTruncatingTail - isScrollEnabled = false - clipsToBounds = false - - isEditable = true - isSelectable = true - isScrollEnabled = false - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - func textViewDidChange(_ textView: UITextView) { - textDidChange(textView.text) - } - - func textViewDidBeginEditing(_ textView: UITextView) { - textDidChange(textView.text) - } - - func textViewDidEndEditing(_ textView: UITextView) { - textDidChange(textView.text) - } - - func textView(_: UITextView, editMenuForTextIn _: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { - .init(children: suggestedActions + [ - UIAction(title: "Insert Newline") { [weak self] _ in - self?.insertText("\n") - }, - ]) - } - - func textView(_: UITextView, shouldChangeTextIn _: NSRange, replacementText text: String) -> Bool { - if text == "\n" { - textDidReturn() - return false - } else { - return true - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/TextEditControlBanner.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/TextEditControlBanner.swift deleted file mode 100644 index 7b28e6ba4c..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/InputEditView/TextEditControlBanner.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// TextEditControlBanner.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -class TextEditControlBanner: UIStackView { - static let height: CGFloat = 32 - - let cameraButton = UIButton() - let photoButton = UIButton() - - let spacer = UIView() - - let sendButton = UIButton() - - init() { - super.init(frame: .zero) - - axis = .horizontal - spacing = 16 - alignment = .center - distribution = .fill - - [ - heightAnchor.constraint(equalToConstant: Self.height), - ].forEach { $0.isActive = true } - - [ - cameraButton, photoButton, - sendButton, - ].forEach { - $0.widthAnchor.constraint(equalToConstant: Self.height).isActive = true - $0.heightAnchor.constraint(equalToConstant: Self.height).isActive = true - } - - [ - cameraButton, photoButton, - spacer, - sendButton, - ].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - addArrangedSubview($0) - } - - cameraButton.setImage(.init(systemName: "camera"), for: .normal) - cameraButton.tintColor = .label - photoButton.setImage(.init(systemName: "photo"), for: .normal) - photoButton.tintColor = .label - - sendButton.setImage(.init(systemName: "paperplane.fill"), for: .normal) - sendButton.tintColor = .label - } - - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError() - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+Chat.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+Chat.swift deleted file mode 100644 index c61f548568..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+Chat.swift +++ /dev/null @@ -1,372 +0,0 @@ -// -// IntelligentsChatController+Chat.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/26. -// - -import AffineGraphQL -import LDSwiftEventSource -import MarkdownParser -import UIKit - -extension IntelligentsChatController { - @objc func chat_onLoad() { - beginProgress() - chat_createSession { session in - self.sessionID = session ?? "" - self.chat_retrieveHistories { - self.dispatchToMain { - self.endProgress() - } - } - } onFailure: { error in - self.presentError(error) { - if let nav = self.navigationController { - nav.popViewController(animated: true) - } else { - self.dismiss(animated: true) - } - } - } - } - - @objc func chat_onSend() { - beginProgress() - let viewModel = inputBox.editor.viewModel.duplicate() - viewModel.text = viewModel.text.trimmingCharacters(in: .whitespacesAndNewlines) - inputBox.editor.viewModel.reset() - inputBox.editor.updateValues() - DispatchQueue.global().async { - self.chat_onSendExecute(viewModel: viewModel) - self.endProgress() - } - } - - func chat_clearHistory() { - beginProgress() - Intelligents.qlClient.perform(mutation: CleanupCopilotSessionMutation(input: .init( - docId: metadata[.documentID] ?? "", - sessionIds: [sessionID], - workspaceId: metadata[.workspaceID] ?? "" - ))) { result in - self.dispatchToMain { - self.endProgress() - if case let .success(value) = result, - let sessions = value.data?.cleanupCopilotSession, - sessions.contains(self.sessionID) - { - self.simpleChatContents.removeAll() - return - } - self.presentError(UnableTo.clearHistory) - } - } - } - - func chat_retrieveHistories(_ completion: @escaping () -> Void) { - Intelligents.qlClient.fetch(query: GetCopilotHistoriesQuery( - workspaceId: metadata[.workspaceID] ?? "", - docId: .init(stringLiteral: metadata[.documentID] ?? ""), - options: .some(.init( - action: false, - fork: false, - limit: .init(nilLiteral: ()), - messageOrder: .some(.case(.asc)), - sessionId: .init(stringLiteral: sessionID), - sessionOrder: .some(.case(.desc)), - skip: .init(nilLiteral: ()), - withPrompt: .init(booleanLiteral: false) - )) - )) { [weak self] result in - if let self, - case let .success(value) = result, - let object = value.data, - let currentUser = object.__data._data["currentUser"] as? DataDict, - let copilot = currentUser._data["copilot"] as? DataDict, - let histories = copilot._data["histories"] as? [DataDict], - let mostRecent = histories.first, - let messages = mostRecent._data["messages"] as? [DataDict], - !messages.isEmpty - { - print("[*] retrieved \(messages.count) messages") - tableView.scrollToBottomOnNextUpdate = true - tableView.alpha = 0 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) { - self.tableView.alpha = 1 - } - } - for message in messages { - guard let role = message._data["role"] as? String, - let content = message._data["content"] as? String - // TODO: ATTACHMENTS - else { continue } - switch role { - case "assistant": - simpleChatContents.updateValue( - .assistant(document: content), - forKey: UUID() - ) - case "user": - simpleChatContents.updateValue( - .user(document: content), - forKey: UUID() - ) - default: - assertionFailure() - } - } - } - completion() - } - } -} - -private extension IntelligentsChatController { - func dispatchToMain(_ block: @escaping () -> Void) { - if Thread.isMainThread { - block() - } else { - DispatchQueue.main.async(execute: block) - } - } - - func beginProgress() { - dispatchToMain { [self] in - header.isUserInteractionEnabled = false - inputBox.isUserInteractionEnabled = false - progressView.isHidden = false - progressView.alpha = 0 - progressView.startAnimating() - UIView.animate(withDuration: 0.25) { - self.inputBox.editor.alpha = 0 - self.progressView.alpha = 1 - } - } - } - - func endProgress() { - dispatchToMain { [self] in - UIView.animate(withDuration: 0.3) { - self.inputBox.editor.alpha = 1 - self.progressView.alpha = 0 - self.header.isUserInteractionEnabled = true - } completion: { _ in - self.inputBox.isUserInteractionEnabled = true - self.progressView.stopAnimating() - } - } - } -} - -private extension IntelligentsChatController { - func chat_onError(_ error: Error) { - print("[*] chat error", error) - dispatchToMain { - let key = UUID() - let content = ChatContent.error(text: error.localizedDescription) - self.simpleChatContents.updateValue(content, forKey: key) - } - } - - func chat_createSession( - forceCreateNewSession: Bool = false, - onSuccess: @escaping (String?) -> Void, - onFailure: @escaping (Error) -> Void - ) { - if !forceCreateNewSession, - let doc = metadata[.documentID], - !doc.isEmpty - { - Intelligents.qlClient.fetch(query: GetCopilotSessionsQuery( - workspaceId: .init(stringLiteral: metadata[.workspaceID] ?? ""), - docId: .init(stringLiteral: doc), - options: .some(QueryChatSessionsInput(InputDict([ - "action": false, - ]))) - )) { result in - switch result { - case let .success(value): - if let result = value.data, - let currentUser = result.__data._data["currentUser"] as? DataDict, - let copilot = currentUser._data["copilot"] as? DataDict, - let sessions = copilot._data["sessions"] as? [DataDict], - let mostRecent = sessions.last, - let sessionID = mostRecent._data["id"] as? String - { - print("[*] using existing session", sessionID) - self.dispatchToMain { onSuccess(sessionID) } - return - } - self.chat_createSession( - forceCreateNewSession: true, - onSuccess: onSuccess, - onFailure: onFailure - ) - case let .failure(error): - self.dispatchToMain { onFailure(error) } - } - } - } - Intelligents.qlClient.perform( - mutation: CreateCopilotSessionMutation(options: .init( - docId: metadata[.documentID] ?? "", - promptName: Prompt.general_Chat_With_AFFiNE_AI.rawValue, - workspaceId: metadata[.workspaceID] ?? "" - )), - queue: .global() - ) { result in - switch result { - case let .success(value): - if let session = value.data?.createCopilotSession { - self.dispatchToMain { onSuccess(session) } - } else { - self.dispatchToMain { - onFailure(UnableTo.createSession) - } - } - case let .failure(error): - self.dispatchToMain { onFailure(error) } - } - } - } - - func chat_onSendExecute(viewModel: InputEditView.ViewModel) { - let text = viewModel.text - // let images = viewModel.attachments - - let assistantContentID = UUID() - dispatchToMain { - let content = ChatContent.user(document: text) - self.simpleChatContents.updateValue(content, forKey: .init()) - self.simpleChatContents.updateValue( - .assistant(document: "..."), - forKey: assistantContentID - ) - self.tableView.scrollToBottomOnNextUpdate = true - } - - let sem = DispatchSemaphore(value: 0) - let sessionID = sessionID - Intelligents.qlClient.perform( - mutation: CreateCopilotMessageMutation(options: .init( - content: .init(stringLiteral: text), - params: .some(.dictionary([ - "docs": [ - "docId": metadata[.documentID] ?? "", - "docContent": metadata[.content] ?? "", - ], - ])), - sessionId: sessionID - )), - queue: .global() - ) { result in - defer { sem.signal() } - switch result { - case let .success(value): - if let messageID = value.data?.createCopilotMessage { - print("[*] messageID", messageID) - self.chat_processWithMessageID( - sessionID: sessionID, - messageID: messageID, - cellID: assistantContentID - ) - } else { - self.chat_onError(UnableTo.createMessage) - } - case let .failure(error): - self.chat_onError(error) - } - } - - sem.wait() - } - - func chat_processWithMessageID(sessionID: String, messageID: String, cellID: UUID) { - let url = Constant.affineUpstreamURL - .appendingPathComponent("api") - .appendingPathComponent("copilot") - .appendingPathComponent("chat") - .appendingPathComponent(sessionID) - .appendingPathComponent("stream") - var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) - comps?.queryItems = [URLQueryItem(name: "messageId", value: messageID)] - - guard let url = comps?.url else { - assertionFailure() - chat_onError(UnableTo.createMessage) - return - } - - dispatchToMain { - self.simpleChatContents.updateValue( - .assistant(document: "..."), - forKey: cellID - ) - } - - let sem = DispatchSemaphore(value: 0) - - let eventHandler = BlockEventHandler() - eventHandler.onOpenedBlock = { - print("[*] chat opened") - } - eventHandler.onClosedBlock = { - sem.signal() - self.chatTask?.stop() - self.chatTask = nil - } - eventHandler.onErrorBlock = { error in - self.chat_onError(error) - } - - var document = "" - eventHandler.onMessageBlock = { _, message in - self.dispatchToMain { - document += message.data - let content = ChatContent.assistant(document: document) - self.simpleChatContents.updateValue(content, forKey: cellID) - } - } - let eventSource = EventSource(config: .init(handler: eventHandler, url: url)) - chatTask = eventSource - eventSource.start() - - sem.wait() - } -} - -extension IntelligentsChatController { - func updateContentToPublisher() { - assert(Thread.isMainThread) - let copy = simpleChatContents - let input: [MessageListView.Element] = copy.map { key, value in - switch value { - case let .assistant(document): - let nodes = MarkdownParser().feed(document) - return .init( - id: key, - cell: .assistant, - viewModel: MessageListView.AssistantCell.ViewModel(blocks: nodes), - object: nil - ) - case let .user(document): - return .init( - id: key, - cell: .user, - viewModel: MessageListView.UserCell.ViewModel(text: document), - object: nil - ) - case let .error(text): - return .init( - id: key, - cell: .hint, - viewModel: MessageListView.HintCell.ViewModel(hint: text), - object: nil - ) - } - } - publisher.send(input) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+Header.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+Header.swift deleted file mode 100644 index 203ac3da78..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+Header.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// IntelligentsChatController+Header.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -extension IntelligentsChatController { - class Header: UIView { - static let height: CGFloat = 44 - - let contentView = UIView() - let titleLabel = UILabel() - let dropMenu = UIButton() - let backButton = UIButton() - let rightBarItemsStack = UIStackView() - let moreMenu = UIButton() - - init() { - super.init(frame: .zero) - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - override var isUserInteractionEnabled: Bool { - didSet { updateAvailabilityStyles() } - } - - func updateAvailabilityStyles() { - if isUserInteractionEnabled { - backButton.isEnabled = true - dropMenu.isEnabled = true - moreMenu.isEnabled = true - } else { - backButton.isEnabled = false - dropMenu.isEnabled = false - moreMenu.isEnabled = false - } - } - - @objc func navigateActionBack() { - parentViewController?.dismissInContext() - } - } -} - -private extension IntelligentsChatController.Header { - func setupLayout() { - contentView.translatesAutoresizingMaskIntoConstraints = false - addSubview(contentView) - [ - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: bottomAnchor), - contentView.heightAnchor.constraint(equalToConstant: Self.height), - ].forEach { $0.isActive = true } - - titleLabel.textColor = .label - titleLabel.font = .systemFont( - ofSize: UIFont.labelFontSize, - weight: .semibold - ) - - backButton.setImage( - UIImage(systemName: "chevron.left"), - for: .normal - ) - backButton.tintColor = .accent - backButton.addTarget(self, action: #selector(navigateActionBack), for: .touchUpInside) - - dropMenu.setImage( - .init(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate), - for: .normal - ) - dropMenu.tintColor = .gray.withAlphaComponent(0.5) - - contentView.addSubview(titleLabel) - contentView.addSubview(backButton) - contentView.addSubview(dropMenu) - contentView.addSubview(rightBarItemsStack) - titleLabel.translatesAutoresizingMaskIntoConstraints = false - backButton.translatesAutoresizingMaskIntoConstraints = false - dropMenu.translatesAutoresizingMaskIntoConstraints = false - rightBarItemsStack.translatesAutoresizingMaskIntoConstraints = false - - rightBarItemsStack.axis = .horizontal - rightBarItemsStack.spacing = 10 - rightBarItemsStack.alignment = .center - rightBarItemsStack.distribution = .equalSpacing - - [ - backButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - backButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - backButton.widthAnchor.constraint(equalToConstant: 44), - backButton.heightAnchor.constraint(equalToConstant: 44), - - rightBarItemsStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - rightBarItemsStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - rightBarItemsStack.heightAnchor.constraint(equalToConstant: 44), - - titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: backButton.trailingAnchor, constant: 10), - - dropMenu.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - dropMenu.widthAnchor.constraint(equalToConstant: 44), - dropMenu.heightAnchor.constraint(equalToConstant: 44), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: dropMenu.leadingAnchor, constant: -10), - ].forEach { $0.isActive = true } - - rightBarItemsStack.addArrangedSubview(moreMenu) - moreMenu.setImage( - .init(systemName: "ellipsis.circle"), - for: .normal - ) - moreMenu.tintColor = .accent - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+InputBox.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+InputBox.swift deleted file mode 100644 index 298430a7ed..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController+InputBox.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// IntelligentsChatController+InputBox.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -extension IntelligentsChatController { - class InputBox: UIView { - let backgroundView = UIView() - let editor = InputEditView() - - init() { - super.init(frame: .zero) - - setupLayout() - - editor.textEditor.font = UIFont.systemFont(ofSize: UIFont.labelFontSize) - editor.placeholderText = "Summarize this article for me...".localized() - - backgroundView.backgroundColor = .init( - light: .init(white: 1, alpha: 1), - dark: .init(white: 0.15, alpha: 1) - ) - backgroundView.layer.cornerRadius = 16 - backgroundView.layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor - backgroundView.layer.shadowOffset = .init(width: 0, height: 0) - backgroundView.layer.shadowRadius = 8 - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - } -} - -private extension IntelligentsChatController.InputBox { - func setupLayout() { - addSubview(backgroundView) - backgroundView.translatesAutoresizingMaskIntoConstraints = false - - addSubview(editor) - editor.translatesAutoresizingMaskIntoConstraints = false - - let inset: CGFloat = 16 - - [ - editor.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), - editor.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), - editor.topAnchor.constraint(equalTo: topAnchor, constant: inset), - editor.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset), - ].forEach { $0.isActive = true } - - [ - backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), - backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0), - backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 0), - backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 128), - ].forEach { $0.isActive = true } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController.swift deleted file mode 100644 index 4a991cbfa9..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/IntelligentsChatController.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// IntelligentsChatController.swift -// -// -// Created by 秋星桥 on 2024/11/18. -// - -import Combine -import LDSwiftEventSource -import OrderedCollections -import UIKit - -public class IntelligentsChatController: UIViewController { - let header = Header() - let inputBox = InputBox() - let progressView = UIActivityIndicatorView() - - let publisher = PassthroughSubject() - lazy var tableView = MessageListView(dataPublisher: publisher.eraseToAnyPublisher()) - - var inputBoxKeyboardAdapterHeightConstraint = NSLayoutConstraint() - - enum ChatContent { - case user(document: String) - case assistant(document: String) - case error(text: String) - } - - var simpleChatContents: OrderedDictionary = [:] { - didSet { updateContentToPublisher() } - } - - var sessionID: String = "" - - public enum MetadataKey: String { - case documentID - case workspaceID - case content - } - - public var metadata: [MetadataKey: String] = [:] - - var chatTask: EventSource? - - override public var title: String? { - set { - super.title = newValue - header.titleLabel.text = newValue - } - get { - super.title - } - } - - public init() { - super.init(nibName: nil, bundle: nil) - title = "Chat with AI".localized() - - overrideUserInterfaceStyle = .dark - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - deinit { - chatTask?.stop() - chatTask = nil - } - - override public func viewDidLoad() { - super.viewDidLoad() - assert(navigationController != nil) - view.backgroundColor = .secondarySystemBackground - - hideKeyboardWhenTappedAround() - - view.addSubview(header) - view.addSubview(tableView) - view.addSubview(inputBox) - view.addSubview(progressView) - setupLayout() - - header.moreMenu.showsMenuAsPrimaryAction = true - header.moreMenu.menu = .init(children: [ - UIAction(title: "Clear History".localized(), image: UIImage(systemName: "eraser")) { [weak self] _ in - self?.chat_clearHistory() - }, - ]) - - // TODO: IMPL - header.dropMenu.isHidden = true - inputBox.editor.controlBanner.cameraButton.isHidden = true - inputBox.editor.controlBanner.photoButton.isHidden = true - - updateContentToPublisher() - chat_onLoad() - } - - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - chatTask?.stop() - chatTask = nil - } - - func setupLayout() { - header.translatesAutoresizingMaskIntoConstraints = false - [ - header.topAnchor.constraint(equalTo: view.topAnchor), - header.leadingAnchor.constraint(equalTo: view.leadingAnchor), - header.trailingAnchor.constraint(equalTo: view.trailingAnchor), - header.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44), - ].forEach { $0.isActive = true } - - inputBox.translatesAutoresizingMaskIntoConstraints = false - [ - inputBox.leadingAnchor.constraint(equalTo: view.leadingAnchor), - inputBox.trailingAnchor.constraint(equalTo: view.trailingAnchor), - inputBox.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), - ].forEach { $0.isActive = true } - - tableView.translatesAutoresizingMaskIntoConstraints = false - [ - tableView.topAnchor.constraint(equalTo: header.bottomAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: inputBox.topAnchor), - ].forEach { $0.isActive = true } - - inputBox.editor.controlBanner.sendButton.addTarget( - self, - action: #selector(chat_onSend), - for: .touchUpInside - ) - inputBox.editor.submitAction = { [weak self] in - guard let self else { return } - chat_onSend() - } - - progressView.hidesWhenStopped = true - progressView.stopAnimating() - progressView.translatesAutoresizingMaskIntoConstraints = false - [ - progressView.centerXAnchor.constraint(equalTo: inputBox.centerXAnchor), - progressView.centerYAnchor.constraint(equalTo: inputBox.centerYAnchor), - ].forEach { $0.isActive = true } - progressView.style = .large - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+AssistantCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+AssistantCell.swift deleted file mode 100644 index 4eb2b4012c..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+AssistantCell.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// MessageListView+AssistantCell.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import MarkdownParser -import MarkdownView -import UIKit - -extension MessageListView { - class AssistantCell: BaseCell { - let avatarView = UIImageView() - let usernameView = UILabel() - let markdownView = MarkdownView() - - override func initializeContent() { - super.initializeContent() - - avatarView.contentMode = .scaleAspectFit - avatarView.image = UIImage(named: "spark", in: .module, with: nil) - usernameView.text = "AFFiNE AI" - usernameView.font = .preferredFont(forTextStyle: .body).bold - usernameView.textColor = .label - - containerView.addSubview(avatarView) - containerView.addSubview(usernameView) - containerView.addSubview(markdownView) - } - - override func prepareForReuse() { - super.prepareForReuse() - markdownView.prepareForReuse() - } - - override func updateContent( - object: any MessageListView.Element.ViewModel, - originalObject _: Element.UserObject? - ) { - guard let object = object as? ViewModel else { - assertionFailure() - return - } - _ = object - } - - override func layoutContent(cache: any MessageListView.TableLayoutEngine.LayoutCache) { - super.layoutContent(cache: cache) - guard let cache = cache as? LayoutCache else { - assertionFailure() - return - } - avatarView.frame = cache.avatarRect - usernameView.frame = cache.usernameRect - markdownView.frame = cache.markdownFrame - - UIView.performWithoutAnimation { - markdownView.updateContentViews(cache.manifests) - } - } - - override class func layoutInsideContainer( - containerWidth: CGFloat, - object: any MessageListView.Element.ViewModel - ) -> any MessageListView.TableLayoutEngine.LayoutCache { - guard let object = object as? ViewModel else { - assertionFailure() - return LayoutCache() - } - let cache = LayoutCache() - cache.width = containerWidth - - let inset: CGFloat = 8 - let bubbleInset = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) - - let avatarRect = CGRect(x: bubbleInset.left, y: bubbleInset.top, width: 24, height: 24) - let usernameRect = CGRect( - x: avatarRect.maxX + bubbleInset.right, - y: bubbleInset.top, - width: containerWidth - avatarRect.maxX - bubbleInset.right, - height: 24 - ) - - let textWidth = containerWidth - bubbleInset.left - bubbleInset.right - - var height: CGFloat = 0 - let manifests = object.blocks.map { - let ret = $0.manifest(theme: object.theme) - ret.setLayoutTheme(.default) - ret.setLayoutWidth(textWidth) - ret.layoutIfNeeded() - height += ret.size.height + Theme.default.spacings.final - return ret - } - if height > 0 { height -= Theme.default.spacings.final } - let textRect = CGRect( - x: bubbleInset.left, - y: usernameRect.maxY + bubbleInset.bottom, - width: textWidth, - height: height - ) - cache.markdownFrame = textRect - cache.avatarRect = avatarRect - cache.usernameRect = usernameRect - cache.manifests = manifests - cache.height = textRect.maxY + bubbleInset.bottom - - return cache - } - } -} - -extension MessageListView.AssistantCell { - class ViewModel: MessageListView.Element.ViewModel { - var theme: Theme - var blocks: [BlockNode] - - enum GroupLocation { - case begin - case center - case end - } - - var groupLocation: GroupLocation = .center - - init(theme: Theme = .default, blocks: [BlockNode]) { - self.theme = theme - self.blocks = blocks - } - - func contentIdentifier(hasher: inout Hasher) { - hasher.combine(blocks) - } - } -} - -extension MessageListView.AssistantCell { - class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache { - var width: CGFloat = 0 - var height: CGFloat = 0 - - var avatarRect: CGRect = .zero - var usernameRect: CGRect = .zero - - var markdownFrame: CGRect = .zero - var manifests: [AnyBlockManifest] = [] - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+BaseCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+BaseCell.swift deleted file mode 100644 index ea84f27705..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+BaseCell.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// MessageListView+BaseCell.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import UIKit - -extension MessageListView { - class BaseCell: UITableViewCell, MessageListView.TableLayoutEngine.LayoutableCell { - var associatedObject: Element? = nil - var cancellable: Set = [] - let containerView: UIView = .init() - - var layoutEngine: MessageListView.TableLayoutEngine? = nil - - func layoutCache() -> MessageListView.TableLayoutEngine.LayoutCache { - guard let associatedObject, let engine = layoutEngine else { - return MessageListView.TableLayoutEngine.ZeroLayoutCache() - } - let cache = engine.requestLayoutCacheFromCell( - forElement: associatedObject, - atWidth: bounds.width - ) - return cache - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - commitInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commitInit() - } - - private func commitInit() { - selectionStyle = .none - separatorInset = .zero - contentView.addSubview(containerView) - contentView.clipsToBounds = false - clipsToBounds = false - initializeContent() - } - - override func layoutSubviews() { - super.layoutSubviews() - guard let cache = layoutCache() as? LayoutCache else { - assertionFailure() - return - } - containerView.frame = cache.containerRect - layoutContent(cache: cache.containerLayoutCache) - } - - func registerViewModel(element: Element) { - removeViewModelObject() - associatedObject = element - updateContent(object: element.viewModel, originalObject: element.object) - setNeedsLayout() - } - - func removeViewModelObject() { - associatedObject = nil - cancellable.forEach { $0.cancel() } - cancellable.removeAll() - } - - func initializeContent() {} - func updateContent(object: any Element.ViewModel, originalObject: Element.UserObject?) { - _ = object - _ = originalObject - } - - func layoutContent(cache: MessageListView.TableLayoutEngine.LayoutCache) { - _ = cache - } - - class func layoutInsideContainer( - containerWidth: CGFloat, - object: any Element.ViewModel - ) -> MessageListView.TableLayoutEngine.LayoutCache { - _ = containerWidth - _ = object - assertionFailure("must override") - return MessageListView.TableLayoutEngine.ZeroLayoutCache() - } - - class func containerInset() -> UIEdgeInsets { - let inset: CGFloat = 16 - let containerInset = UIEdgeInsets(top: inset / 2, left: inset, bottom: inset / 2, right: inset) - return containerInset - } - } -} - -extension MessageListView.BaseCell { - class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache { - var width: CGFloat - var height: CGFloat - var containerRect: CGRect - var containerLayoutCache: any MessageListView.TableLayoutEngine.LayoutCache - - init( - width: CGFloat, - height: CGFloat, - containerRect: CGRect, - containerLayoutCache: any MessageListView.TableLayoutEngine.LayoutCache - ) { - self.width = width - self.height = height - self.containerRect = containerRect - self.containerLayoutCache = containerLayoutCache - } - } - - class func resolveLayout( - dataElement element: MessageListView.Element, - contentWidth width: CGFloat - ) -> any MessageListView.TableLayoutEngine.LayoutCache { - let object = element.viewModel - let containerInset = MessageListView.BaseCell.containerInset() - let containerWidth = width - containerInset.left - containerInset.right - let containerCache = Self.layoutInsideContainer(containerWidth: containerWidth, object: object) - let cellHeight = containerCache.height + containerInset.top + containerInset.bottom - let containerRect = CGRect( - x: containerInset.left, - y: containerInset.top, - width: containerWidth, - height: containerCache.height - ) - let cache = LayoutCache( - width: width, - height: cellHeight, - containerRect: containerRect, - containerLayoutCache: containerCache - ) - return cache - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+HintCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+HintCell.swift deleted file mode 100644 index c21bdbeb30..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+HintCell.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// MessageListView+HintCell.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import UIKit - -extension MessageListView { - class HintCell: BaseCell { - let label = UILabel() - - override func initializeContent() { - super.initializeContent() - label.font = .preferredFont(forTextStyle: .footnote) - label.alpha = 0.5 - label.numberOfLines = 0 - containerView.addSubview(label) - } - - override func updateContent( - object: any MessageListView.Element.ViewModel, - originalObject: Element.UserObject? - ) { - super.updateContent(object: object, originalObject: originalObject) - guard let object = object as? ViewModel else { return } - label.attributedText = object.hint - } - - override func layoutContent(cache: any MessageListView.TableLayoutEngine.LayoutCache) { - super.layoutContent(cache: cache) - guard let cache = cache as? LayoutCache else { - assertionFailure() - return - } - label.frame = cache.labelFrame - } - - override class func layoutInsideContainer( - containerWidth: CGFloat, - object: any MessageListView.Element.ViewModel - ) -> any MessageListView.TableLayoutEngine.LayoutCache { - guard let object = object as? ViewModel else { - assertionFailure() - return LayoutCache() - } - let cache = LayoutCache() - cache.width = containerWidth - cache.height = object.hint.measureHeight(usingWidth: containerWidth) - cache.labelFrame = .init(x: 0, y: 0, width: containerWidth, height: cache.height) - return cache - } - } -} - -extension MessageListView.HintCell { - class ViewModel: MessageListView.Element.ViewModel { - var hint: NSAttributedString = .init() - - init(hint: NSAttributedString) { - self.hint = hint - } - - convenience init(hint: String) { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.preferredFont(forTextStyle: .footnote), - .originalFont: UIFont.preferredFont(forTextStyle: .footnote), - .foregroundColor: UIColor.label, - .paragraphStyle: paragraphStyle, - ] - let text = NSMutableAttributedString(string: hint, attributes: attributes) - self.init(hint: text) - } - - func contentIdentifier(hasher: inout Hasher) { - hasher.combine(hint) - } - } -} - -extension MessageListView.HintCell { - class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache { - var width: CGFloat = 0 - var height: CGFloat = 0 - - var labelFrame: CGRect = .zero - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+SpacerCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+SpacerCell.swift deleted file mode 100644 index 370a9fea0a..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+SpacerCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// MessageListView+SpacerCell.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/12. -// - -import Combine -import UIKit - -extension MessageListView { - class SpacerCell: BaseCell { - override class func layoutInsideContainer( - containerWidth: CGFloat, - object: any MessageListView.Element.ViewModel - ) -> any MessageListView.TableLayoutEngine.LayoutCache { - guard let object = object as? ViewModel else { - assertionFailure() - return LayoutCache() - } - let cache = LayoutCache() - cache.width = containerWidth - cache.height = object.height - return cache - } - } -} - -extension MessageListView.SpacerCell { - class ViewModel: MessageListView.Element.ViewModel { - var height: CGFloat - init(height: CGFloat) { - self.height = height - } - - func contentIdentifier(hasher: inout Hasher) { - hasher.combine(height) - } - } -} - -extension MessageListView.SpacerCell { - class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache { - var width: CGFloat = 0 - var height: CGFloat = 0 - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+UserCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+UserCell.swift deleted file mode 100644 index bc494d6f63..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/Cells/MessageListView+UserCell.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// MessageListView+UserCell.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import UIKit - -extension MessageListView { - class UserCell: BaseCell { - let avatarView = UIImageView() - let usernameView = UILabel() - let bubbleView = UIView() - let textView = UITextView() - - override func initializeContent() { - super.initializeContent() - - textView.isSelectable = true - textView.isScrollEnabled = true - textView.isEditable = false - textView.showsVerticalScrollIndicator = false - textView.showsHorizontalScrollIndicator = false - textView.textColor = .label - textView.textContainer.lineFragmentPadding = .zero - textView.textAlignment = .natural - textView.backgroundColor = .clear - textView.textContainerInset = .zero - textView.textContainer.lineBreakMode = .byTruncatingTail - - avatarView.contentMode = .scaleAspectFit - avatarView.image = UIImage(systemName: "person.fill") - usernameView.text = "You" - usernameView.font = .preferredFont(forTextStyle: .body).bold - usernameView.textColor = .label - - bubbleView.layer.cornerRadius = 8 - bubbleView.backgroundColor = .gray.withAlphaComponent(0.1) - - containerView.addSubview(bubbleView) - containerView.addSubview(avatarView) - containerView.addSubview(usernameView) - containerView.addSubview(textView) - } - - override func updateContent( - object: any MessageListView.Element.ViewModel, - originalObject: Element.UserObject? - ) { - super.updateContent(object: object, originalObject: originalObject) - guard let object = object as? ViewModel else { - assertionFailure() - return - } - textView.attributedText = object.text - } - - override func layoutContent(cache: any MessageListView.TableLayoutEngine.LayoutCache) { - super.layoutContent(cache: cache) - guard let cache = cache as? LayoutCache else { - assertionFailure() - return - } - bubbleView.frame = cache.bubbleFrame - avatarView.frame = cache.avatarFrame - usernameView.frame = cache.usernameFrame - textView.frame = cache.labelFrame - } - - override class func layoutInsideContainer( - containerWidth: CGFloat, - object: any MessageListView.Element.ViewModel - ) -> any MessageListView.TableLayoutEngine.LayoutCache { - guard let object = object as? ViewModel else { - assertionFailure() - return LayoutCache() - } - let cache = LayoutCache() - cache.width = containerWidth - - let inset: CGFloat = 8 - let bubbleInset = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) - - let avatarRect = CGRect( - x: bubbleInset.left, - y: bubbleInset.top, - width: 24, - height: 24 - ) - let usernameFrame = CGRect( - x: avatarRect.maxX + inset, - y: bubbleInset.top, - width: containerWidth - avatarRect.maxX - bubbleInset.right, - height: 24 - ) - - let textWidth = min( - object.text.measureWidth(), - containerWidth - inset * 2 - ) - let textHeight = object.text.measureHeight(usingWidth: textWidth) - let textRect = CGRect( - x: bubbleInset.left, - y: avatarRect.maxY + bubbleInset.top, - width: textWidth, - height: textHeight - ) - let bubbleRect = CGRect( - x: 0, - y: 0, - width: containerWidth, - height: textRect.maxY + bubbleInset.bottom - ) - cache.bubbleFrame = bubbleRect - cache.avatarFrame = avatarRect - cache.usernameFrame = usernameFrame - cache.labelFrame = textRect - cache.height = bubbleRect.maxY - return cache - } - } -} - -extension MessageListView.UserCell { - class ViewModel: MessageListView.Element.ViewModel { - var text: NSAttributedString = .init() - - init(text: NSAttributedString) { - self.text = text - } - - convenience init(text: String) { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .natural - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.preferredFont(forTextStyle: .body), - .originalFont: UIFont.preferredFont(forTextStyle: .body), - .foregroundColor: UIColor.label, - .paragraphStyle: paragraphStyle, - ] - var text = text - while text.contains("\n\n\n") { - text = text.replacingOccurrences(of: "\n\n\n", with: "\n\n") - } - print(text) - self.init(text: NSMutableAttributedString(string: text, attributes: attributes)) - } - - func contentIdentifier(hasher: inout Hasher) { - hasher.combine(text) - } - } -} - -extension MessageListView.UserCell { - class LayoutCache: MessageListView.TableLayoutEngine.LayoutCache { - var width: CGFloat = 0 - var height: CGFloat = 0 - - var bubbleFrame: CGRect = .zero - var labelFrame: CGRect = .zero - var avatarFrame: CGRect = .zero - var usernameFrame: CGRect = .zero - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+DataElement.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+DataElement.swift deleted file mode 100644 index b5a3a750ae..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+DataElement.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// MessageListView+DataElement.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import Foundation -import UIKit - -extension MessageListView { - struct Element: Identifiable { - let id: AnyHashable // equals to message id if applicable - - enum Cell: String, CaseIterable { - case base - case hint - case user - case assistant - case spacer - } - - let cell: Cell - let viewModel: any ViewModel - - typealias UserObject = any(Identifiable & Hashable) - let object: UserObject? - - init(id: AnyHashable, cell: Cell, viewModel: any ViewModel, object: UserObject?) { - assert(cell != .base) - self.id = id - self.cell = cell - self.viewModel = viewModel - self.object = object - } - } -} - -extension MessageListView.Element.Cell { - var cellClass: MessageListView.BaseCell.Type { - switch self { - case .base: - MessageListView.BaseCell.self - case .hint: - MessageListView.HintCell.self - case .user: - MessageListView.UserCell.self - case .assistant: - MessageListView.AssistantCell.self - case .spacer: - MessageListView.SpacerCell.self - } - } -} - -extension MessageListView.Element { - protocol ViewModel { - func contentIdentifier(hasher: inout Hasher) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+Delegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+Delegate.swift deleted file mode 100644 index dae48e3888..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+Delegate.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// MessageListView+Delegate.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/6. -// - -import UIKit - -extension MessageListView: UITableViewDelegate, UITableViewDataSource { - func item(forIndexPath indexPath: IndexPath) -> Element? { - guard indexPath.row < elements.count else { - return nil - } - guard indexPath.row >= 0 else { - return nil - } - return elements.values[indexPath.row] - } - - func numberOfSections(in _: UITableView) -> Int { - 1 - } - - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - elements.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let item = item(forIndexPath: indexPath) else { - assertionFailure() - return UITableViewCell() - } - let cell = tableView.dequeueReusableCell(withIdentifier: item.cell.rawValue, for: indexPath) - if let cell = cell as? BaseCell { - cell.layoutEngine = layoutEngine - cell.registerViewModel(element: item) - } - cell.backgroundColor = .clear - return cell - } - - func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let item = item(forIndexPath: indexPath) else { - return 0 - } - if let height = layoutEngine.height(forElement: item) { - heightKeeper[item.id] = height - return height - } - let ret = layoutEngine.resolveLayoutNow(item).height - heightKeeper[item.id] = ret - return ret - } - - func tableView(_: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - guard let item = item(forIndexPath: indexPath) else { - return 0 - } - if let height = layoutEngine.height(forElement: item) { - return height - } - if let height = heightKeeper[item.id] { - return height - } - return UITableView.automaticDimension - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+LayoutEngine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+LayoutEngine.swift deleted file mode 100644 index f2877e4f98..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+LayoutEngine.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// MessageListView+LayoutEngine.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Foundation - -extension MessageListView { - class TableLayoutEngine { - private let lock = NSLock() - struct LayoutCacheBox { - var cache: LayoutCache - var contentIdentifier: Int - } - - private var layoutCache: [Element.ID: LayoutCacheBox] = [:] - private(set) var contentWidth: CGFloat = .zero - var layoutSession: UUID = .init() - - func setContentWidth(_ width: CGFloat) { - accessLayoutCache { _ in - contentWidth = width - layoutSession = .init() - } - } - - func createSession() -> UUID { - let session = UUID() - layoutSession = session - return session - } - - @discardableResult - func accessLayoutCache(_ block: (inout [Element.ID: LayoutCacheBox]) -> T) -> T { - lock.lock() - defer { lock.unlock() } - return block(&layoutCache) - } - - func contentIdentifier(forElement dataElement: Element) -> Int? { - accessLayoutCache { pool in - guard let box = pool[dataElement.id] else { return nil } - return box.contentIdentifier - } - } - } - - func viewCallingUpdateLayoutEngineWidth() { - guard layoutEngine.contentWidth != tableView.bounds.width else { return } - layoutEngine.setContentWidth(tableView.bounds.width) - reconfigure(enforceReload: false) - NSObject.cancelPreviousPerformRequests( - withTarget: self, - selector: #selector(resolveAllLayoutInBackground), - object: nil - ) - perform(#selector(resolveAllLayoutInBackground), with: nil, afterDelay: 0.1) - } - - @objc private func resolveAllLayoutInBackground() { - let items = Array(elements.values) - let date = Date() - DispatchQueue.global().async { - let session = self.layoutEngine.createSession() - for element in items { - guard self.layoutEngine.layoutSession == session else { continue } - self.layoutEngine.resolveLayoutNow(element) - } - DispatchQueue.main.async { - self.reconfigure(enforceReload: true) - print("[*] layout engine updated \(items.count) items in \(Date().timeIntervalSince(date)) seconds") - } - } - } -} - -extension MessageListView.TableLayoutEngine { - protocol LayoutableCell: AnyObject { - static func resolveLayout( - dataElement: MessageListView.Element, - contentWidth: CGFloat - ) -> LayoutCache - } - - protocol LayoutCache: AnyObject { - var width: CGFloat { get } - var height: CGFloat { get } - } - - class ZeroLayoutCache: LayoutCache { - var width: CGFloat = 0 - var height: CGFloat = 0 - } -} - -extension MessageListView.TableLayoutEngine { - @discardableResult - func resolveLayoutNow(_ element: MessageListView.Element) -> LayoutCache { - var hasher = Hasher() - element.viewModel.contentIdentifier(hasher: &hasher) - let contentIdentifier = hasher.finalize() - - if let cacheBox = accessLayoutCache({ $0[element.id] }) { - if cacheBox.cache.width == contentWidth, - cacheBox.contentIdentifier == contentIdentifier - { return cacheBox.cache } - } - - let target = element.cell.cellClass.self - let cache = target.resolveLayout(dataElement: element, contentWidth: contentWidth) - let cacheBox = LayoutCacheBox(cache: cache, contentIdentifier: contentIdentifier) - accessLayoutCache { $0[element.id] = cacheBox } - return cache - } - - func requestLayoutCacheFromCell( - forElement dataElement: MessageListView.Element, - atWidth width: CGFloat - ) -> LayoutCache { - let cache = accessLayoutCache { pool -> LayoutCache? in - guard let box = pool[dataElement.id] else { return nil } - guard box.contentIdentifier == dataElement.object?.hashValue else { return nil } - guard box.cache.width == contentWidth else { return nil } - guard box.cache.width == width else { return nil } - return box.cache - } - if let cache { return cache } - return resolveLayoutNow(dataElement) - } -} - -extension MessageListView.TableLayoutEngine { - func height(forElement dataElement: MessageListView.Element) -> CGFloat? { - accessLayoutCache { pool in - guard let box = pool[dataElement.id] else { return nil } - guard box.contentIdentifier == dataElement.object?.hashValue else { return nil } - guard box.cache.width == contentWidth else { return nil } - return box.cache.height - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+Update.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+Update.swift deleted file mode 100644 index 315e8cb3de..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView+Update.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// MessageListView+Update.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import Foundation -import OrderedCollections -import UIKit - -extension MessageListView { - func setupPublishers(dataPublisher: AnyPublisher<[Element], Never>) { - // process input from data source where we transform those to view model - let publisher = dataPublisher - .map { input -> [Element] in input + [Element( - id: "spacer", - cell: .spacer, - viewModel: MessageListView.SpacerCell.ViewModel(height: 32), - object: nil - )] } - .map { output in - OrderedDictionary( - uniqueKeysWithValues: output.map { ($0.id, $0) } - ) - } - .eraseToAnyPublisher() - - // after so, limit the refresh rate so we can handle them better - let updateQueue = DispatchQueue(label: "affine.message-list-update-queue", qos: .userInteractive) - let inQueuePublisher = publisher - .throttle(for: .seconds(1 / 5), scheduler: updateQueue, latest: true) - .eraseToAnyPublisher() - - // finally before sending to display, call layout engine to process those items - inQueuePublisher - .sink { [weak self] output in self?.prepare(forNewElements: output) } - .store(in: &cancellables) - } - - func prepare(forNewElements elements: Elements) { - print("[*] received \(elements.count) for update at \(Date())") - elementUpdateProcessLock.lock() - distributedPendingUpdateElements = elements - elementUpdateProcessLock.unlock() - performSelector(onMainThread: #selector(elementsUpdateExecute), with: nil, waitUntilDone: false) - } - - private func pickupElementsPair() -> (oldValue: Elements, newValue: Elements)? { - #if DEBUG // just make sure assert is not called in release mode - assert(!elementUpdateProcessLock.try(), "should not call this method without lock") - #endif - guard let distributedPendingUpdateElements else { return nil } - - let oldValue = elements - elements = distributedPendingUpdateElements - self.distributedPendingUpdateElements = nil - print("[*] pikup is sending \(elements.count) for update at \(Date())") - return (oldValue, distributedPendingUpdateElements) - } - - @objc private func elementsUpdateExecute() { - assert(Thread.isMainThread) - elementUpdateProcessLock.lock() - defer { elementUpdateProcessLock.unlock() } - let pickup = pickupElementsPair() - guard let (oldValue, newValue) = pickup else { return } - guard window != nil else { return } - for value in heightKeeper.keys where !newValue.keys.contains(value) { - heightKeeper.removeValue(forKey: value) - } - - let shouldRealodTableView = newValue.count != oldValue.count - let contentOffset = tableView.contentOffset - UIView.performWithoutAnimation { - self.reconfigure(enforceReload: shouldRealodTableView) - self.tableView.layoutIfNeeded() - } - tableView.contentOffset = contentOffset - - if scrollToBottomOnNextUpdate { - scrollToBottomOnNextUpdate = false - scrollToBottom(useTableViewAnimation: false) - } - } - - func reconfigure(enforceReload: Bool) { - if enforceReload || tableView(tableView, numberOfRowsInSection: 0) != elements.count { - tableView.reloadData() - return - } - var requiresReload = [IndexPath]() - for indexPath in tableView.indexPathsForVisibleRows ?? [] { - guard let item = item(forIndexPath: indexPath) else { continue } - guard let cell = tableView.cellForRow(at: indexPath) as? BaseCell else { continue } - guard type(of: cell) == item.cell.cellClass else { - requiresReload.append(indexPath) - continue - } - layoutEngine.resolveLayoutNow(item) - cell.registerViewModel(element: item) - } - tableView.beginUpdates() - tableView.reloadRows(at: requiresReload, with: .none) - tableView.endUpdates() - } -} - -extension MessageListView { - func scrollToBottom(useTableViewAnimation: Bool = false) { - guard elements.count > 0 else { return } - guard tableView.contentSize.height > tableView.frame.height else { return } - let targetIndexPath = IndexPath(row: elements.count - 1, section: 0) - let cellRect = tableView.rectForRow(at: targetIndexPath) - if tableView.contentOffset.y + tableView.frame.height >= cellRect.origin.y + cellRect.height { return } - UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) { - self.tableView.scrollToRow( - at: targetIndexPath, - at: .bottom, - animated: useTableViewAnimation - ) - self.tableView.layoutIfNeeded() - } - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(finishAutomaticScroll), object: nil) - isAutomaticScrollAnimating = true - perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5) - } - -// func scrollLastCellToTop(useTableViewAnimation: Bool = false) { -// guard elements.count > 1 else { return } -// guard tableView.contentSize.height > tableView.frame.height else { return } -// UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) { -// self.tableView.scrollToRow( -// at: IndexPath(row: self.elements.count - 1, section: 0), -// at: .top, -// animated: useTableViewAnimation -// ) -// } -// NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(finishAutomaticScroll), object: nil) -// isAutomaticScrollAnimating = true -// perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5) -// } - - @objc private func finishAutomaticScroll() { - isAutomaticScrollAnimating = false - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView.swift deleted file mode 100644 index 045596f197..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsChatController/MessageListView/TableView/MessageListView.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// MessageListView.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import Combine -import OrderedCollections -import UIKit - -class MessageListView: UIView { - typealias ElementPublisher = AnyPublisher<[Element], Never> - typealias Elements = OrderedDictionary - var elements: Elements = .init() - - var cancellables: Set = [] - - let tableView: UITableView = .init(frame: .zero, style: .plain) - - let layoutEngine = TableLayoutEngine() - var heightKeeper: [Element.ID: CGFloat] = [:] - let elementUpdateProcessLock = NSLock() - var distributedPendingUpdateElements: Elements? = nil - var isAutomaticScrollAnimating: Bool = false - var scrollToBottomOnNextUpdate = false - - let footerView = UIView(frame: .init(x: 0, y: 0, width: 0, height: 200)) - - init(dataPublisher: AnyPublisher<[Element], Never>) { - super.init(frame: .zero) - - tableView.delegate = self - tableView.dataSource = self - tableView.allowsSelection = false - tableView.allowsMultipleSelection = false - tableView.allowsFocus = false - tableView.selectionFollowsFocus = true - tableView.separatorColor = .clear - tableView.backgroundColor = .clear - for cellIdentifier in Element.Cell.allCases { - tableView.register(cellIdentifier.cellClass, forCellReuseIdentifier: cellIdentifier.rawValue) - } - addSubview(tableView) - - tableView.tableFooterView = footerView - tableView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: topAnchor), - tableView.bottomAnchor.constraint(equalTo: bottomAnchor), - tableView.leadingAnchor.constraint(equalTo: leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - - setupPublishers(dataPublisher: dataPublisher) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - deinit { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - } - - override func layoutSubviews() { - super.layoutSubviews() - viewCallingUpdateLayoutEngineWidth() - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/EphemeralAction.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/EphemeralAction.swift deleted file mode 100644 index f9562f9b59..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/EphemeralAction.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// EphemeralAction.swift -// Intelligents -// -// Created by 秋星桥 on 2025/1/8. -// - -import Foundation - -public extension IntelligentsEphemeralActionController { - enum EphemeralAction { - public enum Language: String, CaseIterable { - case langEnglish = "English" - case langSpanish = "Spanish" - case langGerman = "German" - case langFrench = "French" - case langItalian = "Italian" - case langSimplifiedChinese = "Simplified Chinese" - case langTraditionalChinese = "Traditional Chinese" - case langJapanese = "Japanese" - case langRussian = "Russian" - case langKorean = "Korean" - } - - case translate(to: Language) - case summarize - } -} - -extension IntelligentsEphemeralActionController.EphemeralAction { - var title: String { - switch self { - case let .translate(to): - String(format: NSLocalizedString("Translate to %@", comment: ""), to.rawValue) - case .summarize: - NSLocalizedString("Summarize", comment: "") - } - } - - var prompt: Prompt { - switch self { - case .translate: - .general_Translate_to - case .summarize: - .general_Summary - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/ImageRotatedPreview.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/ImageRotatedPreview.swift deleted file mode 100644 index 9ba608100f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/ImageRotatedPreview.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// ImageRotatedPreview.swift -// Intelligents -// -// Created by 秋星桥 on 2025/1/8. -// - -import UIKit - -public class RotatedImagePreview: UIView { - let imageView = UIImageView() - let rotationDegree: CGFloat = 5 - - public init() { - super.init(frame: .zero) - imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 16 - imageView.clipsToBounds = true - addSubview(imageView) - - clipsToBounds = false - - heightAnchor.constraint(equalToConstant: 300).isActive = true - imageView.transform = CGAffineTransform(rotationAngle: rotationDegree * CGFloat.pi / 180) - } - - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError() - } - - public func configure(previewImage: UIImage) { - imageView.image = previewImage - setNeedsLayout() - } - - override public func layoutSubviews() { - super.layoutSubviews() - - guard let image = imageView.image else { - imageView.frame = .zero - return - } - - let viewHeight = bounds.height // limiter - guard bounds.height > 0 else { return } - - // fit in side - let imageAspectRatio = image.size.width / image.size.height - let imageHeight = viewHeight - let imageWidth = imageHeight * imageAspectRatio - - imageView.frame = CGRect( - x: (bounds.width - imageWidth) / 2, - y: (bounds.height - imageHeight) / 2, - width: imageWidth, - height: imageHeight - ) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+API.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+API.swift deleted file mode 100644 index fae017aff2..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+API.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// IntelligentsEphemeralActionController+API.swift -// Intelligents -// -// Created by 秋星桥 on 2025/1/15. -// - -import AffineGraphQL -import Foundation -import LDSwiftEventSource - -extension IntelligentsEphemeralActionController { - func beginAction() { - print("[*] begin ephemeral action for did \(documentID) wid \(workspaceID)") - chatTask?.stop() - chatTask = nil - copilotDocumentStorage = "" - sessionID = "" - messageID = "" - chat_createSession( - documentIdentifier: documentID, - workspaceIdentifier: workspaceID - ) { session in - self.sessionID = session - self.beginThisRound() - } onFailure: { error in - self.presentError(error) { - self.close() - } - } - } - - func chat_createSession( - documentIdentifier: String, - workspaceIdentifier: String, - onSuccess: @escaping (String) -> Void, - onFailure: @escaping (Error) -> Void - ) { - if documentIdentifier.isEmpty || workspaceIdentifier.isEmpty { - onFailure(UnableTo.identifyDocumentOrWorkspace) - } - Intelligents.qlClient.perform( - mutation: CreateCopilotSessionMutation(options: .init( - docId: documentIdentifier, - promptName: action.prompt.rawValue, - workspaceId: workspaceIdentifier - )), - queue: .global() - ) { result in - switch result { - case let .success(value): - if let session = value.data?.createCopilotSession, !session.isEmpty { - DispatchQueue.main.async { onSuccess(session) } - } else { - DispatchQueue.main.async { - onFailure(UnableTo.createSession) - } - } - case let .failure(error): - DispatchQueue.main.async { onFailure(error) } - } - } - } - - func beginThisRound() { - let parms: [String: AnyHashable] = switch action { - case let .translate(lang): - ["language": lang.rawValue] - case .summarize: - [:] - } - let json = try! CustomJSON(_jsonValue: parms) - Intelligents.qlClient.perform( - mutation: CreateCopilotMessageMutation(options: .init( - content: .init(stringLiteral: "\(documentContent)"), - params: .some(json), - sessionId: sessionID - )), - queue: .global() - ) { result in - switch result { - case let .success(value): - if let messageID = value.data?.createCopilotMessage { - self.messageID = messageID - self.chat_processWithMessageID(sessionID: self.sessionID, messageID: messageID) - } else { - self.presentError(UnableTo.createMessage) { - self.close() - } - } - case let .failure(error): - self.presentError(error) { - self.close() - } - } - } - } - - func chat_processWithMessageID(sessionID: String, messageID: String) { - let url = Constant.affineUpstreamURL - .appendingPathComponent("api") - .appendingPathComponent("copilot") - .appendingPathComponent("chat") - .appendingPathComponent(sessionID) - .appendingPathComponent("stream") - var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) - comps?.queryItems = [URLQueryItem(name: "messageId", value: messageID)] - - guard let url = comps?.url else { - assertionFailure() - presentError(UnableTo.createMessage) - return - } - - let eventHandler = BlockEventHandler() - eventHandler.onOpenedBlock = { - print("[*] chat opened") - } - eventHandler.onErrorBlock = { error in - self.presentError(error) { self.close() } - } - eventHandler.onMessageBlock = { _, message in - self.chat_onEvent(message.data) - } - eventHandler.onClosedBlock = { - self.chatTask?.stop() - self.chatTask = nil - } - let eventSource = EventSource(config: .init(handler: eventHandler, url: url)) - eventSource.start() - chatTask = eventSource - } - - func chat_onEvent(_ data: String) { - if Thread.isMainThread { - copilotDocumentStorage += data - } else { - DispatchQueue.main.asyncAndWait { - self.copilotDocumentStorage += data - } - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+ActionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+ActionBar.swift deleted file mode 100644 index 29baa8ef17..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+ActionBar.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// IntelligentsEphemeralActionController+ActionBar.swift -// Intelligents -// -// Created by 秋星桥 on 2025/1/15. -// - -import UIKit - -extension IntelligentsEphemeralActionController { - class ActionBar: UIView { - let retryButton = DarkActionButton() - let continueToChat = DarkActionButton() - let createNewDoc = DarkActionButton() - - init() { - super.init(frame: .zero) - defer { removeEveryAutoResizingMasks() } - - let contentSpacing: CGFloat = 16 - let buttonGroupHeight: CGFloat = 55 - - let firstButtonSectionGroup = UIView() - addSubview(firstButtonSectionGroup) - [ - firstButtonSectionGroup.topAnchor.constraint(equalTo: topAnchor, constant: contentSpacing), - firstButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor), - firstButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor), - firstButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight), - ].forEach { $0.isActive = true } - - retryButton.title = NSLocalizedString("Retry", comment: "") - retryButton.iconSystemName = "arrow.clockwise" - continueToChat.title = NSLocalizedString("Continue to Chat", comment: "") - continueToChat.iconSystemName = "paperplane" - firstButtonSectionGroup.addSubview(retryButton) - firstButtonSectionGroup.addSubview(continueToChat) - [ - retryButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor), - retryButton.leadingAnchor.constraint(equalTo: firstButtonSectionGroup.leadingAnchor), - retryButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor), - - continueToChat.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor), - continueToChat.trailingAnchor.constraint(equalTo: firstButtonSectionGroup.trailingAnchor), - continueToChat.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor), - - retryButton.widthAnchor.constraint(equalTo: continueToChat.widthAnchor), - retryButton.trailingAnchor.constraint(equalTo: continueToChat.leadingAnchor, constant: -contentSpacing), - ].forEach { $0.isActive = true } - - let secondButtonSectionGroup = UIView() - addSubview(secondButtonSectionGroup) - [ - secondButtonSectionGroup.topAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor, constant: contentSpacing), - secondButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor), - secondButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor), - secondButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight), - ].forEach { $0.isActive = true } - - secondButtonSectionGroup.addSubview(createNewDoc) - createNewDoc.title = NSLocalizedString("Create New Doc", comment: "") - createNewDoc.iconSystemName = "doc.badge.plus" - [ - createNewDoc.topAnchor.constraint(equalTo: secondButtonSectionGroup.topAnchor), - createNewDoc.leadingAnchor.constraint(equalTo: secondButtonSectionGroup.leadingAnchor), - createNewDoc.bottomAnchor.constraint(equalTo: secondButtonSectionGroup.bottomAnchor), - createNewDoc.trailingAnchor.constraint(equalTo: secondButtonSectionGroup.trailingAnchor), - ].forEach { $0.isActive = true } - - [ - secondButtonSectionGroup.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+Header.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+Header.swift deleted file mode 100644 index cabbb3e6fa..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController+Header.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// IntelligentsEphemeralActionController+Header.swift -// Intelligents -// -// Created by 秋星桥 on 2025/1/8. -// - -// -// IntelligentsChatController+Header.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/18. -// - -import UIKit - -extension IntelligentsEphemeralActionController { - class Header: UIView { - static let height: CGFloat = 44 - - let contentView = UIView() - let titleLabel = UILabel() - let dropMenu = UIButton() - let backButton = UIButton() - let rightBarItemsStack = UIStackView() - let moreMenu = UIButton() - - init() { - super.init(frame: .zero) - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - @objc func navigateActionBack() { - parentViewController?.dismissInContext() - } - } -} - -private extension IntelligentsEphemeralActionController.Header { - func setupLayout() { - contentView.translatesAutoresizingMaskIntoConstraints = false - addSubview(contentView) - [ - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: bottomAnchor), - contentView.heightAnchor.constraint(equalToConstant: Self.height), - ].forEach { $0.isActive = true } - - titleLabel.textColor = .label - titleLabel.font = .systemFont( - ofSize: UIFont.labelFontSize, - weight: .semibold - ) - - backButton.setImage( - UIImage(systemName: "chevron.left"), - for: .normal - ) - backButton.tintColor = .accent - backButton.addTarget(self, action: #selector(navigateActionBack), for: .touchUpInside) - - dropMenu.setImage( - .init(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate), - for: .normal - ) - dropMenu.tintColor = .gray.withAlphaComponent(0.5) - - contentView.addSubview(titleLabel) - contentView.addSubview(backButton) - contentView.addSubview(dropMenu) - contentView.addSubview(rightBarItemsStack) - titleLabel.translatesAutoresizingMaskIntoConstraints = false - backButton.translatesAutoresizingMaskIntoConstraints = false - dropMenu.translatesAutoresizingMaskIntoConstraints = false - rightBarItemsStack.translatesAutoresizingMaskIntoConstraints = false - - rightBarItemsStack.axis = .horizontal - rightBarItemsStack.spacing = 10 - rightBarItemsStack.alignment = .center - rightBarItemsStack.distribution = .equalSpacing - - [ - backButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - backButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - backButton.widthAnchor.constraint(equalToConstant: 44), - backButton.heightAnchor.constraint(equalToConstant: 44), - - rightBarItemsStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - rightBarItemsStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - rightBarItemsStack.heightAnchor.constraint(equalToConstant: 44), - - titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: backButton.trailingAnchor, constant: 10), - - dropMenu.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - dropMenu.widthAnchor.constraint(equalToConstant: 44), - dropMenu.heightAnchor.constraint(equalToConstant: 44), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: dropMenu.leadingAnchor, constant: -10), - ].forEach { $0.isActive = true } - - rightBarItemsStack.addArrangedSubview(moreMenu) - moreMenu.setImage( - .init(systemName: "ellipsis.circle"), - for: .normal - ) - moreMenu.tintColor = .accent - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController.swift deleted file mode 100644 index 6794ffe43f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsEphemeralActionController/IntelligentsEphemeralActionController.swift +++ /dev/null @@ -1,297 +0,0 @@ -// -// IntelligentsEphemeralActionController.swift -// Intelligents -// -// Created by 秋星桥 on 2025/1/8. -// - -import LDSwiftEventSource -import MarkdownParser -import MarkdownView -import UIKit - -public class IntelligentsEphemeralActionController: UIViewController { - let action: EphemeralAction - let scrollView = UIScrollView() - let stackView = UIStackView() - - let header = Header() - let preview = RotatedImagePreview() - - let markdownView = MarkdownView() - let indicator = UIActivityIndicatorView(style: .large) - var responseContainer: UIView = .init() - var responseHeightAnchor: NSLayoutConstraint? - - let actionBar = ActionBar() - - public var documentID: String = "" - public var workspaceID: String = "" - public var documentContent: String = "" - public internal(set) var sessionID: String = "" { - didSet { print(#fileID, #function, sessionID) } - } - - public internal(set) var messageID: String = "" { - didSet { print(#fileID, #function, messageID) } - } - - var chatTask: EventSource? - var copilotDocumentStorage: String = "" { - didSet { - updateDocumentPresentationView() - scrollToBottom() - } - } - - public init(action: EphemeralAction) { - self.action = action - super.init(nibName: nil, bundle: nil) - title = action.title - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - override public func viewDidLoad() { - super.viewDidLoad() - - overrideUserInterfaceStyle = .dark - hideKeyboardWhenTappedAround() - view.backgroundColor = .systemBackground - - header.titleLabel.text = title - header.dropMenu.isHidden = true - header.moreMenu.isHidden = true - view.addSubview(header) - header.translatesAutoresizingMaskIntoConstraints = false - [ - header.topAnchor.constraint(equalTo: view.topAnchor), - header.leadingAnchor.constraint(equalTo: view.leadingAnchor), - header.trailingAnchor.constraint(equalTo: view.trailingAnchor), - header.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44), - ].forEach { $0.isActive = true } - - view.addSubview(actionBar) - actionBar.translatesAutoresizingMaskIntoConstraints = false - [ - actionBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), - actionBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - actionBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ].forEach { $0.isActive = true } - - scrollView.clipsToBounds = true - scrollView.alwaysBounceVertical = true - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - scrollView.translatesAutoresizingMaskIntoConstraints = false - [ - scrollView.topAnchor.constraint(equalTo: header.bottomAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: actionBar.topAnchor), - ].forEach { $0.isActive = true } - - let contentView = UIView() - scrollView.addSubview(contentView) - contentView.translatesAutoresizingMaskIntoConstraints = false - [ - contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), - contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.heightAnchor), - ].forEach { $0.isActive = true } - - contentView.addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.spacing = 16 - stackView.alignment = .fill - stackView.distribution = .fill - contentView.addSubview(stackView) - - let stackViewInset: CGFloat = 8 - [ - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: stackViewInset), - stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: stackViewInset), - stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -stackViewInset), - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -stackViewInset), - ].forEach { $0.isActive = true } - - setupContentViews() - - actionBar.retryButton.action = { [weak self] in - self?.beginAction() - } - actionBar.continueToChat.action = { [weak self] in - guard let self else { return } - continueToChat() - } - } - - func setupContentViews() { - defer { stackView.addArrangedSubview(UIView()) } - - preview.layer.cornerRadius = 16 - preview.clipsToBounds = true - preview.contentMode = .scaleAspectFill - preview.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(preview) - - let headerGroup = UIView() - headerGroup.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(headerGroup) - - let headerLabel = UILabel() - let headerIcon = UIImageView() - - headerLabel.translatesAutoresizingMaskIntoConstraints = false - headerLabel.text = NSLocalizedString("AFFiNE AI", comment: "") - headerLabel.font = .preferredFont(for: .title3, weight: .bold) - headerLabel.textColor = .white - headerLabel.textAlignment = .left - headerIcon.translatesAutoresizingMaskIntoConstraints = false - headerIcon.image = .init(named: "spark", in: .module, with: nil) - headerIcon.contentMode = .scaleAspectFit - headerIcon.tintColor = .accent - headerGroup.addSubview(headerLabel) - headerGroup.addSubview(headerIcon) - [ - headerIcon.leadingAnchor.constraint(equalTo: headerGroup.leadingAnchor), - headerIcon.centerYAnchor.constraint(equalTo: headerGroup.centerYAnchor), - headerIcon.widthAnchor.constraint(equalToConstant: 32), - - headerLabel.leadingAnchor.constraint(equalTo: headerIcon.trailingAnchor, constant: 16), - headerLabel.topAnchor.constraint(equalTo: headerGroup.topAnchor), - headerLabel.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor), - ].forEach { $0.isActive = true } - - responseContainer.translatesAutoresizingMaskIntoConstraints = false - responseContainer.setContentHuggingPriority(.required, for: .vertical) - responseContainer.setContentCompressionResistancePriority(.required, for: .vertical) - responseContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 350).isActive = true - stackView.addArrangedSubview(responseContainer) - - responseContainer.addSubview(markdownView) - - markdownView.translatesAutoresizingMaskIntoConstraints = false - [ - markdownView.topAnchor.constraint(equalTo: responseContainer.topAnchor), - markdownView.leadingAnchor.constraint(equalTo: responseContainer.leadingAnchor), - markdownView.trailingAnchor.constraint(equalTo: responseContainer.trailingAnchor), - markdownView.bottomAnchor.constraint(equalTo: responseContainer.bottomAnchor), - ].forEach { - $0.isActive = true - } - - indicator.startAnimating() - indicator.translatesAutoresizingMaskIntoConstraints = false - responseContainer.addSubview(indicator) - [ - indicator.centerXAnchor.constraint(equalTo: responseContainer.centerXAnchor), - indicator.centerYAnchor.constraint(equalTo: responseContainer.centerYAnchor), - indicator.heightAnchor.constraint(equalToConstant: 200), - ].forEach { - $0.isActive = true - } - - updateDocumentPresentationView() - } - - public func configure(previewImage: UIImage) { - preview.configure(previewImage: previewImage) - } - - private var isFirstAppear: Bool = true - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - guard isFirstAppear else { return } - isFirstAppear = false - onFirstAppear() - } - - func onFirstAppear() { - beginAction() - } - - func close() { - if let navigationController { - navigationController.popViewController(animated: true) - } else { - dismiss(animated: true) - } - } - - private var previousLayoutWidth: CGFloat = 0 - - override public func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - if previousLayoutWidth != view.bounds.width { - previousLayoutWidth = view.bounds.width - updateDocumentPresentationView() - } - } - - func updateDocumentPresentationView() { - assert(Thread.isMainThread) - - responseHeightAnchor?.isActive = false - responseHeightAnchor = nil - - if copilotDocumentStorage.isEmpty { - indicator.isHidden = false - indicator.startAnimating() - responseHeightAnchor = responseContainer.heightAnchor.constraint(equalToConstant: 200) - responseHeightAnchor?.isActive = true - markdownView.updateContentViews([]) - return - } - - indicator.isHidden = true - indicator.stopAnimating() - - let document = MarkdownParser().feed(copilotDocumentStorage) - var height: CGFloat = 0 - let manifests = document.map { - let ret = $0.manifest(theme: .default) - ret.setLayoutWidth(responseContainer.bounds.width) - ret.layoutIfNeeded() - height += ret.size.height - height += Theme.default.spacings.final - return ret - } - markdownView.updateContentViews(manifests) - if height > 0 { height -= Theme.default.spacings.final } - responseHeightAnchor = responseContainer.heightAnchor.constraint(equalToConstant: height) - responseHeightAnchor?.isActive = true - } - - func scrollToBottom() { - guard !copilotDocumentStorage.isEmpty else { return } - let bottomOffset = CGPoint( - x: 0, - y: max(0, scrollView.contentSize.height - scrollView.bounds.size.height) - ) - UIView.animate( - withDuration: 0.5, - delay: 0, - usingSpringWithDamping: 1.0, - initialSpringVelocity: 0.8 - ) { self.scrollView.setContentOffset(bottomOffset, animated: false) } - } -} - -extension IntelligentsEphemeralActionController { - func continueToChat() { - let chatController = IntelligentsChatController() - chatController.metadata[.documentID] = documentID - chatController.metadata[.workspaceID] = workspaceID - chatController.metadata[.content] = documentContent - navigationController?.pushViewController(chatController, animated: true) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Capture.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Capture.swift deleted file mode 100644 index e257762f68..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Capture.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// IntelligentsFocusApertureView+Capture.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/21. -// - -import UIKit - -extension IntelligentsFocusApertureView { - func captureImageBuffer(_ targetContentView: UIView) { - let contentSize = targetContentView.frame.size - - let renderer = UIGraphicsImageRenderer(size: contentSize) - let image = renderer.image { _ in - let drawRect = CGRect( - x: 0, - y: 0, - width: contentSize.width, - height: contentSize.height - ) - - targetContentView.drawHierarchy( - in: drawRect, - afterScreenUpdates: true - ) - } - capturedImage = image - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Delegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Delegate.swift deleted file mode 100644 index eb13eff51b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Delegate.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// IntelligentsFocusApertureView+Delegate.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/21. -// - -import Foundation - -public enum IntelligentsFocusApertureViewActionType: String { - case translateTo - case summary - case chatWithAI - case dismiss -} - -public protocol IntelligentsFocusApertureViewDelegate: AnyObject { - func focusApertureRequestAction( - from: IntelligentsFocusApertureView, - actionType: IntelligentsFocusApertureViewActionType - ) -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Layout.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Layout.swift deleted file mode 100644 index 3ba0b06226..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Layout.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// IntelligentsFocusApertureView+Layout.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/21. -// - -import UIKit - -extension IntelligentsFocusApertureView { - func prepareFrameLayout() { - guard let viewController = targetViewController, - let view = viewController.view - else { - assertionFailure() - return - } - let safeLayout = viewController.view.safeAreaLayoutGuide - - frameConstraints = [ - // use safe area to layout content views - leadingAnchor.constraint(equalTo: safeLayout.leadingAnchor), - trailingAnchor.constraint(equalTo: safeLayout.trailingAnchor), - topAnchor.constraint(equalTo: safeLayout.topAnchor), - bottomAnchor.constraint(equalTo: safeLayout.bottomAnchor), - // cover all safe area so use constraints over view - backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - backgroundView.topAnchor.constraint(equalTo: view.topAnchor), - backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ] - } - - func prepareContentLayouts() { - guard let targetView else { - assertionFailure() - return - } - - contentBeginConstraints = [ - snapshotImageView.leftAnchor.constraint(equalTo: targetView.leftAnchor), - snapshotImageView.rightAnchor.constraint(equalTo: targetView.rightAnchor), - snapshotImageView.topAnchor.constraint(equalTo: targetView.topAnchor), - snapshotImageView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor), - - controlButtonsPanel.leftAnchor.constraint(equalTo: leftAnchor), - controlButtonsPanel.rightAnchor.constraint(equalTo: rightAnchor), - controlButtonsPanel.topAnchor.constraint(equalTo: bottomAnchor), - ] - - let sharedInset: CGFloat = 32 - contentFinalConstraints = [ - snapshotImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset), - snapshotImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset), - snapshotImageView.topAnchor.constraint(equalTo: topAnchor), - snapshotImageView.bottomAnchor.constraint(equalTo: controlButtonsPanel.topAnchor, constant: -sharedInset / 2), - - controlButtonsPanel.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset), - controlButtonsPanel.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset), - controlButtonsPanel.bottomAnchor.constraint(equalTo: bottomAnchor), - ] - } - - enum LayoutType { - case begin - case complete - } - - func activateLayoutForAnimation(_ type: LayoutType) { - NSLayoutConstraint.activate(frameConstraints) - switch type { - case .begin: - NSLayoutConstraint.deactivate(contentFinalConstraints) - NSLayoutConstraint.activate(contentBeginConstraints) - - snapshotImageView.layer.cornerRadius = 0 - case .complete: - NSLayoutConstraint.deactivate(contentBeginConstraints) - NSLayoutConstraint.activate(contentFinalConstraints) - - snapshotImageView.layer.cornerRadius = 32 - } - let effectiveView = superview ?? self - effectiveView.setNeedsUpdateConstraints() - effectiveView.setNeedsLayout() - updateConstraints() - layoutIfNeeded() - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift deleted file mode 100644 index 9216be4a3f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// IntelligentsFocusApertureView+Panel.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/21. -// - -import UIKit - -extension IntelligentsFocusApertureView { - class ControlButtonsPanel: UIView { - let headerLabel = UILabel() - let headerIcon = UIImageView() - - let translateButton = DarkActionButton() - let summaryButton = DarkActionButton() - let chatWithAIButton = DarkActionButton() - - init() { - super.init(frame: .zero) - defer { removeEveryAutoResizingMasks() } - - let contentSpacing: CGFloat = 16 - let buttonGroupHeight: CGFloat = 55 - - let headerGroup = UIView() - addSubview(headerGroup) - [ - headerGroup.topAnchor.constraint(equalTo: topAnchor), - headerGroup.leadingAnchor.constraint(equalTo: leadingAnchor), - headerGroup.trailingAnchor.constraint(equalTo: trailingAnchor), - ].forEach { $0.isActive = true } - - headerLabel.text = NSLocalizedString("AFFiNE AI", comment: "") // TODO: FREE TRAIL??? - // title 3 with bold - headerLabel.font = .preferredFont(for: .title3, weight: .bold) - headerLabel.textColor = .white - headerLabel.textAlignment = .left - headerIcon.image = .init(named: "spark", in: .module, with: nil) - headerIcon.contentMode = .scaleAspectFit - headerIcon.tintColor = .accent - headerGroup.addSubview(headerLabel) - headerGroup.addSubview(headerIcon) - [ - headerLabel.topAnchor.constraint(equalTo: headerGroup.topAnchor), - headerLabel.leadingAnchor.constraint(equalTo: headerGroup.leadingAnchor), - headerLabel.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor), - - headerIcon.topAnchor.constraint(equalTo: headerGroup.topAnchor), - headerIcon.trailingAnchor.constraint(equalTo: headerGroup.trailingAnchor), - headerIcon.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor), - - headerIcon.widthAnchor.constraint(equalToConstant: 32), - headerIcon.trailingAnchor.constraint(equalTo: headerGroup.trailingAnchor), - headerIcon.leadingAnchor.constraint(equalTo: headerLabel.trailingAnchor, constant: contentSpacing), - ].forEach { $0.isActive = true } - - let firstButtonSectionGroup = UIView() - addSubview(firstButtonSectionGroup) - [ - firstButtonSectionGroup.topAnchor.constraint(equalTo: headerGroup.bottomAnchor, constant: contentSpacing), - firstButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor), - firstButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor), - firstButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight), - ].forEach { $0.isActive = true } - - translateButton.title = NSLocalizedString("Translate", comment: "") - translateButton.iconSystemName = "textformat" - summaryButton.title = NSLocalizedString("Summary", comment: "") - summaryButton.iconSystemName = "doc.text" - firstButtonSectionGroup.addSubview(translateButton) - firstButtonSectionGroup.addSubview(summaryButton) - [ - translateButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor), - translateButton.leadingAnchor.constraint(equalTo: firstButtonSectionGroup.leadingAnchor), - translateButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor), - - summaryButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor), - summaryButton.trailingAnchor.constraint(equalTo: firstButtonSectionGroup.trailingAnchor), - summaryButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor), - - translateButton.widthAnchor.constraint(equalTo: summaryButton.widthAnchor), - translateButton.trailingAnchor.constraint(equalTo: summaryButton.leadingAnchor, constant: -contentSpacing), - ].forEach { $0.isActive = true } - - let secondButtonSectionGroup = UIView() - addSubview(secondButtonSectionGroup) - [ - secondButtonSectionGroup.topAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor, constant: contentSpacing), - secondButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor), - secondButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor), - secondButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight), - ].forEach { $0.isActive = true } - - secondButtonSectionGroup.addSubview(chatWithAIButton) - chatWithAIButton.title = NSLocalizedString("Chat with AI", comment: "") - chatWithAIButton.iconSystemName = "paperplane" - [ - chatWithAIButton.topAnchor.constraint(equalTo: secondButtonSectionGroup.topAnchor), - chatWithAIButton.leadingAnchor.constraint(equalTo: secondButtonSectionGroup.leadingAnchor), - chatWithAIButton.bottomAnchor.constraint(equalTo: secondButtonSectionGroup.bottomAnchor), - chatWithAIButton.trailingAnchor.constraint(equalTo: secondButtonSectionGroup.trailingAnchor), - ].forEach { $0.isActive = true } - - [ - secondButtonSectionGroup.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift deleted file mode 100644 index ee0a881d0f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// IntelligentsFocusApertureView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/11/21. -// - -import UIKit - -public class IntelligentsFocusApertureView: UIView { - public let backgroundView = UIView() - public let snapshotImageView = UIImageView() - let controlButtonsPanel = ControlButtonsPanel() - - public var animationDuration: TimeInterval = 0.75 - - public internal(set) weak var targetView: UIView? - public internal(set) weak var targetViewController: UIViewController? - public internal(set) weak var capturedImage: UIImage? { - get { snapshotImageView.image } - set { snapshotImageView.image = newValue } - } - - var frameConstraints: [NSLayoutConstraint] = [] - var contentBeginConstraints: [NSLayoutConstraint] = [] - var contentFinalConstraints: [NSLayoutConstraint] = [] - - public weak var delegate: (any IntelligentsFocusApertureViewDelegate)? - - public init() { - super.init(frame: .zero) - - backgroundView.backgroundColor = .black - backgroundView.isUserInteractionEnabled = true - let tap = UITapGestureRecognizer( - target: self, - action: #selector(dismissFocus) - ) - tap.cancelsTouchesInView = true - backgroundView.addGestureRecognizer(tap) - - snapshotImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - snapshotImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - snapshotImageView.layer.contentsGravity = .top - snapshotImageView.layer.masksToBounds = true - snapshotImageView.contentMode = .scaleAspectFill - snapshotImageView.isUserInteractionEnabled = true - snapshotImageView.addGestureRecognizer(UITapGestureRecognizer( - target: self, - action: #selector(dismissFocus) - )) - - addSubview(backgroundView) - addSubview(controlButtonsPanel) - addSubview(snapshotImageView) - bringSubviewToFront(snapshotImageView) - - controlButtonsPanel.translateButton.action = { [weak self] in - guard let self else { return } - delegate?.focusApertureRequestAction(from: self, actionType: .translateTo) - } - controlButtonsPanel.summaryButton.action = { [weak self] in - guard let self else { return } - delegate?.focusApertureRequestAction(from: self, actionType: .summary) - } - controlButtonsPanel.chatWithAIButton.action = { [weak self] in - guard let self else { return } - delegate?.focusApertureRequestAction(from: self, actionType: .chatWithAI) - } - removeEveryAutoResizingMasks() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - public func prepareAnimationWith( - capturingTargetContentView targetContentView: UIView, - coveringRootViewController viewController: UIViewController - ) { - captureImageBuffer(targetContentView) - - targetView = targetContentView - targetViewController = viewController - - viewController.view.addSubview(self) - - prepareFrameLayout() - prepareContentLayouts() - activateLayoutForAnimation(.begin) - } - - public func executeAnimationKickIn(_ completion: @escaping () -> Void = {}) { - activateLayoutForAnimation(.begin) - isUserInteractionEnabled = false - UIView.animate( - withDuration: animationDuration, - delay: 0, - usingSpringWithDamping: 1.0, - initialSpringVelocity: 0.8 - ) { - self.activateLayoutForAnimation(.complete) - } completion: { _ in - self.isUserInteractionEnabled = true - completion() - } - } - - public func executeAnimationDismiss(_ completion: @escaping () -> Void = {}) { - activateLayoutForAnimation(.complete) - isUserInteractionEnabled = false - UIView.animate( - withDuration: animationDuration, - delay: 0, - usingSpringWithDamping: 1.0, - initialSpringVelocity: 0.8 - ) { - self.activateLayoutForAnimation(.begin) - } completion: { _ in - self.isUserInteractionEnabled = true - completion() - } - } - - @objc func dismissFocus() { - isUserInteractionEnabled = false - executeAnimationDismiss { - self.removeFromSuperview() - self.delegate?.focusApertureRequestAction(from: self, actionType: .dismiss) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings deleted file mode 100644 index 88701da424..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,17 +0,0 @@ -/* - Localizable.strings - Intelligents - - Created by 秋星桥 on 2024/11/18. - -*/ - -"Chat with AI" = "Chat with AI"; -"AFFiNE AI" = "AFFiNE AI"; -"Translate" = "Translate"; -"Summary" = "Summary"; -"Summarize this article for me..." = "Summarize this article for me..."; -"System" = "System"; -"AFFiNE AI" = "AFFiNE AI"; -"You" = "You"; -"Error" = "Error"; diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index d89c4a6cef..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* - Localizable.strings - Intelligents - - Created by 秋星桥 on 2024/11/18. - -*/ - -"Chat with AI" = "与 AI 聊天"; -"AFFiNE AI" = "AFFiNE 人工智能"; -"Translate" = "翻译"; -"Summary" = "总结"; -"Summarize this article for me..." = "请为我总结这份文档..."; -"System" = "系统"; -"AFFiNE AI" = "AFFiNE AI"; -"You" = "你"; -"Error" = "错误"; -"OK" = "确定"; diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/.gitignore b/packages/frontend/apps/ios/App/Packages/MarkdownView/.gitignore deleted file mode 100644 index 0023a53406..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/.swiftpm/xcode/xcshareddata/xcschemes/FlowMarkdownView.xcscheme b/packages/frontend/apps/ios/App/Packages/MarkdownView/.swiftpm/xcode/xcshareddata/xcschemes/FlowMarkdownView.xcscheme deleted file mode 100644 index de651b9d85..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/.swiftpm/xcode/xcshareddata/xcschemes/FlowMarkdownView.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcodeproj/project.pbxproj deleted file mode 100644 index 65782460a1..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcodeproj/project.pbxproj +++ /dev/null @@ -1,411 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXBuildFile section */ - 50219C662D3E2304006CB93C /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50219C652D3E2304006CB93C /* App.swift */; }; - 507C16722D2719F100B478D2 /* MarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = 507C16712D2719F100B478D2 /* MarkdownView */; }; - 5084C6742D281A41007310F0 /* LookinServer in Frameworks */ = {isa = PBXBuildFile; productRef = 5084C6732D281A41007310F0 /* LookinServer */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 50219C652D3E2304006CB93C /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; - 505E99EA2D26D8380014A6D3 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 505E99E72D26D8380014A6D3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 507C16722D2719F100B478D2 /* MarkdownView in Frameworks */, - 5084C6742D281A41007310F0 /* LookinServer in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 5015F6D32D26DCFB005FA7D2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 50219C642D3E22FB006CB93C /* Example */ = { - isa = PBXGroup; - children = ( - 50219C652D3E2304006CB93C /* App.swift */, - ); - path = Example; - sourceTree = ""; - }; - 505E99E12D26D8380014A6D3 = { - isa = PBXGroup; - children = ( - 50219C642D3E22FB006CB93C /* Example */, - 5015F6D32D26DCFB005FA7D2 /* Frameworks */, - 505E99EB2D26D8380014A6D3 /* Products */, - ); - sourceTree = ""; - }; - 505E99EB2D26D8380014A6D3 /* Products */ = { - isa = PBXGroup; - children = ( - 505E99EA2D26D8380014A6D3 /* Example.app */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 505E99E92D26D8380014A6D3 /* Example */ = { - isa = PBXNativeTarget; - buildConfigurationList = 505E99F82D26D8390014A6D3 /* Build configuration list for PBXNativeTarget "Example" */; - buildPhases = ( - 5015F6CD2D26DB1B005FA7D2 /* Format Source */, - 505E99E62D26D8380014A6D3 /* Sources */, - 505E99E72D26D8380014A6D3 /* Frameworks */, - 505E99E82D26D8380014A6D3 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Example; - packageProductDependencies = ( - 507C16712D2719F100B478D2 /* MarkdownView */, - 5084C6732D281A41007310F0 /* LookinServer */, - ); - productName = Example; - productReference = 505E99EA2D26D8380014A6D3 /* Example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 505E99E22D26D8380014A6D3 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 1620; - TargetAttributes = { - 505E99E92D26D8380014A6D3 = { - CreatedOnToolsVersion = 16.2; - LastSwiftMigration = 1620; - }; - }; - }; - buildConfigurationList = 505E99E52D26D8380014A6D3 /* Build configuration list for PBXProject "Example" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 505E99E12D26D8380014A6D3; - minimizedProjectReferenceProxies = 1; - packageReferences = ( - 5084C6722D281A38007310F0 /* XCRemoteSwiftPackageReference "LookinServer" */, - ); - preferredProjectObjectVersion = 77; - productRefGroup = 505E99EB2D26D8380014A6D3 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 505E99E92D26D8380014A6D3 /* Example */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 505E99E82D26D8380014A6D3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 5015F6CD2D26DB1B005FA7D2 /* Format Source */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Format Source"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/opt/homebrew/bin/swiftformat . --swiftversion 6.0\n\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 505E99E62D26D8380014A6D3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 50219C662D3E2304006CB93C /* App.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 505E99F62D26D8390014A6D3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 505E99F72D26D8390014A6D3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 505E99F92D26D8390014A6D3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 964G86XT2P; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = YES; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 505E99FA2D26D8390014A6D3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 964G86XT2P; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = YES; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 505E99E52D26D8380014A6D3 /* Build configuration list for PBXProject "Example" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 505E99F62D26D8390014A6D3 /* Debug */, - 505E99F72D26D8390014A6D3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 505E99F82D26D8390014A6D3 /* Build configuration list for PBXNativeTarget "Example" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 505E99F92D26D8390014A6D3 /* Debug */, - 505E99FA2D26D8390014A6D3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 5084C6722D281A38007310F0 /* XCRemoteSwiftPackageReference "LookinServer" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/QMUI/LookinServer/"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.2.8; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 507C16712D2719F100B478D2 /* MarkdownView */ = { - isa = XCSwiftPackageProductDependency; - productName = MarkdownView; - }; - 5084C6732D281A41007310F0 /* LookinServer */ = { - isa = XCSwiftPackageProductDependency; - package = 5084C6722D281A38007310F0 /* XCRemoteSwiftPackageReference "LookinServer" */; - productName = LookinServer; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 505E99E22D26D8380014A6D3 /* Project object */; -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a625..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcworkspace/contents.xcworkspacedata b/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 684b9e8df0..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example/App.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example/App.swift deleted file mode 100644 index c972267baa..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Example/Example/App.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// App.swift -// Example -// -// Created by 秋星桥 on 1/20/25. -// - -import SwiftUI - -@main -struct TheApp: App { - var body: some Scene { - WindowGroup { - NavigationView { - Content() - .navigationTitle("MarkdownView") - .navigationBarTitleDisplayMode(.inline) - } - .navigationViewStyle(.stack) - } - } -} - -import MarkdownParser -import MarkdownView - -class ContentController: UIViewController { - let document = MarkdownParser().feed(testDocument) - let scrollView = UIScrollView() - let markdownView = MarkdownView(theme: .default) - - override func viewDidLoad() { - super.viewDidLoad() - view.addSubview(scrollView) - scrollView.addSubview(markdownView) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - scrollView.frame = view.bounds - let width = view.bounds.width - 32 - let manifest = document.map { - let manifest = $0.manifest(theme: markdownView.theme) - manifest.setLayoutWidth(width) - manifest.layoutIfNeeded() - return manifest - } - markdownView.updateContentViews(manifest) - markdownView.frame = .init( - x: 16, - y: 16, - width: width, - height: markdownView.height - ) - scrollView.contentSize = .init( - width: width, - height: markdownView.height + 100 - ) - } -} - -struct Content: UIViewControllerRepresentable { - func makeUIViewController(context _: Context) -> ContentController { - ContentController() - } - - func updateUIViewController(_: ContentController, context _: Context) {} -} - -let testDocument = ###""" -# Markdown 测试文稿 - -这是一篇用于测试渲染引擎性能的 Markdown 文档,包含多种格式和元素。以下是不同 Markdown 语法的示例: - -## 标题 - -### 三级标题 - -#### 四级标题 - -##### 五级标题 - -###### 六级标题 - -## 段落与文本格式 - -这是一个普通段落。**这是加粗的文字**,*这是斜体的文字*,***这是加粗且斜体的文字***。~~这是删除线~~。 - -这是`行内代码`的示例。 - -## 列表 - -### 无序列表 - -- 项目 1 -- 项目 2 - - 子项目 2.1 - - 子项目 2.2 -- 项目 3 - -### 有序列表 - -1. 第一项 -2. 第二项 - 1. 子项 2.1 - 2. 子项 2.2 -3. 第三项 - -## 引用 - -> 这是一个引用块。引用块可以包含多行文字,甚至可以包含其他 Markdown 元素,比如**加粗**或`代码`。 - -## 代码块 - -```python -def hello_world(): - print("Hello, World!") -``` - -```javascript -function helloWorld() { - console.log("Hello, World!"); -} -``` - -## 表格 - -| 序号 | 名称 | 描述 | -| ---- | ---------- | ------------- | -| 1 | 项目 A | 这是项目 A | -| 2 | 项目 B | 这是项目 B | -| 3 | 项目 C | 这是项目 C | - -## 链接与图片 - -这是一个[短链接](https://example.com)的示例。 - -![图片描述](https://via.placeholder.com/150) - -## HTML 嵌入 - -

这是一个红色的段落,使用 HTML 标签实现。

- -这是一个短链接 - -## 分隔线 - ---- - -## 脚注 - -这是一个脚注的示例[^1]。 - -[^1]: 这是脚注的内容。 - -## 内嵌 HTML - -
- 这是一个带有边框的 HTML 块。 -
- -## 数学公式(如果支持) - -这是一个行内公式:$E = mc^2$。 - -这是一个块级公式: -$$ -\int_{a}^{b} x^2 dx -$$ - -## 任务列表 - -- [x] 完成任务 1 -- [ ] 完成任务 2 -- [ ] 完成任务 3 - -## 结束语 - -这篇文档包含了多种 Markdown 格式和 HTML 元素,适合用于测试渲染引擎的性能和兼容性。希望它能帮助你完成测试! -"""### diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Package.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Package.swift deleted file mode 100644 index bb620e0c3f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Package.swift +++ /dev/null @@ -1,29 +0,0 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "MarkdownView", - platforms: [ - .iOS(.v14), - .macCatalyst(.v14), - ], - products: [ - .library(name: "MarkdownView", targets: ["MarkdownView"]), - ], - dependencies: [ - .package(url: "https://github.com/JohnSundell/Splash", from: "0.16.0"), - .package(url: "https://github.com/swiftlang/swift-cmark", from: "0.6.0"), - ], - targets: [ - .target(name: "MarkdownView", dependencies: [ - "MarkdownParser", - "Splash", - ]), - .target(name: "MarkdownParser", dependencies: [ - .product(name: "cmark-gfm", package: "swift-cmark"), - .product(name: "cmark-gfm-extensions", package: "swift-cmark"), - ]), - ] -) diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/BlockNode/BlockNode+Rewrite.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/BlockNode/BlockNode+Rewrite.swift deleted file mode 100644 index 03990f4a45..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/BlockNode/BlockNode+Rewrite.swift +++ /dev/null @@ -1,104 +0,0 @@ -import Foundation - -extension Sequence { - func rewrite(_ r: (BlockNode) throws -> [BlockNode]) rethrows -> [BlockNode] { - try flatMap { try $0.rewrite(r) } - } - - func rewrite(_ r: (InlineNode) throws -> [InlineNode]) rethrows -> [BlockNode] { - try flatMap { try $0.rewrite(r) } - } -} - -extension BlockNode { - func rewrite(_ r: (BlockNode) throws -> [BlockNode]) rethrows -> [BlockNode] { - switch self { - case let .blockquote(children): - try r(.blockquote(children: children.rewrite(r))) - case let .bulletedList(isTight, items): - try r( - .bulletedList( - isTight: isTight, - items: items.map { - try RawListItem(children: $0.children.rewrite(r)) - } - ) - ) - case let .numberedList(isTight, start, items): - try r( - .numberedList( - isTight: isTight, - start: start, - items: items.map { - try RawListItem(children: $0.children.rewrite(r)) - } - ) - ) - case let .taskList(isTight, items): - try r( - .taskList( - isTight: isTight, - items: items.map { - try RawTaskListItem(isCompleted: $0.isCompleted, children: $0.children.rewrite(r)) - } - ) - ) - default: - try r(self) - } - } - - func rewrite(_ r: (InlineNode) throws -> [InlineNode]) rethrows -> [BlockNode] { - switch self { - case let .blockquote(children): - try [.blockquote(children: children.rewrite(r))] - case let .bulletedList(isTight, items): - try [ - .bulletedList( - isTight: isTight, - items: items.map { - try RawListItem(children: $0.children.rewrite(r)) - } - ), - ] - case let .numberedList(isTight, start, items): - try [ - .numberedList( - isTight: isTight, - start: start, - items: items.map { - try RawListItem(children: $0.children.rewrite(r)) - } - ), - ] - case let .taskList(isTight, items): - try [ - .taskList( - isTight: isTight, - items: items.map { - try RawTaskListItem(isCompleted: $0.isCompleted, children: $0.children.rewrite(r)) - } - ), - ] - case let .paragraph(content): - try [.paragraph(content: content.rewrite(r))] - case let .heading(level, content): - try [.heading(level: level, content: content.rewrite(r))] - case let .table(columnAlignments, rows): - try [ - .table( - columnAlignments: columnAlignments, - rows: rows.map { - try RawTableRow( - cells: $0.cells.map { - try RawTableCell(content: $0.content.rewrite(r)) - } - ) - } - ), - ] - default: - [self] - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/BlockNode/BlockNode.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/BlockNode/BlockNode.swift deleted file mode 100644 index 1115c3c853..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/BlockNode/BlockNode.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation - -public enum BlockNode: Hashable, Equatable, Codable { - case blockquote(children: [BlockNode]) - case bulletedList(isTight: Bool, items: [RawListItem]) - case numberedList(isTight: Bool, start: Int, items: [RawListItem]) - case taskList(isTight: Bool, items: [RawTaskListItem]) - case codeBlock(fenceInfo: String?, content: String) -// case htmlBlock(content: String) - case paragraph(content: [InlineNode]) - case heading(level: Int, content: [InlineNode]) - case table(columnAlignments: [RawTableColumnAlignment], rows: [RawTableRow]) - case thematicBreak -// -// public var typeIdentifier: String { -// switch self { -// case .blockquote: -// "blockquote" -// case .bulletedList: -// "bulletedList" -// case .numberedList: -// "numberedList" -// case .taskList: -// "taskList" -// case .codeBlock: -// "codeBlock" -// case .htmlBlock: -// "htmlBlock" -// case .paragraph: -// "paragraph" -// case .heading: -// "heading" -// case .table: -// "table" -// case .thematicBreak: -// "thematicBreak" -// } -// } -} - -extension BlockNode { - var children: [BlockNode] { - switch self { - case let .blockquote(children): - children - case let .bulletedList(_, items): - items.map(\.children).flatMap(\.self) - case let .numberedList(_, _, items): - items.map(\.children).flatMap(\.self) - case let .taskList(_, items): - items.map(\.children).flatMap(\.self) - default: - [] - } - } - - var isParagraph: Bool { - guard case .paragraph = self else { return false } - return true - } -} - -public struct RawListItem: Hashable, Equatable, Codable { - public let children: [BlockNode] -} - -public struct RawTaskListItem: Hashable, Equatable, Codable { - public let isCompleted: Bool - public let children: [BlockNode] -} - -public enum RawTableColumnAlignment: Character, Equatable, Codable { - case none = "\0" - case left = "l" - case center = "c" - case right = "r" -} - -public struct RawTableRow: Hashable, Equatable, Codable { - public let cells: [RawTableCell] -} - -public struct RawTableCell: Hashable, Equatable, Codable { - public let content: [InlineNode] -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode+Collect.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode+Collect.swift deleted file mode 100644 index 07d6fa4846..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode+Collect.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -extension Sequence { - func collect(_ c: (InlineNode) throws -> [Result]) rethrows -> [Result] { - try flatMap { try $0.collect(c) } - } -} - -extension InlineNode { - func collect(_ c: (InlineNode) throws -> [Result]) rethrows -> [Result] { - try children.collect(c) + c(self) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode+Rewrite.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode+Rewrite.swift deleted file mode 100644 index 6b75897983..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode+Rewrite.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -extension Sequence { - func rewrite(_ r: (InlineNode) throws -> [InlineNode]) rethrows -> [InlineNode] { - try flatMap { try $0.rewrite(r) } - } -} - -extension InlineNode { - func rewrite(_ r: (InlineNode) throws -> [InlineNode]) rethrows -> [InlineNode] { - var inline = self - inline.children = try children.rewrite(r) - return try r(inline) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode.swift deleted file mode 100644 index 7352ae5642..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/InlineNode/InlineNode.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -public enum InlineNode: Hashable, Sendable, Equatable, Codable { - case text(String) - case softBreak - case lineBreak - case code(String) - case html(String) - case emphasis(children: [InlineNode]) - case strong(children: [InlineNode]) - case strikethrough(children: [InlineNode]) - case link(destination: String, children: [InlineNode]) - case image(source: String, children: [InlineNode]) -} - -extension InlineNode { - var children: [InlineNode] { - get { - switch self { - case let .emphasis(children): - children - case let .strong(children): - children - case let .strikethrough(children): - children - case let .link(_, children): - children - case let .image(_, children): - children - default: - [] - } - } - - set { - switch self { - case .emphasis: - self = .emphasis(children: newValue) - case .strong: - self = .strong(children: newValue) - case .strikethrough: - self = .strikethrough(children: newValue) - case let .link(destination, _): - self = .link(destination: destination, children: newValue) - case let .image(source, _): - self = .image(source: source, children: newValue) - default: - break - } - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Delegate.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Delegate.swift deleted file mode 100644 index 89cb30a86a..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Delegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// MarkdownParser+Delegate.swift -// FlowMarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation - -public extension MarkdownParser { - protocol Delegate: AnyObject { - func updateBlockNodes(_ blockNodes: [BlockNode]) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Node.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Node.swift deleted file mode 100644 index fe5eedde24..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Node.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MarkdownParser+Node.swift -// FlowMarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import cmark_gfm -import cmark_gfm_extensions -import Foundation - -extension MarkdownParser { - func dumpBlocks(root: UnsafeNode?) { - guard let root else { - assertionFailure() - return - } - assert(root.pointee.type == CMARK_NODE_DOCUMENT.rawValue) - - blocks = root.children.compactMap(BlockNode.init(unsafeNode:)) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Setup.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Setup.swift deleted file mode 100644 index adf584d21b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser+Setup.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MarkdownParser+Setup.swift -// FlowMarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import cmark_gfm -import cmark_gfm_extensions - -import Foundation - -extension MarkdownParser { - func setupExtensions(parser: UnsafeMutablePointer) { - cmark_gfm_core_extensions_ensure_registered() - let extensionNames = ["autolink", "strikethrough", "tagfilter", "tasklist", "table"] - for extensionName in extensionNames { - guard let syntaxExtension = cmark_find_syntax_extension(extensionName) else { - assertionFailure() - continue - } - cmark_parser_attach_syntax_extension(parser, syntaxExtension) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser.swift deleted file mode 100644 index 08de046f88..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/MarkdownParser/MarkdownParser.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MarkdownParser.swift -// FlowMarkdownView -// -// Created by 秋星桥 on 2025/1/2. -// - -import cmark_gfm -import cmark_gfm_extensions -import Foundation - -public class MarkdownParser { - public internal(set) var blocks: [BlockNode] = [] { - didSet { delegate?.updateBlockNodes(blocks) } - } - - public weak var delegate: Delegate? - var currentDoc = String() - - public init() {} - - @discardableResult - public func feed(_ text: String) -> [BlockNode] { - currentDoc += text - let parser = cmark_parser_new(CMARK_OPT_DEFAULT)! - defer { cmark_parser_free(parser) } - setupExtensions(parser: parser) - cmark_parser_feed(parser, currentDoc, currentDoc.utf8.count) - let node = cmark_parser_finish(parser) - defer { cmark_node_free(node) } - dumpBlocks(root: node) - return blocks - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/Utils/Converters.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/Utils/Converters.swift deleted file mode 100644 index 56cdb687d1..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/Utils/Converters.swift +++ /dev/null @@ -1,289 +0,0 @@ -import cmark_gfm -import cmark_gfm_extensions -import Foundation - -typealias UnsafeNode = UnsafeMutablePointer - -extension BlockNode { - init?(unsafeNode: UnsafeNode) { - switch unsafeNode.nodeType { - case .blockquote: - self = .blockquote(children: unsafeNode.children.compactMap(BlockNode.init(unsafeNode:))) - case .list: - if unsafeNode.children.contains(where: \.isTaskListItem) { - self = .taskList( - isTight: unsafeNode.isTightList, - items: unsafeNode.children.map(RawTaskListItem.init(unsafeNode:)) - ) - } else { - switch unsafeNode.listType { - case CMARK_BULLET_LIST: - self = .bulletedList( - isTight: unsafeNode.isTightList, - items: unsafeNode.children.map(RawListItem.init(unsafeNode:)) - ) - case CMARK_ORDERED_LIST: - self = .numberedList( - isTight: unsafeNode.isTightList, - start: unsafeNode.listStart, - items: unsafeNode.children.map(RawListItem.init(unsafeNode:)) - ) - default: - fatalError("cmark reported a list node without a list type.") - } - } - case .codeBlock: - self = .codeBlock(fenceInfo: unsafeNode.fenceInfo, content: unsafeNode.literal ?? "") - case .htmlBlock: -// self = .htmlBlock(content: unsafeNode.literal ?? "") - self = .codeBlock(fenceInfo: "html", content: unsafeNode.literal ?? "") - case .paragraph: - self = .paragraph(content: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:))) - case .heading: - self = .heading( - level: unsafeNode.headingLevel, - content: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:)) - ) - case .table: - self = .table( - columnAlignments: unsafeNode.tableAlignments, - rows: unsafeNode.children.map(RawTableRow.init(unsafeNode:)) - ) - case .thematicBreak: - self = .thematicBreak - default: - assertionFailure("Unhandled node type '\(unsafeNode.nodeType)' in BlockNode.") - return nil - } - } -} - -extension RawListItem { - init(unsafeNode: UnsafeNode) { - guard unsafeNode.nodeType == .item else { - fatalError("Expected a list item but got a '\(unsafeNode.nodeType)' instead.") - } - self.init(children: unsafeNode.children.compactMap(BlockNode.init(unsafeNode:))) - } -} - -extension RawTaskListItem { - init(unsafeNode: UnsafeNode) { - guard unsafeNode.nodeType == .taskListItem || unsafeNode.nodeType == .item else { - fatalError("Expected a list item but got a '\(unsafeNode.nodeType)' instead.") - } - self.init( - isCompleted: unsafeNode.isTaskListItemChecked, - children: unsafeNode.children.compactMap(BlockNode.init(unsafeNode:)) - ) - } -} - -extension RawTableRow { - init(unsafeNode: UnsafeNode) { - guard unsafeNode.nodeType == .tableRow || unsafeNode.nodeType == .tableHead else { - fatalError("Expected a table row but got a '\(unsafeNode.nodeType)' instead.") - } - self.init(cells: unsafeNode.children.map(RawTableCell.init(unsafeNode:))) - } -} - -extension RawTableCell { - init(unsafeNode: UnsafeNode) { - guard unsafeNode.nodeType == .tableCell else { - fatalError("Expected a table cell but got a '\(unsafeNode.nodeType)' instead.") - } - self.init(content: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:))) - } -} - -extension InlineNode { - init?(unsafeNode: UnsafeNode) { - switch unsafeNode.nodeType { - case .text: - self = .text(unsafeNode.literal ?? "") - case .softBreak: - self = .softBreak - case .lineBreak: - self = .lineBreak - case .code: - self = .code(unsafeNode.literal ?? "") - case .html: - self = .html(unsafeNode.literal ?? "") - case .emphasis: - self = .emphasis(children: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:))) - case .strong: - self = .strong(children: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:))) - case .strikethrough: - self = .strikethrough(children: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:))) - case .link: - self = .link( - destination: unsafeNode.url ?? "", - children: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:)) - ) - case .image: - self = .image( - source: unsafeNode.url ?? "", - children: unsafeNode.children.compactMap(InlineNode.init(unsafeNode:)) - ) - default: - assertionFailure("Unhandled node type '\(unsafeNode.nodeType)' in InlineNode.") - return nil - } - } -} - -extension UnsafeNode { - var nodeType: NodeType { - let typeString = String(cString: cmark_node_get_type_string(self)) - guard let nodeType = NodeType(rawValue: typeString) else { - fatalError("Unknown node type '\(typeString)' found.") - } - return nodeType - } - - var children: UnsafeNodeSequence { - .init(cmark_node_first_child(self)) - } - - var literal: String? { - cmark_node_get_literal(self).map(String.init(cString:)) - } - - var url: String? { - cmark_node_get_url(self).map(String.init(cString:)) - } - - var isTaskListItem: Bool { - nodeType == .taskListItem - } - - var listType: cmark_list_type { - cmark_node_get_list_type(self) - } - - var listStart: Int { - Int(cmark_node_get_list_start(self)) - } - - var isTaskListItemChecked: Bool { - cmark_gfm_extensions_get_tasklist_item_checked(self) - } - - var isTightList: Bool { - cmark_node_get_list_tight(self) != 0 - } - - var fenceInfo: String? { - cmark_node_get_fence_info(self).map(String.init(cString:)) - } - - var headingLevel: Int { - Int(cmark_node_get_heading_level(self)) - } - - var tableColumns: Int { - Int(cmark_gfm_extensions_get_table_columns(self)) - } - - var tableAlignments: [RawTableColumnAlignment] { - (0 ..< tableColumns).map { column in - let ascii = cmark_gfm_extensions_get_table_alignments(self)[column] - let scalar = UnicodeScalar(ascii) - let character = Character(scalar) - return .init(rawValue: character) ?? .none - } - } -} - -enum NodeType: String { - case document - case blockquote = "block_quote" - case list - case item - case codeBlock = "code_block" - case htmlBlock = "html_block" - case customBlock = "custom_block" - case paragraph - case heading - case thematicBreak = "thematic_break" - case text - case softBreak = "softbreak" - case lineBreak = "linebreak" - case code - case html = "html_inline" - case customInline = "custom_inline" - case emphasis = "emph" - case strong - case link - case image - case inlineAttributes = "attribute" - case none = "NONE" - case unknown = "" - - // Extensions - - case strikethrough - case table - case tableHead = "table_header" - case tableRow = "table_row" - case tableCell = "table_cell" - case taskListItem = "tasklist" -} - -struct UnsafeNodeSequence: Sequence { - struct Iterator: IteratorProtocol { - var node: UnsafeNode? - - init(_ node: UnsafeNode?) { - self.node = node - } - - mutating func next() -> UnsafeNode? { - guard let node else { return nil } - defer { self.node = cmark_node_next(node) } - return node - } - } - - let node: UnsafeNode? - - init(_ node: UnsafeNode?) { - self.node = node - } - - func makeIterator() -> Iterator { - .init(node) - } -} - -// Extension node types are not exported in `cmark_gfm_extensions`, -// so we need to look for them in the symbol table -struct ExtensionNodeTypes { - let CMARK_NODE_TABLE: cmark_node_type - let CMARK_NODE_TABLE_ROW: cmark_node_type - let CMARK_NODE_TABLE_CELL: cmark_node_type - let CMARK_NODE_STRIKETHROUGH: cmark_node_type - - static let shared = ExtensionNodeTypes() - - init() { - func findNodeType(_ name: String, in handle: UnsafeMutableRawPointer!) -> cmark_node_type? { - guard let symbol = dlsym(handle, name) else { - return nil - } - return symbol.assumingMemoryBound(to: cmark_node_type.self).pointee - } - - let handle = dlopen(nil, RTLD_LAZY) - - CMARK_NODE_TABLE = findNodeType("CMARK_NODE_TABLE", in: handle) ?? CMARK_NODE_NONE - CMARK_NODE_TABLE_ROW = findNodeType("CMARK_NODE_TABLE_ROW", in: handle) ?? CMARK_NODE_NONE - CMARK_NODE_TABLE_CELL = - findNodeType("CMARK_NODE_TABLE_CELL", in: handle) ?? CMARK_NODE_NONE - CMARK_NODE_STRIKETHROUGH = - findNodeType("CMARK_NODE_STRIKETHROUGH", in: handle) ?? CMARK_NODE_NONE - - dlclose(handle) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/Utils/Ext+Array.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/Utils/Ext+Array.swift deleted file mode 100644 index ff19cb1973..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownParser/Utils/Ext+Array.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Ext+Array.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation - -extension Array { - subscript(safe index: Int) -> Element? { - guard index >= 0, index < count else { return nil } - return self[index] - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BlockquoteView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BlockquoteView.swift deleted file mode 100644 index 4ee0be808f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BlockquoteView.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// BlockquoteView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class BlockquoteView: BlockView { - var manifest: Manifest { _manifest as! Manifest } - - let backgroundView = UIView() - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(backgroundView) - backgroundView.backgroundColor = .gray.withAlphaComponent(0.05) - backgroundView.layer.cornerRadius = 8 - backgroundView.clipsToBounds = true - backgroundView.layer.masksToBounds = true - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - backgroundView.frame = bounds - childrenViews.first?.frame = manifest.childrenGroupRect - childrenContainer.frame = bounds - } - - override func viewDidUpdate() { - super.viewDidUpdate() - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension BlockquoteView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var childrenGroupWidth: CGFloat { - max(0, size.width - spacings.general * 2) - } - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth + spacings.general * 2 - } - - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenGroupRect: CGRect = .zero - - required init() {} - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .blockquote(children) = block else { - assertionFailure() - return - } - childrenGroup.setChildren(nodes: children) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(childrenGroupWidth) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - childrenGroup.layoutIfNeeded() - size.height = childrenGroup.size.height + spacings.general * 2 - childrenGroupRect = CGRect( - x: spacings.general, - y: spacings.general, - width: childrenGroup.size.width, - height: childrenGroup.size.height - ) - } - - func determineViewType() -> BlockView.Type { - BlockquoteView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BulletedItemView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BulletedItemView.swift deleted file mode 100644 index c65dd58c34..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BulletedItemView.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// BulletedItemView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class BulletedItemView: BlockView { - let bulletedIcon = CircleView() - var manifest: Manifest { _manifest as! Manifest } - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(bulletedIcon) - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - bulletedIcon.frame = manifest.iconRect - childrenViews.first?.frame = manifest.childrenRect - childrenContainer.frame = bounds - } - - override func viewDidUpdate() { - super.viewDidUpdate() - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension BulletedItemView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth + iconLayoutGuideRect.width - } - - var iconLayoutGuideRect: CGRect { - .init(x: 0, y: 0, width: sizes.bullet + spacings.list, height: fonts.body.baseLineHeight) - } - - var iconRect: CGRect = .zero - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenRect: CGRect = .zero - - var childrenGroupWidth: CGFloat { - max(0, size.width - iconLayoutGuideRect.width) - } - - required init() { - childrenGroup.overrideGroupSpacing = 0 - } - - func load(block _: BlockNode) { - assertionFailure("should not be called") - } - - func setItems(_ items: RawListItem) { - dirty = true - childrenGroup.setChildren(nodes: items.children) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(childrenGroupWidth) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let iconLayoutGuideRect = iconLayoutGuideRect - - childrenGroup.layoutIfNeeded() - size.height = max(iconLayoutGuideRect.height, childrenGroup.size.height) - iconRect = CGRect( - x: 0, - y: (iconLayoutGuideRect.height - sizes.bullet) / 2, - width: sizes.bullet, - height: sizes.bullet - ) - childrenRect = CGRect( - x: iconLayoutGuideRect.maxX, - y: 0, - width: childrenGroup.size.width, - height: childrenGroup.size.height - ) - } - - func determineViewType() -> BlockView.Type { - BulletedItemView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BulletedListView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BulletedListView.swift deleted file mode 100644 index 371755465c..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/BulletedListView.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// BulletedListView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class BulletedListView: BlockView { - var manifest: Manifest { _manifest as! Manifest } - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - childrenViews.first?.frame = manifest.childrenGroupRect - childrenContainer.frame = bounds - } - - override func viewDidUpdate() { - super.viewDidUpdate() - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension BulletedListView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth - } - - required init() { - childrenGroup.overrideGroupSpacing = 0 - } - - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenGroupRect: CGRect = .zero - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .bulletedList(_, items) = block else { - assertionFailure() - return - } - childrenGroup.setChildren(manifests: items.map { listItem in - let manifest = BulletedItemView.Manifest() - manifest.setItems(listItem) - return manifest - }) - childrenGroup.setLayoutWidth(size.width) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(width) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - childrenGroup.layoutIfNeeded() - size.height = childrenGroup.size.height - childrenGroupRect = CGRect( - x: 0, - y: 0, - width: size.width, - height: size.height - ) - } - - func determineViewType() -> BlockView.Type { - BulletedListView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/CodeBlockView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/CodeBlockView.swift deleted file mode 100644 index b3cbcf1c11..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/CodeBlockView.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// CodeBlockView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import Splash -import UIKit - -class CodeBlockView: BlockView { - let backgroundView = UIView() - let fenceView = UIView() - let fenceLabel = TextLabel() - let fenceCopyButton = UIButton() - let scrollView = UIScrollView() - let codeTextView = TextLabel() - - var manifest: Manifest { _manifest as! Manifest } - - override func viewDidLoad() { - super.viewDidLoad() - clipsToBounds = true - layer.cornerRadius = 8 - layer.masksToBounds = true - - addSubview(backgroundView) - backgroundView.backgroundColor = .gray.withAlphaComponent(0.05) - - addSubview(fenceView) - fenceView.backgroundColor = .gray.withAlphaComponent(0.05) - - addSubview(fenceCopyButton) - fenceCopyButton.setImage(UIImage(systemName: "doc.on.doc"), for: .normal) - fenceCopyButton.tintColor = .label - fenceCopyButton.addTarget(self, action: #selector(copyButtonTapped), for: .touchUpInside) - fenceCopyButton.imageView?.contentMode = .scaleAspectFit - - addSubview(fenceLabel) - fenceLabel.isSelectable = false - fenceLabel.textColor = .label - fenceLabel.font = .preferredFont(forTextStyle: .caption1) - fenceLabel.textAlignment = .left - - addSubview(scrollView) - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.addSubview(codeTextView) - - scrollView.clipsToBounds = false - scrollView.layer.masksToBounds = false - scrollView.bringSubviewToFront(codeTextView) - } - - override func viewDidLayout() { - super.viewDidLayout() - backgroundView.frame = bounds - fenceView.frame = manifest.fenceRect - fenceLabel.frame = manifest.fenceLabelRect - fenceCopyButton.frame = manifest.fenceCopyButtonRect - scrollView.frame = manifest.scrollRect - codeTextView.frame = manifest.codeContentRect - scrollView.contentSize = manifest.scrollableContentSize - } - - override func viewDidUpdate() { - super.viewDidUpdate() - fenceLabel.attributedText = manifest.fenceInfo - codeTextView.attributedText = manifest.content - } - - @objc func copyButtonTapped() { - UIPasteboard.general.string = manifest.content.string - fenceCopyButton.setImage(UIImage(systemName: "checkmark"), for: .normal) - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(setButtonImageBack), object: nil) - perform(#selector(setButtonImageBack), with: nil, afterDelay: 1) - } - - @objc func setButtonImageBack() { - fenceCopyButton.setImage(UIImage(systemName: "doc.on.doc"), for: .normal) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension CodeBlockView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - content.measureWidth() + spacings.general * 4 - } - - var fenceInfo: NSAttributedString = .init() - var content: NSAttributedString = .init() - - var fenceRect: CGRect = .zero - var fenceCopyButtonRect: CGRect = .zero - var fenceLabelRect: CGRect = .zero - var scrollRect: CGRect = .zero - var codeContentRect: CGRect = .zero - var scrollableContentSize: CGSize = .zero - - required init() {} - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .codeBlock(info, content) = block else { - assertionFailure() - return - } - var infoText: String = info ?? "" - infoText = infoText.trimmingCharacters(in: .whitespacesAndNewlines) - if infoText.isEmpty { infoText = "#" } - fenceInfo = NSAttributedString(string: infoText, attributes: [ - .font: theme.fonts.code, - .foregroundColor: theme.colors.body, - .originalFont: theme.fonts.code, - ]) - let code = content.trimmingCharacters(in: .whitespacesAndNewlines) - let codeTheme = theme.codeTheme(withFont: theme.fonts.code) - let output = AttributedStringOutputFormat(theme: codeTheme) - let result: NSMutableAttributedString? - switch info?.lowercased() { - case "swift": - let splash = SyntaxHighlighter(format: output, grammar: SwiftGrammar()) - result = splash.highlight(code).mutableCopy() as? NSMutableAttributedString - default: - let splash = SyntaxHighlighter(format: output) - result = splash.highlight(code).mutableCopy() as? NSMutableAttributedString - } - let defaultAttrs: [NSAttributedString.Key: Any] = [ - .font: theme.fonts.code, - .originalFont: theme.fonts.code, - ] - result?.addAttributes(defaultAttrs, range: NSRange(location: 0, length: result?.length ?? 0)) - self.content = result ?? .init(string: code, attributes: defaultAttrs) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let fenceLabelHeight = fenceInfo.measureHeight(usingWidth: .greatestFiniteMagnitude) - let fenceHeight = fenceLabelHeight + spacings.general * 2 - let fenceCopyButtonSize = fenceLabelHeight - fenceRect = CGRect( - x: 0, - y: 0, - width: size.width, - height: fenceHeight - ) - fenceLabelRect = .init( - x: spacings.general, - y: spacings.general, - width: size.width - fenceCopyButtonSize - spacings.general * 2, - height: fenceLabelHeight - ) - fenceCopyButtonRect = .init( - x: size.width - fenceCopyButtonSize - spacings.general, - y: fenceLabelRect.minY, - width: fenceCopyButtonSize, - height: fenceCopyButtonSize - ) - let contentWidth = content.measureWidth() - let contentHeight = content.measureHeight(usingWidth: .greatestFiniteMagnitude) - scrollRect = .init( - x: 0, - y: fenceRect.maxY, - width: size.width, - height: contentHeight + spacings.general * 2 - ) - size.height = scrollRect.maxY - codeContentRect = .init( - x: spacings.general, - y: spacings.general, - width: contentWidth + spacings.general * 2, - height: contentHeight - ) - scrollableContentSize = .init( - width: codeContentRect.maxX + spacings.general, - height: codeContentRect.maxY + spacings.general - ) - } - - func determineViewType() -> BlockView.Type { - CodeBlockView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/GroupBlockView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/GroupBlockView.swift deleted file mode 100644 index 9d7f1a7977..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/GroupBlockView.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// GroupBlockView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class GroupBlockView: BlockView { - var manifest: Manifest { _manifest as! Manifest } - - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - childrenContainer.frame = bounds - for (index, view) in childrenViews.enumerated() { - view.frame = manifest.children[index].rect - } - } - - override func viewDidUpdate() { - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: manifest.children.map(\.manifest) - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension GroupBlockView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - children.map(\.manifest.intrinsicWidth).max() ?? 0 - } - - var overrideGroupSpacing: CGFloat? = nil { - didSet { dirty = true } - } - - var spacing: CGFloat { - if let overrideGroupSpacing { return overrideGroupSpacing } - return theme.spacings.general - } - - required init() {} - - func load(block _: BlockNode) { - assertionFailure("should not be called") - } - - var children: [Child] = [] { - didSet { dirty = true } - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - children.forEach { $0.manifest.setLayoutWidth(width) } - } - - func setChildren(manifests: [AnyBlockManifest]) { - dirty = true - children = manifests.map { Child(manifest: $0) } - } - - func setChildren(nodes: [BlockNode]) { - setChildren(manifests: nodes.map { $0.manifest(theme: theme) }) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - children.forEach { $0.manifest.setLayoutTheme(theme) } - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - var anchor: CGFloat = 0 - for child in children { - child.manifest.setLayoutWidth(size.width) - child.manifest.layoutIfNeeded() - child.rect = CGRect( - x: 0, - y: anchor + spacing, - width: child.manifest.size.width, - height: child.manifest.size.height - ) - anchor = child.rect.maxY - } - size.height = anchor - } - - func determineViewType() -> BlockView.Type { - GroupBlockView.self - } - } -} - -extension GroupBlockView.Manifest { - class Child { - let manifest: AnyBlockManifest - var rect: CGRect - - init(manifest: AnyBlockManifest, rect: CGRect = .zero) { - self.manifest = manifest - self.rect = rect - } - } - - func build(reusingChildren: [Child], forManifests manifests: [AnyBlockManifest]) -> [Child] { - var ans = [Child]() - for idx in manifests.indices { - let manifest = manifests[idx] - if let child = reusingChildren[safe: idx], type(of: child.manifest) == type(of: manifest) { - ans.append(child) - } else { - ans.append(Child(manifest: manifest)) - } - } - return ans - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/HTMLBlockView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/HTMLBlockView.swift deleted file mode 100644 index 4c4e2cec4a..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/HTMLBlockView.swift +++ /dev/null @@ -1,90 +0,0 @@ -//// -//// HTMLBlockView.swift -//// MarkdownView -//// -//// Created by 秋星桥 on 2025/1/3. -//// -// -// import Foundation -// import MarkdownParser -// import UIKit -// -// class HTMLBlockView: BlockView { -// let text = TextView() -// var manifest: Manifest { _manifest as! Manifest } -// -// override func viewDidLoad() { -// super.viewDidLoad() -// addSubview(text) -// text.isEditable = false -// text.isSelectable = true -// text.isScrollEnabled = false -// } -// -// override func viewDidLayout() { -// super.viewDidLayout() -// text.frame = manifest.contentRect -// } -// -// override func viewDidUpdate() { -// super.viewDidUpdate() -// text.attributedText = manifest.content -// } -// -// override func accept(_ manifest: AnyBlockManifest) -> Bool { -// manifest is Manifest -// } -// } -// -// extension HTMLBlockView { -// class Manifest: BlockManifest { -// var size: CGSize = .zero -// var theme: Theme = .default -// var dirty: Bool = true -// -// var intrinsicWidth: CGFloat { -// size.width -// } -// -// var content: NSMutableAttributedString = .init() -// var contentRect: CGRect = .zero -// -// required init() {} -// -// var block: BlockNode? = nil -// func load(block: BlockNode) { -// guard self.block != block else { return } -// dirty = true -// self.block = block -// guard case let .htmlBlock(contents) = block else { -// assertionFailure() -// return -// } -// let htmlData = NSString(string: contents).data(using: String.Encoding.unicode.rawValue) -// let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html] -// let ans = try? NSMutableAttributedString( -// data: htmlData ?? Data(), -// options: options, -// documentAttributes: nil -// ) -// let content = ans ?? .init() -// content.addAttributes( -// [.originalFont: theme.fonts.body], -// range: .init(location: 0, length: content.length) -// ) -// self.content = content -// } -// -// func layoutIfNeeded() { -// guard dirty, size.width > 0 else { return } -// defer { dirty = false } -// let textHeight = content.measureHeight(usingWidth: size.width) -// contentRect = .init(x: 0, y: 0, width: size.width, height: textHeight) -// size.height = textHeight -// } -// -// func determineViewType() -> BlockView.Type { -// HTMLBlockView.self -// } -// } -// } diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/HeadingView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/HeadingView.swift deleted file mode 100644 index ac057c6b7b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/HeadingView.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// HeadingView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class HeadingView: BlockView { - let text = TextLabel() - var manifest: Manifest { _manifest as! Manifest } - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(text) - } - - override func viewDidLayout() { - super.viewDidLayout() - text.frame = manifest.contentRect - } - - override func viewDidUpdate() { - super.viewDidUpdate() - text.attributedText = manifest.content - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension HeadingView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - size.width - } - - var content: NSMutableAttributedString = .init() - var contentRect: CGRect = .zero - - required init() {} - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .heading(level, inlines) = block else { - assertionFailure() - return - } - let attrText = inlines.render(theme: theme) - var supposeFont: UIFont = theme.fonts.title - if level <= 1 { - supposeFont = theme.fonts.largeTitle - } - attrText.addAttributes( - [ - .font: supposeFont, - .originalFont: supposeFont, - .foregroundColor: theme.colors.body, - ], - range: .init(location: 0, length: attrText.length) - ) - content = attrText - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let textHeight = content.measureHeight(usingWidth: size.width) - contentRect = .init(x: 0, y: 0, width: size.width, height: textHeight) - size.height = textHeight - } - - func determineViewType() -> BlockView.Type { - HeadingView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/NumberedItemView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/NumberedItemView.swift deleted file mode 100644 index 4a6dabc22f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/NumberedItemView.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// BulletedItemView 2.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class NumberedItemView: BlockView { - let numberView = TextLabel() - var manifest: Manifest { _manifest as! Manifest } - - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - numberView.textAlignment = .left - numberView.font = .preferredFont(forTextStyle: .body) - numberView.textColor = .label - addSubview(numberView) - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - childrenContainer.frame = bounds - numberView.frame = manifest.iconRect - childrenViews.first?.frame = manifest.childrenRect - } - - override func viewDidUpdate() { - super.viewDidUpdate() - numberView.attributedText = manifest.number - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension NumberedItemView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth + iconLayoutGuideRect.width - } - - var number: NSAttributedString = .init() - var iconRect: CGRect = .zero - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenRect: CGRect = .zero - - var iconWidth: CGFloat { - "99.".size(withAttributes: [.font: theme.fonts.body]).width - } - - var iconLayoutGuideRect: CGRect { - .init(x: 0, y: 0, width: iconWidth + spacings.list, height: fonts.body.baseLineHeight) - } - - var childrenGroupWidth: CGFloat { - max(0, size.width - iconLayoutGuideRect.width) - } - - required init() { - childrenGroup.overrideGroupSpacing = 0 - } - - func load(block _: BlockNode) { - assertionFailure("should not be called") - } - - func setNumber(_ number: Int) { - dirty = true - self.number = NSMutableAttributedString(string: "\(number).", attributes: [ - .font: theme.fonts.body, - .originalFont: theme.fonts.body, - .foregroundColor: UIColor.label, - ]) - } - - func setItems(_ items: RawListItem) { - dirty = true - childrenGroup.setChildren(nodes: items.children) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(childrenGroupWidth) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let iconLayoutGuideRect = iconLayoutGuideRect - - childrenGroup.setLayoutWidth(childrenGroupWidth) - childrenGroup.layoutIfNeeded() - size.height = max(iconLayoutGuideRect.height, childrenGroup.size.height) - iconRect = CGRect( - x: 0, - y: (iconLayoutGuideRect.height - iconLayoutGuideRect.height) / 2, - width: iconWidth, - height: iconLayoutGuideRect.height - ) - childrenRect = CGRect( - x: iconLayoutGuideRect.maxX, - y: 0, - width: childrenGroup.size.width, - height: childrenGroup.size.height - ) - } - - func determineViewType() -> BlockView.Type { - NumberedItemView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/NumberedListView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/NumberedListView.swift deleted file mode 100644 index d94a0479cb..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/NumberedListView.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// NumberedListView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class NumberedListView: BlockView { - var manifest: Manifest { _manifest as! Manifest } - - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - childrenContainer.frame = bounds - childrenViews.first?.frame = manifest.childrenGroupRect - } - - override func viewDidUpdate() { - super.viewDidUpdate() - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension NumberedListView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth - } - - required init() { - childrenGroup.overrideGroupSpacing = 0 - } - - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenGroupRect: CGRect = .zero - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .numberedList(_, start, items) = block else { - assertionFailure() - return - } - var number = start - childrenGroup.setChildren(manifests: items.map { listItem in - defer { number += 1 } - let manifest = NumberedItemView.Manifest() - manifest.setNumber(number) - manifest.setItems(listItem) - return manifest - }) - childrenGroup.setLayoutWidth(size.width) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(width) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - childrenGroup.layoutIfNeeded() - size.height = childrenGroup.size.height - childrenGroupRect = CGRect( - x: 0, - y: 0, - width: size.width, - height: size.height - ) - } - - func determineViewType() -> BlockView.Type { - NumberedListView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/ParagraphView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/ParagraphView.swift deleted file mode 100644 index 8c4074802e..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/ParagraphView.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// ParagraphView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class ParagraphView: BlockView { - let text = TextLabel() - var manifest: Manifest { _manifest as! Manifest } - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(text) - } - - override func viewDidLayout() { - super.viewDidLayout() - text.frame = manifest.contentRect - } - - override func viewDidUpdate() { - super.viewDidUpdate() - text.attributedText = manifest.content - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension ParagraphView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - content.measureWidth() - } - - var content: NSMutableAttributedString = .init() - var contentRect: CGRect = .zero - - required init() {} - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .paragraph(contents) = block else { - assertionFailure() - return - } - content = contents.render(theme: theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let textHeight = content.measureHeight(usingWidth: size.width) - contentRect = .init(x: 0, y: 0, width: size.width, height: textHeight) - size.height = textHeight - } - - func determineViewType() -> BlockView.Type { - ParagraphView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockManifest.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockManifest.swift deleted file mode 100644 index 945e304b9f..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockManifest.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// BlockManifest.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -public typealias AnyBlockManifest = any BlockManifest - -public protocol BlockManifest: AnyObject { - var size: CGSize { get set } - var theme: Theme { get set } - var dirty: Bool { get set } - - var intrinsicWidth: CGFloat { get } - - init() - - func setLayoutWidth(_ width: CGFloat) - func setLayoutTheme(_ theme: Theme) - func load(block: BlockNode) - func layoutIfNeeded() - - @inline(__always) func determineViewType() -> BlockView.Type -} - -public extension BlockManifest { - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - size.width = width - size.height = .zero - dirty = true - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - self.theme = theme - dirty = true - } - - func layoutIfNeeded() { - dirty = false - } -} - -extension BlockManifest { - var fonts: Theme.Fonts { theme.fonts } - var colors: Theme.Colors { theme.colors } - var spacings: Theme.Spacings { theme.spacings } - var sizes: Theme.Sizes { theme.sizes } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockView+DiffableUpdate.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockView+DiffableUpdate.swift deleted file mode 100644 index 1720a51b7d..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockView+DiffableUpdate.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// BlockView+DiffableUpdate.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation -import UIKit - -extension UIView { - func diffableUpdate(reusingViews blockViews: inout [BlockView], manifests: [AnyBlockManifest]) { - defer { - assert(blockViews.count == manifests.count, "blockViews count must match manifests count") - } - var shouldRemovedIndices: [Int] = [] - defer { - for index in shouldRemovedIndices.sorted(by: >) { - blockViews.remove(at: index) - } - } - for idx in 0 ..< max(blockViews.count, manifests.count) { - guard let manifest = manifests[safe: idx] else { - if let view = blockViews[safe: idx] { - view.removeFromSuperview() - shouldRemovedIndices.append(idx) - } - continue - } - lazy var view = { - let view = manifest.determineViewType().init(manifest: manifest) - addSubview(view) - return view - }() - if let currentView = blockViews[safe: idx] { - if currentView.accept(manifest) { - currentView.set(manifest) - continue - } else { - currentView.removeFromSuperview() - blockViews[idx] = view - } - } else { - addSubview(view) - blockViews.insert(view, at: idx) - } - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockView.swift deleted file mode 100644 index 2b4951b947..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/Supplements/BlockView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// BlockView.swift -// FlowMarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation -import MarkdownParser -import UIKit - -public class BlockView: UIView { - private(set) var _manifest: AnyBlockManifest - required init(manifest: AnyBlockManifest) { - _manifest = manifest - super.init(frame: .zero) - backgroundColor = .clear - -// NSLog("[*] \(type(of: self)) was initialized at \(Date()) \(debugDescription)") - - viewDidLoad() - viewDidUpdate() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - override public func action(for _: CALayer, forKey _: String) -> (any CAAction)? { - nil - } - - override public func layoutSubviews() { - super.layoutSubviews() - assert(_manifest.size.width >= 0, "\(type(of: self))'s manifest has invalid size") - assert(_manifest.size.height >= 0, "\(type(of: self))'s manifest has invalid size") - viewDidLayout() - } - - func viewDidLoad() { - assert(Thread.isMainThread) - } - - func viewDidUpdate() { - assert(Thread.isMainThread) - } - - func viewDidLayout() { - assert(Thread.isMainThread) - } - - func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } - - func set(_ manifest: AnyBlockManifest) { - _manifest = manifest - viewDidUpdate() - setNeedsLayout() - } -} - -public extension BlockView { - class Manifest: BlockManifest { - public var size: CGSize = .zero - public var theme: Theme = .default - public var dirty: Bool = true - - public var intrinsicWidth: CGFloat { 0 } - - public required init() {} - - public func load(block _: BlockNode) {} - - public func layoutIfNeeded() { - dirty = false - } - - public func determineViewType() -> BlockView.Type { - BlockView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TableView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TableView.swift deleted file mode 100644 index 577d517b8b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TableView.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// TableView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class TableView: BlockView { - var manifest: Manifest { _manifest as! Manifest } - var childrenViews: [TextLabel] = [] - let scrollView = UIScrollView() - let gridView = GridView() - - override func viewDidLoad() { - super.viewDidLoad() - scrollView.isScrollEnabled = true - scrollView.contentInset = .zero - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.alwaysBounceVertical = false - scrollView.alwaysBounceHorizontal = false - scrollView.clipsToBounds = true - scrollView.backgroundColor = .clear - addSubview(scrollView) - scrollView.addSubview(gridView) - } - - override func viewDidLayout() { - super.viewDidLayout() - scrollView.frame = bounds - - let flatCells = manifest.cells.flatMap(\.self) - for (index, view) in childrenViews.enumerated() { - view.frame = flatCells[index].contentRect - } - gridView.frame = .init( - x: 0, - y: 0, - width: scrollView.contentSize.width, - height: scrollView.contentSize.height - ) - - scrollView.contentSize = manifest.contentSize - } - - override func viewDidUpdate() { - super.viewDidUpdate() - let currentCells = childrenViews - let targetCells = manifest.cells.flatMap(\.self) - for idx in 0 ..< max(currentCells.count, targetCells.count) { - if let target = targetCells[safe: idx] { - if let current = currentCells[safe: idx] { - current.attributedText = target.content - current.frame = target.rect - } else { - let view = TextLabel(frame: target.rect) - view.attributedText = target.content - scrollView.addSubview(view) - childrenViews.append(view) - } - } else { - currentCells[safe: idx]?.removeFromSuperview() - } - } - gridView.lines = manifest.drawLine.map { start, end in - .init(start: start, end: end) - } - scrollView.contentSize = manifest.contentSize - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension TableView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - contentSize.width - } - - var cells: [[Cell]] = [] - var drawLine: [(CGPoint, CGPoint)] = [] - - var contentSize: CGSize { - let rect = cells.last?.last?.rect ?? .zero - return .init(width: rect.maxX, height: rect.maxY) - } - - required init() {} - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .table(_, rawCells) = block else { - assertionFailure() - return - } - - cells = rawCells.map { row in - row.cells.map { cell in - let content = cell.content.render(theme: theme) - return Cell(content: content, rect: .zero) - } - } - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let cols = cells.first?.count ?? 0 - let rows = cells.count - drawLine.removeAll() - - guard rows > 0, cols > 0 else { - size.height = 0 - return - } - - // first pass calculate intrinsic width of each column to get the width - var colWidths: [Int: CGFloat] = [:] - var rowHeights: [Int: CGFloat] = [:] - for col in 0 ..< cols { - for row in 0 ..< rows { - let cell = cells[row][col] - colWidths[col] = max(colWidths[col] ?? 0, cell.intrinsicSize.width + 32) - rowHeights[row] = max(rowHeights[row] ?? 0, cell.intrinsicSize.height + 16) - } - } - - // now calculate the rects and points - var anchorY: CGFloat = 0 - var linePoints: [CGFloat] = [0] - var colPoints: [CGFloat] = [0] - for rowIdx in 0 ..< rows { - var anchorX: CGFloat = 0 - let rowHeight = rowHeights[rowIdx] ?? 0 - linePoints.append(anchorY) - for colIdx in 0 ..< cols { // column - let colWidth = colWidths[colIdx] ?? 0 - if rowIdx == 0 { colPoints.append(anchorX) } - let rect = CGRect(x: anchorX, y: anchorY, width: colWidth, height: rowHeight) - cells[rowIdx][colIdx].rect = rect - anchorX = rect.maxX - } - colPoints.append(anchorX + spacings.general) - anchorY += rowHeight - } - linePoints.append(anchorY) - - for x in colPoints { - drawLine.append((CGPoint(x: x, y: 0), CGPoint(x: x, y: linePoints.last ?? 0))) - } - - for y in linePoints { - drawLine.append((CGPoint(x: 0, y: y), CGPoint(x: colPoints.last ?? 0, y: y))) - } - - size.height = contentSize.height - } - - func determineViewType() -> BlockView.Type { - TableView.self - } - } -} - -extension TableView.Manifest { - class Cell { - let content: NSMutableAttributedString - var rect: CGRect { didSet { updateContentRect() } } - let intrinsicSize: CGSize - var contentRect: CGRect - init( - content: NSMutableAttributedString, - rect: CGRect = .zero - ) { - self.content = content - self.rect = rect - intrinsicSize = .init( - width: content.measureWidth(), - height: content.measureHeight(usingWidth: .greatestFiniteMagnitude) - ) - contentRect = .zero - } - - func updateContentRect() { - contentRect = .init( - x: rect.minX + (rect.width - intrinsicSize.width) / 2, - y: rect.minY + (rect.height - intrinsicSize.height) / 2, - width: intrinsicSize.width, - height: intrinsicSize.height - ) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TaskItemView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TaskItemView.swift deleted file mode 100644 index 350a0dc87b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TaskItemView.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// TaskItemView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class TaskItemView: BlockView { - let bulletedIcon = CircleView() - var manifest: Manifest { _manifest as! Manifest } - - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(bulletedIcon) - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - childrenContainer.frame = bounds - bulletedIcon.frame = manifest.iconRect - childrenViews.first?.frame = manifest.childrenRect - } - - override func viewDidUpdate() { - super.viewDidUpdate() - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension TaskItemView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth + iconLayoutGuideRect.width - } - - var iconLayoutGuideRect: CGRect { - .init(x: 0, y: 0, width: sizes.bullet + spacings.list, height: fonts.body.baseLineHeight) - } - - var iconRect: CGRect = .zero - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenRect: CGRect = .zero - - var childrenGroupWidth: CGFloat { - max(0, size.width - iconLayoutGuideRect.width) - } - - required init() { - childrenGroup.overrideGroupSpacing = 0 - } - - func load(block _: BlockNode) { - assertionFailure("should not be called") - } - - func setItems(_ items: RawTaskListItem) { - dirty = true - childrenGroup.setChildren(nodes: items.children) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(childrenGroupWidth) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - let iconLayoutGuideRect = iconLayoutGuideRect - - childrenGroup.layoutIfNeeded() - size.height = max(iconLayoutGuideRect.height, childrenGroup.size.height) - iconRect = CGRect( - x: 0, - y: (iconLayoutGuideRect.height - sizes.bullet) / 2, - width: sizes.bullet, - height: sizes.bullet - ) - childrenRect = CGRect( - x: iconLayoutGuideRect.maxX, - y: 0, - width: childrenGroup.size.width, - height: childrenGroup.size.height - ) - } - - func determineViewType() -> BlockView.Type { - TaskItemView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TaskListView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TaskListView.swift deleted file mode 100644 index e33c976fcb..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/TaskListView.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// TaskListView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class TaskListView: BlockView { - var manifest: Manifest { _manifest as! Manifest } - - let childrenContainer = UIView() - var childrenViews: [BlockView] = [] - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(childrenContainer) - } - - override func viewDidLayout() { - super.viewDidLayout() - childrenContainer.frame = bounds - childrenViews.first?.frame = manifest.childrenGroupRect - } - - override func viewDidUpdate() { - super.viewDidUpdate() - childrenContainer.diffableUpdate( - reusingViews: &childrenViews, - manifests: [manifest.childrenGroup] - ) - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension TaskListView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - childrenGroup.intrinsicWidth - } - - required init() { - childrenGroup.overrideGroupSpacing = 0 - } - - let childrenGroup: GroupBlockView.Manifest = .init() - var childrenGroupRect: CGRect = .zero - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case let .taskList(_, items) = block else { - assertionFailure() - return - } - childrenGroup.setChildren(manifests: items.map { listItem in - let manifest = TaskItemView.Manifest() - manifest.setItems(listItem) - return manifest - }) - childrenGroup.setLayoutWidth(size.width) - } - - func setLayoutWidth(_ width: CGFloat) { - guard size.width != width else { return } - assert(width >= 0) - dirty = true - size.width = width - childrenGroup.setLayoutWidth(width) - } - - func setLayoutTheme(_ theme: Theme) { - guard self.theme != theme else { return } - dirty = true - self.theme = theme - childrenGroup.setLayoutTheme(theme) - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - childrenGroup.layoutIfNeeded() - size.height = childrenGroup.size.height - childrenGroupRect = CGRect( - x: 0, - y: 0, - width: size.width, - height: size.height - ) - } - - func determineViewType() -> BlockView.Type { - TaskListView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/ThematicBreakView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/ThematicBreakView.swift deleted file mode 100644 index 0c7d70864d..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownBlockView/ThematicBreakView.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// ThematicBreakView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import MarkdownParser -import UIKit - -class ThematicBreakView: BlockView { - let separateView = UIView() - - override func viewDidLoad() { - super.viewDidLoad() - addSubview(separateView) - separateView.backgroundColor = .label.withAlphaComponent(0.1) - } - - override func viewDidLayout() { - super.viewDidLayout() - separateView.frame = bounds - } - - override func accept(_ manifest: AnyBlockManifest) -> Bool { - manifest is Manifest - } -} - -extension ThematicBreakView { - class Manifest: BlockManifest { - var size: CGSize = .zero - var theme: Theme = .default - var dirty: Bool = true - - var intrinsicWidth: CGFloat { - 32 - } - - required init() {} - - var block: BlockNode? = nil - func load(block: BlockNode) { - guard self.block != block else { return } - dirty = true - self.block = block - guard case .thematicBreak = block else { - assertionFailure() - return - } - } - - func layoutIfNeeded() { - guard dirty, size.width > 0 else { return } - defer { dirty = false } - size.height = 1 - } - - func determineViewType() -> BlockView.Type { - ThematicBreakView.self - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownView/MarkdownView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownView/MarkdownView.swift deleted file mode 100644 index 120475eb5b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/MarkdownView/MarkdownView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - -import MarkdownParser -import UIKit - -public class MarkdownView: UIView { - public var height: CGFloat = 0 - - var blockViews: [BlockView] = [] - - public var theme: Theme - public init(theme: Theme = .default) { - self.theme = theme - super.init(frame: .zero) - clipsToBounds = true - } - - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError() - } - - public func prepareForReuse() { - blockViews.forEach { $0.removeFromSuperview() } - blockViews.removeAll() - } - - public func updateContentViews(_ manifest: [AnyBlockManifest]) { - assert(Thread.isMainThread) - diffableUpdate(reusingViews: &blockViews, manifests: manifest) - var anchorY: CGFloat = 0 - for view in blockViews { - view.frame = CGRect( - x: 0, - y: anchorY, - width: view._manifest.size.width, - height: view._manifest.size.height - ) - anchorY = view.frame.maxY + theme.spacings.final - } - height = blockViews.map(\.frame.maxY).max() ?? 0 - assert(subviews.count == blockViews.count) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/CircleView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/CircleView.swift deleted file mode 100644 index 164831d90e..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/CircleView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CircleView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import UIKit - -class CircleView: UIView { - init() { - super.init(frame: .zero) - backgroundColor = .label - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = (frame.height + frame.width) / 4 - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+Array.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+Array.swift deleted file mode 100644 index f8892a85f6..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+Array.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Ext+Array.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation - -extension Array { - subscript(safe index: Int) -> Element? { - guard index >= 0, index < count else { return nil } - return self[index] - } -} - -private let maxConcurrentDefaultValue: Int = max(1, ProcessInfo.processInfo.processorCount) - -extension Array { - @inline(__always) - func splitInSubArrays(into size: Int) -> [[Element]] { - (0 ..< size).map { - stride(from: $0, to: count, by: size).map { self[$0] } - } - } - - func forParallelEach( - maxConcurrent: Int = maxConcurrentDefaultValue, - block: @escaping (Element) -> Void - ) { - assert(maxConcurrent > 0) - if count < maxConcurrent || maxConcurrent <= 1 { - for element in self { - block(element) - } - return - } - - let cuts = splitInSubArrays(into: maxConcurrent) - - let group = DispatchGroup() - for cut in cuts { - group.enter() - DispatchQueue.global().async { - cut.forEach(block) - group.leave() - } - } - group.wait() - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+BlockNode.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+BlockNode.swift deleted file mode 100644 index 7767ad2afb..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+BlockNode.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Ext+BlockNode.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation -import MarkdownParser - -public extension BlockNode { - var manifestType: BlockManifest.Type { - switch self { - case .blockquote: - BlockquoteView.Manifest.self - case .bulletedList: - BulletedListView.Manifest.self - case .numberedList: - NumberedListView.Manifest.self - case .taskList: - TaskListView.Manifest.self - case .codeBlock: - CodeBlockView.Manifest.self -// case .htmlBlock: -// HTMLBlockView.Manifest.self - case .paragraph: - ParagraphView.Manifest.self - case .heading: - HeadingView.Manifest.self - case .table: - TableView.Manifest.self - case .thematicBreak: - ThematicBreakView.Manifest.self - } - } - - func manifest(theme: Theme) -> AnyBlockManifest { - let object = manifestType.init() - object.setLayoutTheme(theme) - object.load(block: self) - return object - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+DispatchQueue.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+DispatchQueue.swift deleted file mode 100644 index 92fe2432be..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+DispatchQueue.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Ext+DispatchQueue.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation - -public extension DispatchQueue { - static func isCurrent(_ queue: DispatchQueue) -> Bool { - let key = DispatchSpecificKey() - queue.setSpecific(key: key, value: ()) - defer { queue.setSpecific(key: key, value: nil) } - return DispatchQueue.getSpecific(key: key) != nil - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+InlineNode.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+InlineNode.swift deleted file mode 100644 index b7701bb36d..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+InlineNode.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// Ext+InlineNode.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation -import MarkdownParser -import UIKit - -extension [InlineNode] { - func render(theme: Theme) -> NSMutableAttributedString { - let result = NSMutableAttributedString() - for node in self { - result.append(node.render(theme: theme)) - } - return result - } -} - -extension InlineNode { - func render(theme: Theme) -> NSAttributedString { - switch self { - case let .text(string): - return NSAttributedString( - string: string, - attributes: [ - .font: theme.fonts.body, - .foregroundColor: theme.colors.body, - .originalFont: theme.fonts.body, - ] - ) - case .softBreak: - return NSAttributedString(string: " ", attributes: [ - .font: theme.fonts.body, - .foregroundColor: theme.colors.body, - .originalFont: theme.fonts.body, - ]) - case .lineBreak: - return NSAttributedString(string: "\n", attributes: [ - .font: theme.fonts.body, - .foregroundColor: theme.colors.body, - .originalFont: theme.fonts.body, - ]) - case let .code(string): - return NSAttributedString( - string: "\(string)", - attributes: [ - .font: theme.fonts.codeInline, - .originalFont: theme.fonts.codeInline, - .foregroundColor: theme.colors.code, - .backgroundColor: theme.colors.codeBackground.withAlphaComponent(0.05), - ] - ) - case let .html(content): - return NSAttributedString( - string: "\(content)", - attributes: [ - .font: theme.fonts.codeInline, - .originalFont: theme.fonts.codeInline, - .foregroundColor: theme.colors.code, - .backgroundColor: theme.colors.codeBackground.withAlphaComponent(0.05), - ] - ) -// let htmlData = NSString(string: content).data(using: String.Encoding.unicode.rawValue) -// let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html] -// let ans = try? NSMutableAttributedString( -// data: htmlData ?? Data(), -// options: options, -// documentAttributes: nil -// ) -// return ans ?? .init() - case let .emphasis(children): - let ans = NSMutableAttributedString() - children.map { $0.render(theme: theme) }.forEach { ans.append($0) } - ans.addAttributes( - [ - .underlineStyle: NSUnderlineStyle.thick.rawValue, - .underlineColor: theme.colors.emphasis, - ], - range: NSRange(location: 0, length: ans.length) - ) - return ans - case let .strong(children): - let ans = NSMutableAttributedString() - children.map { $0.render(theme: theme) }.forEach { ans.append($0) } - ans.addAttributes( - [.font: theme.fonts.bold], - range: NSRange(location: 0, length: ans.length) - ) - return ans - case let .strikethrough(children): - let ans = NSMutableAttributedString() - children.map { $0.render(theme: theme) }.forEach { ans.append($0) } - ans.addAttributes( - [.strikethroughStyle: NSUnderlineStyle.thick.rawValue], - range: NSRange(location: 0, length: ans.length) - ) - return ans - case let .link(destination, children): - let ans = NSMutableAttributedString() - children.map { $0.render(theme: theme) }.forEach { ans.append($0) } - ans.addAttributes( - [.link: destination], - range: NSRange(location: 0, length: ans.length) - ) - return ans - case let .image(source, _): // children => alternative text can be ignored? - return NSAttributedString( - string: source, - attributes: [ - .link: source, - .font: theme.fonts.body, - .originalFont: theme.fonts.body, - .foregroundColor: theme.colors.body, - ] - ) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+NSAttributedString.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+NSAttributedString.swift deleted file mode 100644 index 26f77bc7b2..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+NSAttributedString.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Ext+NSAttributedString.swift -// FlowDown -// -// Created by 秋星桥 on 2025/1/2. -// - -import UIKit - -class TextMeasurementHelper { - static let shared = TextMeasurementHelper() - - private var textStorage: NSTextStorage - private var textContainer: NSTextContainer - private var layoutManager: NSLayoutManager - - private let lock = NSLock() - - init() { - textStorage = NSTextStorage() - textContainer = NSTextContainer(size: CGSize(width: CGFloat.infinity, height: CGFloat.infinity)) - layoutManager = NSLayoutManager() - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - textContainer.lineFragmentPadding = 0 - } - - func measureSize( - of attributedString: NSAttributedString, - usingWidth width: CGFloat, - lineLimit: Int = 0, - lineBreakMode: NSLineBreakMode = .byTruncatingTail - ) -> CGSize { - lock.lock() - defer { lock.unlock() } - - textContainer.size = CGSize(width: width, height: .infinity) - textContainer.maximumNumberOfLines = lineLimit - textContainer.lineBreakMode = lineBreakMode - textStorage.beginEditing() - textStorage.setAttributedString(attributedString) - textStorage.endEditing() - - let size = layoutManager.usedRect(for: textContainer).size - return .init(width: ceil(size.width), height: ceil(size.height)) - } -} - -extension NSAttributedString: @unchecked @retroactive Sendable {} - -public extension NSAttributedString { - func measureWidth() -> CGFloat { - if string.trimmingCharacters(in: .whitespacesAndNewlines).count <= 0 { - return 0 - } - return TextMeasurementHelper.shared.measureSize( - of: self, - usingWidth: .infinity - ).width - } - - func measureHeight( - usingWidth width: CGFloat, - lineLimit: Int = 0, - lineBreakMode: NSLineBreakMode = .byTruncatingTail - ) -> CGFloat { - if string.trimmingCharacters(in: .whitespacesAndNewlines).count <= 0 { - return 0 - } - - return TextMeasurementHelper.shared.measureSize( - of: self, - usingWidth: width, - lineLimit: lineLimit, - lineBreakMode: lineBreakMode - ).height - } -} - -public extension NSAttributedString.Key { - @inline(__always) static let coreTextRunDelegate = NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String) - @inline(__always) static let originalFont = NSAttributedString.Key(rawValue: "NSOriginalFont") -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+UIColor.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+UIColor.swift deleted file mode 100644 index 8dc4bf6224..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+UIColor.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Ext+UIColor.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/7. -// - -import UIKit - -extension UIColor { - convenience init(light: UIColor, dark: UIColor) { - if #available(iOS 13.0, tvOS 13.0, *) { - self.init(dynamicProvider: { $0.userInterfaceStyle == .dark ? dark : light }) - } else { - self.init(cgColor: light.cgColor) - } - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+UIFont.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+UIFont.swift deleted file mode 100644 index 574acc0453..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Ext+UIFont.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Ext+UIFont.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import UIKit - -public extension UIFont { - var bold: UIFont { - UIFont(descriptor: fontDescriptor.withSymbolicTraits(.traitBold)!, size: 0) - } - - var italic: UIFont { - UIFont(descriptor: fontDescriptor.withSymbolicTraits(.traitItalic)!, size: 0) - } - - var monospaced: UIFont { - let settings = [[ - UIFontDescriptor.FeatureKey.featureIdentifier: kNumberSpacingType, - UIFontDescriptor.FeatureKey.typeIdentifier: kMonospacedNumbersSelector, - ]] - - let attributes = [UIFontDescriptor.AttributeName.featureSettings: settings] - let newDescriptor = fontDescriptor.addingAttributes(attributes) - return UIFont(descriptor: newDescriptor, size: 0) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/GridView.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/GridView.swift deleted file mode 100644 index f1df4a5b00..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/GridView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// GridView.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import UIKit - -class GridView: UIView { - var stokeColor: UIColor = .label.withAlphaComponent(0.25) { - didSet { - layer.borderWidth = 1 - layer.borderColor = stokeColor.cgColor - setNeedsDisplay() - } - } - - var lines: [CGPointPair] = [] { - didSet { setNeedsDisplay() } - } - - init() { - super.init(frame: .zero) - backgroundColor = .clear - layer.borderWidth = 1 - layer.borderColor = stokeColor.cgColor - layer.contentsGravity = .top - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError() - } - - override var frame: CGRect { - set { UIView.performWithoutAnimation { super.frame = newValue } } - get { super.frame } - } - - override func draw(_ rect: CGRect) { - super.draw(rect) - - guard let context = UIGraphicsGetCurrentContext() else { return } - - context.setStrokeColor(stokeColor.cgColor) - context.setLineWidth(1) - - for pathPair in lines { - let adjustedStart = CGPoint(x: pathPair.start.x, y: pathPair.start.y) - let adjustedEnd = CGPoint(x: pathPair.end.x, y: pathPair.end.y) - context.move(to: adjustedStart) - context.addLine(to: adjustedEnd) - } - context.strokePath() - } -} - -extension GridView { - struct CGPointPair: Equatable { - let start: CGPoint - let end: CGPoint - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/TextLabel.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/TextLabel.swift deleted file mode 100644 index cf252773ad..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/TextLabel.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// TextLabel.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/12. -// - -import UIKit - -class TextLabel: UITextView { - #if DEBUG - private var setupCompleted: Bool = false - #endif - - override required init(frame: CGRect, textContainer: NSTextContainer?) { - super.init(frame: frame, textContainer: textContainer) - commitInit() - } - - convenience init(frame: CGRect = .zero) { - if #available(iOS 16.0, macCatalyst 16.0, *) { - self.init(usingTextLayoutManager: false) - } else { - self.init(frame: frame, textContainer: nil) - _ = layoutManager.textContainers - } - commitInit() - } - - func commitInit() { - #if DEBUG - assert(!setupCompleted) - setupCompleted = true - #endif - showsVerticalScrollIndicator = false - showsHorizontalScrollIndicator = false - textColor = .label - textContainer.lineFragmentPadding = .zero - textAlignment = .natural - backgroundColor = .clear - textContainerInset = .zero - textContainer.lineBreakMode = .byTruncatingTail - clipsToBounds = false - isSelectable = true - isScrollEnabled = false - isEditable = false - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Theme.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Theme.swift deleted file mode 100644 index 1eddf7898b..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Sources/MarkdownView/Supplements/Theme.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// Theme.swift -// MarkdownView -// -// Created by 秋星桥 on 2025/1/3. -// - -import Foundation -import Splash -import UIKit - -public extension Theme { - static var `default`: Theme = .init() -} - -public struct Theme: Equatable { - public struct Fonts: Equatable { - public var body = UIFont.preferredFont(forTextStyle: .body) - public var codeInline = UIFont.monospacedSystemFont( - ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize, - weight: .regular - ) - public var bold = UIFont.preferredFont(forTextStyle: .body).bold - public var italic = UIFont.preferredFont(forTextStyle: .body).italic - public var code = UIFont.monospacedSystemFont( - ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize * 0.85, - weight: .regular - ) - public var largeTitle = UIFont.preferredFont(forTextStyle: .body).bold - public var title = UIFont.preferredFont(forTextStyle: .body).bold - } - - public var fonts: Fonts = .init() - - public struct Colors: Equatable { - public var body = UIColor.label - public var emphasis = UIColor.systemOrange - public var code = UIColor.label - public var codeBackground = UIColor.gray.withAlphaComponent(0.25) - } - - public var colors: Colors = .init() - - public struct Spacings: Equatable { - public var final: CGFloat = 16 - public var general: CGFloat = 8 - public var list: CGFloat = 12 - public var cell: CGFloat = 32 - } - - public var spacings: Spacings = .init() - - public struct Sizes: Equatable { - public var bullet: CGFloat = 4 - } - - public var sizes: Sizes = .init() - - public init() {} -} - -extension UIFont { - var baseLineHeight: CGFloat { - NSAttributedString(string: "88", attributes: [ - .font: self, - .originalFont: self, - ]).measureHeight(usingWidth: .greatestFiniteMagnitude) - } -} - -private let codeThemeTemplate: Splash.Theme = .init( - font: .init(size: Double(0)), - plainTextColor: .label, - tokenColors: [ - .keyword: Color( - light: Color(red: 0.948, green: 0.140, blue: 0.547, alpha: 1), - dark: Color(red: 0.948, green: 0.140, blue: 0.547, alpha: 1) - ), - .string: Color( - light: Color(red: 0.988, green: 0.273, blue: 0.317, alpha: 1), - dark: Color(red: 0.988, green: 0.273, blue: 0.317, alpha: 1) - ), - .type: Color( - light: Color(red: 0.384, green: 0.698, blue: 0.161, alpha: 1), - dark: Color(red: 0.584, green: 0.898, blue: 0.361, alpha: 1) - ), - .call: Color( - light: Color(red: 0.384, green: 0.698, blue: 0.161, alpha: 1), - dark: Color(red: 0.584, green: 0.898, blue: 0.361, alpha: 1) - ), - .number: Color( - light: Color(red: 0.387, green: 0.317, blue: 0.774, alpha: 1), - dark: Color(red: 0.587, green: 0.517, blue: 0.974, alpha: 1) - ), - .comment: Color( - light: Color(red: 0.424, green: 0.475, blue: 0.529, alpha: 1), - dark: Color(red: 0.424, green: 0.475, blue: 0.529, alpha: 1) - ), - .property: Color( - light: Color(red: 0.384, green: 0.698, blue: 0.161, alpha: 1), - dark: Color(red: 0.584, green: 0.898, blue: 0.361, alpha: 1) - ), - .dotAccess: Color( - light: Color(red: 0.384, green: 0.698, blue: 0.161, alpha: 1), - dark: Color(red: 0.584, green: 0.898, blue: 0.361, alpha: 1) - ), - .preprocessing: Color( - light: Color(red: 0.752, green: 0.326, blue: 0.12, alpha: 19), - dark: Color(red: 0.952, green: 0.526, blue: 0.22, alpha: 19) - ), - ], - backgroundColor: .clear -) - -public extension Theme { - func codeTheme(withFont font: UIFont) -> Splash.Theme { - var ret = codeThemeTemplate - ret.font = .init(size: Double(font.pointSize)) - return ret - } -} diff --git a/packages/frontend/apps/ios/App/Packages/MarkdownView/Tests/FlowMarkdownViewTests/FlowMarkdownViewTests.swift b/packages/frontend/apps/ios/App/Packages/MarkdownView/Tests/FlowMarkdownViewTests/FlowMarkdownViewTests.swift deleted file mode 100644 index 451d56e581..0000000000 --- a/packages/frontend/apps/ios/App/Packages/MarkdownView/Tests/FlowMarkdownViewTests/FlowMarkdownViewTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -@testable import FlowMarkdownView -import Testing - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -}