diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 5870e8c65..b12a2b139 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 = 77; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -13,7 +13,6 @@ 50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; }; 50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; }; 50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; }; - 50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; }; 50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */; }; 50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */; }; 9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; }; @@ -78,6 +77,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ C45499AB2D140B5000E21978 /* NBStore */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = NBStore; sourceTree = ""; }; @@ -88,7 +89,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */, 9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */, 50802D612D112F8700694021 /* Intelligents in Frameworks */, 2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */, @@ -521,7 +521,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 = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -559,7 +559,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 = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -613,10 +613,6 @@ isa = XCSwiftPackageProductDependency; productName = Intelligents; }; - 50A285DB2D112B24000D5A6D /* Intelligents */ = { - isa = XCSwiftPackageProductDependency; - productName = Intelligents; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 504EC2FC1FED79650016851F /* Project object */; 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 deleted file mode 100644 index 63f227d74..000000000 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,59 +0,0 @@ -{ - "pins" : [ - { - "identity" : "apollo-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apollographql/apollo-ios", - "state" : { - "revision" : "39fea7617346c0731be25f61afd537e7032fb562", - "version" : "1.22.0" - } - }, - { - "identity" : "chidorimenu", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Lakr233/ChidoriMenu", - "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" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-eventsource", - "kind" : "remoteSourceControl", - "location" : "https://github.com/LaunchDarkly/swift-eventsource.git", - "state" : { - "revision" : "57051701c58a93603ffa2051f8e9cf0c8cff7814", - "version" : "3.3.0" - } - } - ], - "version" : 2 -} 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 a0763ea43..0973b9f21 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 @@ -3,37 +3,28 @@ { "identity" : "apollo-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/apollographql/apollo-ios.git", + "location" : "https://github.com/apollographql/apollo-ios", "state" : { - "revision" : "39fea7617346c0731be25f61afd537e7032fb562", - "version" : "1.22.0" + "revision" : "9aa748d6f0526a744d49d59a2383dc7fdf9d645b", + "version" : "1.18.0" } }, { - "identity" : "chidorimenu", + "identity" : "snapkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/Lakr233/ChidoriMenu", + "location" : "https://github.com/SnapKit/SnapKit.git", "state" : { - "revision" : "3bb4323fe0f7f8f435d15656c3eeffcbb7c9c605", - "version" : "3.0.0" + "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", + "version" : "5.7.1" } }, { - "identity" : "splash", + "identity" : "sqlite.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/Splash", + "location" : "https://github.com/stephencelis/SQLite.swift.git", "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" : "392dd6058624d9f6c5b4c769d165ddd8c7293394", + "version" : "0.15.4" } }, { @@ -46,12 +37,30 @@ } }, { - "identity" : "swift-eventsource", + "identity" : "swift-toolchain-sqlite", "kind" : "remoteSourceControl", - "location" : "https://github.com/LaunchDarkly/swift-eventsource.git", + "location" : "https://github.com/swiftlang/swift-toolchain-sqlite", "state" : { - "revision" : "57051701c58a93603ffa2051f8e9cf0c8cff7814", - "version" : "3.3.0" + "revision" : "b626d3002773b1a1304166643e7f118f724b2132", + "version" : "1.0.4" + } + }, + { + "identity" : "swifterswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwifterSwift/SwifterSwift.git", + "state" : { + "revision" : "39fa28c90a3ebe3d53f80289304fd880cf2c42d0", + "version" : "6.2.0" + } + }, + { + "identity" : "then", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devxoul/Then", + "state" : { + "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", + "version" : "3.0.0" } } ], diff --git a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift index e34d2da57..eedfaaa68 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift @@ -5,123 +5,18 @@ // Created by 秋星桥 on 2025/1/8. // -import ChidoriMenu import Intelligents import UIKit -extension AFFiNEViewController: IntelligentsButtonDelegate, IntelligentsFocusApertureViewDelegate { +extension AFFiNEViewController: IntelligentsButtonDelegate { func onIntelligentsButtonTapped(_ button: IntelligentsButton) { - guard let webView else { - assertionFailure() // ? wdym ? - return - } - + IntelligentContext.shared.webView = webView! button.beginProgress() - - let group = DispatchGroup() - - group.enter() - webView.evaluateScript(.getCurrentServerBaseUrl) { result in - self.baseUrl = result as? String - print("[*] setting baseUrl: \(self.baseUrl ?? "")") - group.leave() - } - - group.enter() - webView.evaluateScript(.getCurrentDocId) { result in - self.documentID = result as? String - print("[*] setting documentID: \(self.documentID ?? "")") - group.leave() - } - - group.enter() - webView.evaluateScript(.getCurrentWorkspaceId) { result in - self.workspaceID = result as? String - print("[*] setting workspaceID: \(self.workspaceID ?? "")") - group.leave() - } - - group.enter() - webView.evaluateScript(.getCurrentDocContentInMarkdown) { input in - self.documentContent = input as? String - print("[*] setting documentContent: \(self.documentContent?.count ?? 0) chars") - group.leave() - } - - DispatchQueue.global().asyncAfter(deadline: .now()) { - group.wait() - DispatchQueue.main.async { - button.stopProgress() - webView.resignFirstResponder() - self.openIntelligentsSheet() - } - } - } - - @discardableResult - func openIntelligentsSheet() -> IntelligentsFocusApertureView? { - dismissIntelligentsButton() - view.resignFirstResponder() - // stop scroll on webview - if let contentOffset = webView?.scrollView.contentOffset { - webView?.scrollView.contentOffset = contentOffset - } - let focus = IntelligentsFocusApertureView() - focus.prepareAnimationWith( - capturingTargetContentView: webView ?? .init(), - coveringRootViewController: self - ) - focus.delegate = self - focus.executeAnimationKickIn() - dismissIntelligentsButton() - return focus - } - - func openSimpleChat() { - let targetController = IntelligentsChatController() - presentIntoCurrentContext(withTargetController: targetController) - } - - func focusApertureRequestAction( - from view: IntelligentsFocusApertureView, - actionType: IntelligentsFocusApertureViewActionType - ) { - switch actionType { - case .translateTo: - var actions: [UIAction] = [] - for lang in IntelligentsEphemeralActionController.EphemeralAction.Language.allCases { - actions.append(.init(title: lang.rawValue) { [weak self] _ in - guard let self else { return } - let controller = IntelligentsEphemeralActionController( - action: .translate(to: lang) - ) - controller.workspaceID = workspaceID ?? "" - controller.documentID = documentID ?? "" - controller.documentContent = documentContent ?? "" - controller.configure(previewImage: view.capturedImage ?? .init()) - presentIntoCurrentContext(withTargetController: controller) - }) - } - view.present(menu: .init(children: actions)) { controller in - controller.overrideUserInterfaceStyle = .dark - } controllerDidPresent: { _ in } - case .summary: - let controller = IntelligentsEphemeralActionController( - action: .summarize - ) - controller.configure(previewImage: view.capturedImage ?? .init()) - controller.workspaceID = workspaceID ?? "" - controller.documentID = documentID ?? "" - controller.documentContent = documentContent ?? "" - presentIntoCurrentContext(withTargetController: controller) - case .chatWithAI: - let controller = IntelligentsChatController() - controller.metadata[.documentID] = documentID - controller.metadata[.workspaceID] = workspaceID - controller.metadata[.content] = documentContent - presentIntoCurrentContext(withTargetController: controller) - case .dismiss: - presentIntelligentsButton() + + IntelligentContext.shared.preparePresent() { + button.stopProgress() + let controller = IntelligentsController() + self.present(controller, animated: true) } } } diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index b17db0815..22227845a 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -3,13 +3,6 @@ import Intelligents import UIKit class AFFiNEViewController: CAPBridgeViewController { - var baseUrl: String? { - didSet { Intelligents.setUpstreamEndpoint(baseUrl ?? "") } - } - var documentID: String? - var workspaceID: String? - var documentContent: String? - override func viewDidLoad() { super.viewDidLoad() webView?.allowsBackForwardNavigationGestures = true @@ -36,7 +29,7 @@ class AFFiNEViewController: CAPBridgeViewController { CookiePlugin(), HashcashPlugin(), NavigationGesturePlugin(), - IntelligentsPlugin(representController: self), + // IntelligentsPlugin(representController: self), // no longer put in use NbStorePlugin(), ] plugins.forEach { bridge?.registerPluginInstance($0) } @@ -45,16 +38,11 @@ class AFFiNEViewController: CAPBridgeViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) navigationController?.setNavigationBarHidden(false, animated: animated) - } - -#if DEBUG - override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - super.motionEnded(motion, with: event) - if motion == .motionShake { - presentIntelligentsButton() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.presentIntelligentsButton() } } -#endif } + diff --git a/packages/frontend/apps/ios/App/App/Info.plist b/packages/frontend/apps/ios/App/App/Info.plist index ff1e14a21..7bc4ac16e 100644 --- a/packages/frontend/apps/ios/App/App/Info.plist +++ b/packages/frontend/apps/ios/App/App/Info.plist @@ -42,7 +42,7 @@ NSPhotoLibraryUsageDescription AFFiNE requires access to select photos from your photo library and insert them into your documents NSUserTrackingUsageDescription - Rest assured, enabling this permission won't access your private info on other sites. It's only used to identify your device and improve security and product experience. + Rest assured, enabling this permission won't access your private info on other sites. It's only used to identify your device and improve security and product experience. UILaunchScreen UIImageName diff --git a/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift index f348b0629..cff51cf72 100644 --- a/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift +++ b/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift @@ -1,36 +1,36 @@ import Capacitor import Foundation -@objc(IntelligentsPlugin) -public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin { - public let identifier = "IntelligentsPlugin" - public let jsName = "Intelligents" - public let pluginMethods: [CAPPluginMethod] = [ - CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise), - ] - public private(set) weak var representController: UIViewController? - - init(representController: UIViewController) { - self.representController = representController - super.init() - } - - deinit { - representController = nil - } - - @objc public func presentIntelligentsButton(_ call: CAPPluginCall) { - DispatchQueue.main.async { - self.representController?.presentIntelligentsButton() - call.resolve() - } - } - - @objc public func dismissIntelligentsButton(_ call: CAPPluginCall) { - DispatchQueue.main.async { - self.representController?.dismissIntelligentsButton() - call.resolve() - } - } -} +//@objc(IntelligentsPlugin) +//public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin { +// public let identifier = "IntelligentsPlugin" +// public let jsName = "Intelligents" +// public let pluginMethods: [CAPPluginMethod] = [ +// CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise), +// CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise), +// ] +// public private(set) weak var representController: UIViewController? +// +// init(representController: UIViewController) { +// self.representController = representController +// super.init() +// } +// +// deinit { +// representController = nil +// } +// +// @objc public func presentIntelligentsButton(_ call: CAPPluginCall) { +// DispatchQueue.main.async { +// self.representController?.presentIntelligentsButton() +// call.resolve() +// } +// } +// +// @objc public func dismissIntelligentsButton(_ call: CAPPluginCall) { +// DispatchQueue.main.async { +// self.representController?.dismissIntelligentsButton() +// call.resolve() +// } +// } +//} diff --git a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift index 45d68f4dc..9e015b0b5 100644 --- a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift +++ b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift @@ -1711,6 +1711,7 @@ extension UniffiError: Equatable, Hashable {} + extension UniffiError: Foundation.LocalizedError { public var errorDescription: String? { String(reflecting: self) @@ -1718,6 +1719,8 @@ extension UniffiError: Foundation.LocalizedError { } + + #if swift(>=5.8) @_documentation(visibility: private) #endif diff --git a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.modulemap b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.modulemap index a7925044f..45fbfa8fb 100644 --- a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.modulemap +++ b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.modulemap @@ -1,4 +1,7 @@ module affine_mobile_nativeFFI { header "affine_mobile_nativeFFI.h" export * + use "Darwin" + use "_Builtin_stdbool" + use "_Builtin_stdint" } \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.resolved b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.resolved deleted file mode 100644 index 20f317d36..000000000 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "apollo-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apollographql/apollo-ios.git", - "state" : { - "revision" : "6ecc75281dab2fa231cb0d5fed3a3713826fecae", - "version" : "1.21.0" - } - } - ], - "version" : 2 -} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.swift index 14a8270da..c87014c63 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library(name: "AffineGraphQL", targets: ["AffineGraphQL"]), ], dependencies: [ - .package(url: "https://github.com/apollographql/apollo-ios", exact: "1.22.0"), + .package(url: "https://github.com/apollographql/apollo-ios", exact: "1.18.0"), ], targets: [ .target( diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoriesQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoriesQuery.graphql.swift index 4624ed7aa..cda9e34e9 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoriesQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoriesQuery.graphql.swift @@ -7,7 +7,7 @@ public class GetCopilotHistoriesQuery: GraphQLQuery { public static let operationName: String = "getCopilotHistories" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query getCopilotHistories($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename histories(docId: $docId, options: $options) { __typename sessionId tokens action createdAt messages { __typename id role content attachments createdAt } } } } }"# + #"query getCopilotHistories($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename histories(docId: $docId, options: $options) { __typename sessionId pinned tokens action createdAt messages { __typename id role content streamObjects { __typename type textDelta toolCallId toolName args result } attachments createdAt } } } } }"# )) public var workspaceId: String @@ -86,6 +86,7 @@ public class GetCopilotHistoriesQuery: GraphQLQuery { public static var __selections: [ApolloAPI.Selection] { [ .field("__typename", String.self), .field("sessionId", String.self), + .field("pinned", Bool.self), .field("tokens", Int.self), .field("action", String?.self), .field("createdAt", AffineGraphQL.DateTime.self), @@ -93,6 +94,7 @@ public class GetCopilotHistoriesQuery: GraphQLQuery { ] } public var sessionId: String { __data["sessionId"] } + public var pinned: Bool { __data["pinned"] } /// The number of tokens used in the session public var tokens: Int { __data["tokens"] } /// An mark identifying which view to use to display the session @@ -113,6 +115,7 @@ public class GetCopilotHistoriesQuery: GraphQLQuery { .field("id", AffineGraphQL.ID?.self), .field("role", String.self), .field("content", String.self), + .field("streamObjects", [StreamObject]?.self), .field("attachments", [String]?.self), .field("createdAt", AffineGraphQL.DateTime.self), ] } @@ -120,8 +123,35 @@ public class GetCopilotHistoriesQuery: GraphQLQuery { public var id: AffineGraphQL.ID? { __data["id"] } public var role: String { __data["role"] } public var content: String { __data["content"] } + public var streamObjects: [StreamObject]? { __data["streamObjects"] } public var attachments: [String]? { __data["attachments"] } public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] } + + /// CurrentUser.Copilot.History.Message.StreamObject + /// + /// Parent Type: `StreamObject` + public struct StreamObject: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.StreamObject } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("type", String.self), + .field("textDelta", String?.self), + .field("toolCallId", String?.self), + .field("toolName", String?.self), + .field("args", AffineGraphQL.JSON?.self), + .field("result", AffineGraphQL.JSON?.self), + ] } + + public var type: String { __data["type"] } + public var textDelta: String? { __data["textDelta"] } + public var toolCallId: String? { __data["toolCallId"] } + public var toolName: String? { __data["toolName"] } + public var args: AffineGraphQL.JSON? { __data["args"] } + public var result: AffineGraphQL.JSON? { __data["result"] } + } } } } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoryIdsQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoryIdsQuery.graphql.swift index 9f3ad7e87..adf2aaf2f 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoryIdsQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotHistoryIdsQuery.graphql.swift @@ -7,7 +7,7 @@ public class GetCopilotHistoryIdsQuery: GraphQLQuery { public static let operationName: String = "getCopilotHistoryIds" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query getCopilotHistoryIds($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename histories(docId: $docId, options: $options) { __typename sessionId messages { __typename id role createdAt } } } } }"# + #"query getCopilotHistoryIds($workspaceId: String!, $docId: String, $options: QueryChatHistoriesInput) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename histories(docId: $docId, options: $options) { __typename sessionId pinned messages { __typename id role createdAt } } } } }"# )) public var workspaceId: String @@ -86,10 +86,12 @@ public class GetCopilotHistoryIdsQuery: GraphQLQuery { public static var __selections: [ApolloAPI.Selection] { [ .field("__typename", String.self), .field("sessionId", String.self), + .field("pinned", Bool.self), .field("messages", [Message].self), ] } public var sessionId: String { __data["sessionId"] } + public var pinned: Bool { __data["pinned"] } public var messages: [Message] { __data["messages"] } /// CurrentUser.Copilot.History.Message diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionQuery.graphql.swift index 97ff8d721..5affd6246 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionQuery.graphql.swift @@ -7,7 +7,7 @@ public class GetCopilotSessionQuery: GraphQLQuery { public static let operationName: String = "getCopilotSession" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query getCopilotSession($workspaceId: String!, $sessionId: String!) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename session(sessionId: $sessionId) { __typename id parentSessionId promptName model optionalModels } } } }"# + #"query getCopilotSession($workspaceId: String!, $sessionId: String!) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename session(sessionId: $sessionId) { __typename id parentSessionId docId pinned promptName model optionalModels } } } }"# )) public var workspaceId: String @@ -81,6 +81,8 @@ public class GetCopilotSessionQuery: GraphQLQuery { .field("__typename", String.self), .field("id", AffineGraphQL.ID.self), .field("parentSessionId", AffineGraphQL.ID?.self), + .field("docId", String?.self), + .field("pinned", Bool.self), .field("promptName", String.self), .field("model", String.self), .field("optionalModels", [String].self), @@ -88,6 +90,8 @@ public class GetCopilotSessionQuery: GraphQLQuery { public var id: AffineGraphQL.ID { __data["id"] } public var parentSessionId: AffineGraphQL.ID? { __data["parentSessionId"] } + public var docId: String? { __data["docId"] } + public var pinned: Bool { __data["pinned"] } public var promptName: String { __data["promptName"] } public var model: String { __data["model"] } public var optionalModels: [String] { __data["optionalModels"] } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionsQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionsQuery.graphql.swift index 95d53c26e..54f3f6a1b 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionsQuery.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetCopilotSessionsQuery.graphql.swift @@ -7,7 +7,7 @@ public class GetCopilotSessionsQuery: GraphQLQuery { public static let operationName: String = "getCopilotSessions" public static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( - #"query getCopilotSessions($workspaceId: String!, $docId: String, $options: QueryChatSessionsInput) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename sessions(docId: $docId, options: $options) { __typename id parentSessionId promptName model optionalModels } } } }"# + #"query getCopilotSessions($workspaceId: String!, $docId: String, $options: QueryChatSessionsInput) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename sessions(docId: $docId, options: $options) { __typename id parentSessionId docId pinned promptName model optionalModels } } } }"# )) public var workspaceId: String @@ -88,6 +88,8 @@ public class GetCopilotSessionsQuery: GraphQLQuery { .field("__typename", String.self), .field("id", AffineGraphQL.ID.self), .field("parentSessionId", AffineGraphQL.ID?.self), + .field("docId", String?.self), + .field("pinned", Bool.self), .field("promptName", String.self), .field("model", String.self), .field("optionalModels", [String].self), @@ -95,6 +97,8 @@ public class GetCopilotSessionsQuery: GraphQLQuery { public var id: AffineGraphQL.ID { __data["id"] } public var parentSessionId: AffineGraphQL.ID? { __data["parentSessionId"] } + public var docId: String? { __data["docId"] } + public var pinned: Bool { __data["pinned"] } public var promptName: String { __data["promptName"] } public var model: String { __data["model"] } public var optionalModels: [String] { __data["optionalModels"] } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetRecentlyUpdatedDocsQuery.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetRecentlyUpdatedDocsQuery.graphql.swift new file mode 100644 index 000000000..8c09ace04 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Operations/Queries/GetRecentlyUpdatedDocsQuery.graphql.swift @@ -0,0 +1,138 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class GetRecentlyUpdatedDocsQuery: GraphQLQuery { + public static let operationName: String = "getRecentlyUpdatedDocs" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query getRecentlyUpdatedDocs($workspaceId: String!, $pagination: PaginationInput!) { workspace(id: $workspaceId) { __typename recentlyUpdatedDocs(pagination: $pagination) { __typename totalCount pageInfo { __typename endCursor hasNextPage } edges { __typename node { __typename id title createdAt updatedAt creatorId lastUpdaterId } } } } }"# + )) + + public var workspaceId: String + public var pagination: PaginationInput + + public init( + workspaceId: String, + pagination: PaginationInput + ) { + self.workspaceId = workspaceId + self.pagination = pagination + } + + public var __variables: Variables? { [ + "workspaceId": workspaceId, + "pagination": pagination + ] } + + public struct Data: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query } + public static var __selections: [ApolloAPI.Selection] { [ + .field("workspace", Workspace.self, arguments: ["id": .variable("workspaceId")]), + ] } + + /// Get workspace by id + public var workspace: Workspace { __data["workspace"] } + + /// Workspace + /// + /// Parent Type: `WorkspaceType` + public struct Workspace: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("recentlyUpdatedDocs", RecentlyUpdatedDocs.self, arguments: ["pagination": .variable("pagination")]), + ] } + + /// Get recently updated docs of a workspace + public var recentlyUpdatedDocs: RecentlyUpdatedDocs { __data["recentlyUpdatedDocs"] } + + /// Workspace.RecentlyUpdatedDocs + /// + /// Parent Type: `PaginatedDocType` + public struct RecentlyUpdatedDocs: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PaginatedDocType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("totalCount", Int.self), + .field("pageInfo", PageInfo.self), + .field("edges", [Edge].self), + ] } + + public var totalCount: Int { __data["totalCount"] } + public var pageInfo: PageInfo { __data["pageInfo"] } + public var edges: [Edge] { __data["edges"] } + + /// Workspace.RecentlyUpdatedDocs.PageInfo + /// + /// Parent Type: `PageInfo` + public struct PageInfo: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PageInfo } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("endCursor", String?.self), + .field("hasNextPage", Bool.self), + ] } + + public var endCursor: String? { __data["endCursor"] } + public var hasNextPage: Bool { __data["hasNextPage"] } + } + + /// Workspace.RecentlyUpdatedDocs.Edge + /// + /// Parent Type: `DocTypeEdge` + public struct Edge: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.DocTypeEdge } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("node", Node.self), + ] } + + public var node: Node { __data["node"] } + + /// Workspace.RecentlyUpdatedDocs.Edge.Node + /// + /// Parent Type: `DocType` + public struct Node: AffineGraphQL.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.DocType } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", String.self), + .field("title", String?.self), + .field("createdAt", AffineGraphQL.DateTime?.self), + .field("updatedAt", AffineGraphQL.DateTime?.self), + .field("creatorId", String?.self), + .field("lastUpdaterId", String?.self), + ] } + + public var id: String { __data["id"] } + public var title: String? { __data["title"] } + public var createdAt: AffineGraphQL.DateTime? { __data["createdAt"] } + public var updatedAt: AffineGraphQL.DateTime? { __data["updatedAt"] } + public var creatorId: String? { __data["creatorId"] } + public var lastUpdaterId: String? { __data["lastUpdaterId"] } + } + } + } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatSessionInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatSessionInput.graphql.swift index f92474a65..c8c8fc915 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatSessionInput.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/CreateChatSessionInput.graphql.swift @@ -11,22 +11,29 @@ public struct CreateChatSessionInput: InputObject { } public init( - docId: String, + docId: GraphQLNullable = nil, + pinned: GraphQLNullable = nil, promptName: String, workspaceId: String ) { __data = InputDict([ "docId": docId, + "pinned": pinned, "promptName": promptName, "workspaceId": workspaceId ]) } - public var docId: String { + public var docId: GraphQLNullable { get { __data["docId"] } set { __data["docId"] = newValue } } + public var pinned: GraphQLNullable { + get { __data["pinned"] } + set { __data["pinned"] = newValue } + } + /// The prompt name to use for the session public var promptName: String { get { __data["promptName"] } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpdateChatSessionInput.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpdateChatSessionInput.graphql.swift index afe008716..bb2bf1f04 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpdateChatSessionInput.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/InputObjects/UpdateChatSessionInput.graphql.swift @@ -11,17 +11,33 @@ public struct UpdateChatSessionInput: InputObject { } public init( - promptName: String, + docId: GraphQLNullable = nil, + pinned: GraphQLNullable = nil, + promptName: GraphQLNullable = nil, sessionId: String ) { __data = InputDict([ + "docId": docId, + "pinned": pinned, "promptName": promptName, "sessionId": sessionId ]) } + /// The workspace id of the session + public var docId: GraphQLNullable { + get { __data["docId"] } + set { __data["docId"] = newValue } + } + + /// Whether to pin the session + public var pinned: GraphQLNullable { + get { __data["pinned"] } + set { __data["pinned"] = newValue } + } + /// The prompt name to use for the session - public var promptName: String { + public var promptName: GraphQLNullable { get { __data["promptName"] } set { __data["promptName"] = newValue } } diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/StreamObject.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/StreamObject.graphql.swift new file mode 100644 index 000000000..7857b6d3a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/Objects/StreamObject.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let StreamObject = ApolloAPI.Object( + typename: "StreamObject", + implementedInterfaces: [], + keyFields: nil + ) +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift index 2d0a2df16..414967d89 100644 --- a/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift +++ b/packages/frontend/apps/ios/App/Packages/AffineGraphQL/Sources/Schema/SchemaMetadata.graphql.swift @@ -81,6 +81,7 @@ public enum SchemaMetadata: ApolloAPI.SchemaMetadata { case "SearchResultObjectType": return AffineGraphQL.Objects.SearchResultObjectType case "SearchResultPagination": return AffineGraphQL.Objects.SearchResultPagination case "ServerConfigType": return AffineGraphQL.Objects.ServerConfigType + case "StreamObject": return AffineGraphQL.Objects.StreamObject case "SubscriptionPrice": return AffineGraphQL.Objects.SubscriptionPrice case "SubscriptionType": return AffineGraphQL.Objects.SubscriptionType case "TranscriptionItemType": return AffineGraphQL.Objects.TranscriptionItemType diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.resolved b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.resolved deleted file mode 100644 index a0763ea43..000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.resolved +++ /dev/null @@ -1,59 +0,0 @@ -{ - "pins" : [ - { - "identity" : "apollo-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apollographql/apollo-ios.git", - "state" : { - "revision" : "39fea7617346c0731be25f61afd537e7032fb562", - "version" : "1.22.0" - } - }, - { - "identity" : "chidorimenu", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Lakr233/ChidoriMenu", - "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" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-eventsource", - "kind" : "remoteSourceControl", - "location" : "https://github.com/LaunchDarkly/swift-eventsource.git", - "state" : { - "revision" : "57051701c58a93603ffa2051f8e9cf0c8cff7814", - "version" : "3.3.0" - } - } - ], - "version" : 2 -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index 1f7fef2c5..e8104d16e 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -7,29 +7,30 @@ let package = Package( name: "Intelligents", defaultLocalization: "en", platforms: [ - .iOS(.v15), - .macCatalyst(.v15), + .iOS(.v16), ], 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/LaunchDarkly/swift-eventsource.git", from: "3.3.0"), + .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.18.0"), .package(url: "https://github.com/apple/swift-collections", from: "1.2.0"), - .package(url: "https://github.com/Lakr233/ChidoriMenu", from: "3.0.0"), + .package(url: "https://github.com/devxoul/Then", from: "3.0.0"), + .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.0.1"), + .package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"), ], targets: [ .target(name: "Intelligents", dependencies: [ "AffineGraphQL", - "ChidoriMenu", - "MarkdownView", - "ChidoriMenu", + "SnapKit", + "Then", + "SwifterSwift", .product(name: "Apollo", package: "apollo-ios"), - .product(name: "LDSwiftEventSource", package: "swift-eventsource"), .product(name: "OrderedCollections", package: "swift-collections"), + ], resources: [ + .process("Resources/main.metal"), + .process("Interface/View/InputBox/InputBox.xcassets"), ]), ] ) 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 e5f639f6f..000000000 --- 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 bb7c80943..000000000 --- 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 85fba639b..000000000 --- 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 6bbac1b90..000000000 --- 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 f7ee027bb..000000000 --- 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 7feddb601..000000000 --- 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 932f5f3eb..000000000 --- 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 b94160a5c..000000000 --- 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 1da7e3b99..000000000 --- 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 eb58f6849..000000000 --- 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 1959304e7..000000000 --- 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/Intelligents.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift index b16a00f45..f07f81b7e 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift @@ -5,19 +5,7 @@ import AffineGraphQL import Apollo import Foundation -public enum Intelligents { - public private(set) static var qlClient: ApolloClient = createQlClient() - - public static func setUpstreamEndpoint(_ upstream: String) { - guard let url = URL(string: upstream) else { - assertionFailure() - return - } - print("[*] setting up upstream endpoint to \(url.absoluteString)") - Constant.affineUpstreamURL = url - qlClient = createQlClient() - } -} +public enum Intelligents {} private extension Intelligents { private final class URLSessionCookieClient: URLSessionClient { @@ -29,18 +17,4 @@ private extension Intelligents { } } } - - static func createQlClient() -> ApolloClient { - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = DefaultInterceptorProvider( - client: URLSessionCookieClient(), - shouldInvalidateClientOnDeinit: true, - store: store - ) - let transport = RequestChainNetworkTransport( - interceptorProvider: provider, - endpointURL: Constant.affineUpstreamURL.appendingPathComponent("graphql") - ) - return .init(networkTransport: transport, store: store) - } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift new file mode 100644 index 000000000..f2929ea94 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/BlurTransition.swift @@ -0,0 +1,158 @@ +// +// BlurTransition.swift +// BlurTransition +// +// Created by 秋星桥 on 6/16/23. +// + +import UIKit + +extension UIViewController { + func presentWithFullScreenBlurTransition(_ viewController: UIViewController) { + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = BlurTransitioningDelegate.shared + present(viewController, animated: true) + } +} + +class BlurTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { + static let shared = BlurTransitioningDelegate() + + func animationController( + forPresented _: UIViewController, + presenting _: UIViewController, + source _: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + BlurTransitionAnimator(presenting: true) + } + + func animationController( + forDismissed _: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + BlurTransitionAnimator(presenting: false) + } +} + +class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { + private let presenting: Bool + + private let snapshotViewTag = "snapshotView".hashValue + private let blurViewTag = "blurView".hashValue + + init(presenting: Bool) { + self.presenting = presenting + super.init() + } + + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + 0.5 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + if presenting { + animatePresentation(using: transitionContext) + } else { + animateDismissal(using: transitionContext) + } + } + + private func animatePresentation(using transitionContext: UIViewControllerContextTransitioning) { + guard let toViewController = transitionContext.viewController(forKey: .to), + let fromViewController = transitionContext.viewController(forKey: .from) + else { + transitionContext.completeTransition(false) + assertionFailure() + return + } + + let toView = toViewController.view! + let fromView = fromViewController.view! + + let containerView = transitionContext.containerView + + guard let fromViewSnapshot = fromView.snapshotView(afterScreenUpdates: false) else { + transitionContext.completeTransition(false) + assertionFailure() + return + } + fromViewSnapshot.frame = fromView.frame + fromViewSnapshot.tag = snapshotViewTag + containerView.addSubview(fromViewSnapshot) + fromView.isHidden = true + + let blurEffectView = UIVisualEffectView() + blurEffectView.frame = containerView.bounds + blurEffectView.tag = blurViewTag + containerView.addSubview(blurEffectView) + + toView.frame = containerView.bounds + toView.alpha = 0 + toView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + containerView.addSubview(toView) + + toView.layoutIfNeeded() + + performWithAnimation(animations: { + blurEffectView.effect = UIBlurEffect(style: .systemMaterial) + fromViewSnapshot.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + toView.alpha = 1 + toView.transform = .identity + fromView.layoutIfNeeded() + toView.layoutIfNeeded() + }) { _ in + let success = !transitionContext.transitionWasCancelled + if !success { + assertionFailure() + fromView.isHidden = false + fromViewSnapshot.removeFromSuperview() + blurEffectView.removeFromSuperview() + toView.removeFromSuperview() + } + transitionContext.completeTransition(success) + } + } + + private func animateDismissal(using transitionContext: UIViewControllerContextTransitioning) { + guard let fromViewController = transitionContext.viewController(forKey: .from), + let toViewController = transitionContext.viewController(forKey: .to) + else { + transitionContext.completeTransition(false) + assertionFailure() + return + } + + let fromView = fromViewController.view! + let toView = toViewController.view! + let containerView = transitionContext.containerView + + guard let fromViewSnapshot = containerView.viewWithTag(snapshotViewTag), + let blurEffectView = containerView.viewWithTag(blurViewTag) as? UIVisualEffectView + else { + toView.isHidden = false + assertionFailure() + transitionContext.completeTransition(true) + return + } + + performWithAnimation(animations: { + fromViewSnapshot.transform = .identity + blurEffectView.effect = nil + fromView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + fromView.alpha = 0 + }) { _ in + let success = !transitionContext.transitionWasCancelled + if success { + toView.isHidden = false + fromViewSnapshot.removeFromSuperview() + blurEffectView.removeFromSuperview() + fromView.layoutIfNeeded() + toView.layoutIfNeeded() + } else { + assertionFailure() + fromView.transform = .identity + fromView.alpha = 1 + } + transitionContext.completeTransition(success) + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/IntelligentsController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/IntelligentsController.swift new file mode 100644 index 000000000..bca2bf4c1 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/IntelligentsController/IntelligentsController.swift @@ -0,0 +1,27 @@ +// +// IntelligentsController.swift +// Intelligents +// +// Created by 秋星桥 on 6/17/25. +// + +import UIKit + +public class IntelligentsController: UINavigationController { + public init() { + super.init(rootViewController: MainViewController()) + modalPresentationStyle = .custom + transitioningDelegate = BlurTransitioningDelegate.shared + setNavigationBarHidden(true, animated: false) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + override public func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Header.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Header.swift new file mode 100644 index 000000000..36fb59af9 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Header.swift @@ -0,0 +1,22 @@ +// +// MainViewController+Header.swift +// Intelligents +// +// Created by 秋星桥 on 6/19/25. +// + +import UIKit + +extension MainViewController: MainHeaderViewDelegate { + func mainHeaderViewDidTapClose() { + dismiss(animated: true) + } + + func mainHeaderViewDidTapDropdown() { + print(#function) + } + + func mainHeaderViewDidTapMenu() { + print(#function) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift new file mode 100644 index 000000000..c1304562d --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift @@ -0,0 +1,123 @@ +// +// MainViewController+Input.swift +// Intelligents +// +// Created by 秋星桥 on 6/19/25. +// + +import PhotosUI +import UIKit +import UniformTypeIdentifiers + +extension MainViewController: InputBoxDelegate { + func inputBoxDidSelectTakePhoto(_: InputBox) { + let imagePickerController = UIImagePickerController() + imagePickerController.delegate = self + imagePickerController.sourceType = .camera + imagePickerController.allowsEditing = false + present(imagePickerController, animated: true) + } + + func inputBoxDidSelectPhotoLibrary(_: InputBox) { + var configuration = PHPickerConfiguration() + configuration.filter = .images + configuration.selectionLimit = 0 // 0 means no limit + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + present(picker, animated: true) + } + + func inputBoxDidSelectAttachFiles(_: InputBox) { + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [ + .pdf, .plainText, .commaSeparatedText, .data, + ]) + documentPicker.delegate = self + documentPicker.allowsMultipleSelection = false + present(documentPicker, animated: true) + } + + func inputBoxDidSelectEmbedDocs(_ inputBox: InputBox) { + print(#function, inputBox) + } + + func inputBoxDidSelectAttachment(_ inputBox: InputBox) { + print(#function, inputBox) + } + + func inputBoxDidSend(_ inputBox: InputBox) { + print(#function, inputBox, inputBox.viewModel) + } + + func inputBoxTextDidChange(_ text: String) { + print(#function, text) + } +} + +// MARK: - UIImagePickerControllerDelegate + +extension MainViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + defer { picker.dismiss(animated: true) } + + guard let image = info[.originalImage] as? UIImage else { return } + inputBox.addImageAttachment(image) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } +} + +// MARK: - PHPickerViewControllerDelegate + +extension MainViewController: PHPickerViewControllerDelegate { + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + defer { picker.dismiss(animated: true) } + + for result in results { + if result.itemProvider.canLoadObject(ofClass: UIImage.self) { + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] object, error in + guard let image = object as? UIImage, error == nil else { return } + + DispatchQueue.main.async { + self?.inputBox.addImageAttachment(image) + } + } + } + } + } +} + +// MARK: - UIDocumentPickerDelegate + +extension MainViewController: UIDocumentPickerDelegate { + func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + for url in urls { + // Start accessing security-scoped resource + guard url.startAccessingSecurityScopedResource() else { continue } + defer { url.stopAccessingSecurityScopedResource() } + + // Copy file to temporary directory + let context = IntelligentContext.shared + context.prepareTemporaryDirectory() + + let tempURL = context.temporaryDirectory.appendingPathComponent(url.lastPathComponent) + + do { + // Remove existing file if it exists + if FileManager.default.fileExists(atPath: tempURL.path) { + try FileManager.default.removeItem(at: tempURL) + } + + // Copy file to temporary directory + try FileManager.default.copyItem(at: url, to: tempURL) + + // Add file attachment using the temporary URL + inputBox.addFileAttachment(tempURL) + } catch { + print("Failed to copy file: \(error)") + } + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift new file mode 100644 index 000000000..87afcd687 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift @@ -0,0 +1,59 @@ +import Combine +import SnapKit +import Then +import UIKit + +class MainViewController: UIViewController { + // MARK: - UI Components + + lazy var headerView = MainHeaderView().then { + $0.delegate = self + } + + lazy var inputBox = InputBox().then { + $0.delegate = self + } + + // MARK: - Properties + + private var cancellables = Set() + private let intelligentContext = IntelligentContext.shared + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .affineLayerBackgroundPrimary + + let inputBox = InputBox().then { + $0.delegate = self + } + self.inputBox = inputBox + + view.addSubview(headerView) + view.addSubview(inputBox) + + headerView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.leading.trailing.equalToSuperview() + } + + inputBox.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(view.keyboardLayoutGuide.snp.top) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController!.setNavigationBarHidden(true, animated: animated) + DispatchQueue.main.async { + self.inputBox.textView.becomeFirstResponder() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController!.setNavigationBarHidden(false, animated: animated) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Removal.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Removal.swift new file mode 100644 index 000000000..51e1e621c --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Removal.swift @@ -0,0 +1,45 @@ +// +// ParticleView+Removal.swift +// UIEffectKit +// +// Created by 秋星桥 on 6/13/25. +// + +import UIKit + +public extension UIView { + func removeFromSuperviewWithExplodeEffect() { + guard let superview else { return } + guard let window else { + removeFromSuperview() + return + } + guard MTLCreateSystemDefaultDevice() != nil else { + removeFromSuperview() + return + } + + let image = createViewSnapshot() + guard let cgImage = image.cgImage else { + removeFromSuperview() + return + } + + let frameInWindow = superview.convert(frame, to: window) + let particleView = ParticleView(frame: frameInWindow) + + window.addSubview(particleView) + particleView.layer.zPosition = 1000 + particleView.frame = frameInWindow + particleView.setNeedsLayout() + particleView.layoutIfNeeded() + + particleView.beginWith(cgImage, targetFrame: frameInWindow, onComplete: { + particleView.removeFromSuperview() + }, onFirstFrameRendered: { [weak self] in + DispatchQueue.main.async { + self?.removeFromSuperview() + } + }) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Renderer.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Renderer.swift new file mode 100644 index 000000000..4e7e1bf67 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView+Renderer.swift @@ -0,0 +1,331 @@ +// +// ParticleView+Renderer.swift +// UIEffectKit +// +// Created by 秋星桥 on 6/13/25. +// + +import MetalKit + +extension ParticleView { + class Renderer: NSObject, MTKViewDelegate { + private struct Particle { + var position: simd_float2 + var velocity: simd_float2 + var life: simd_float1 + var duration: simd_float1 + } + + private struct Vertex { + var position: simd_float4 + var uv: simd_float2 + var opacity: simd_float1 + } + + private var isPrepared = false + private var renderPipeline: MTLRenderPipelineState! + private var computePipeline: MTLComputePipelineState! + private var vertexBuffer: MTLBuffer! + private var particleBuffer: MTLBuffer! + private var particleCount: Int = 0 + private var texture: MTLTexture! + private var targetFrameSize: simd_float2 = .zero + private var stepSize: Float = 0 + private var commandQueue: MTLCommandQueue! + private var maxLife: Float = 0 + private var onComplete: (() -> Void)? + private var onFirstFrameRendered: (() -> Void)? + private var hasRenderedFirstFrame = false + private var device: MTLDevice! + + func prepareResources( + with device: MTLDevice, + image: CGImage, + targetFrame: CGRect, + onComplete: @escaping () -> Void, + onFirstFrameRendered: @escaping () -> Void + ) { + guard !isPrepared else { return } + + self.device = device + self.onComplete = onComplete + self.onFirstFrameRendered = onFirstFrameRendered + let integralTargetFrame = targetFrame.integral + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self else { return } + + setupPipelineStates(with: device) + setupVertexBuffer(with: device) + setupParticleSystem(targetFrame: integralTargetFrame, device: device) + setupTexture(from: image, device: device) + finalizeSetup(targetFrame: integralTargetFrame, device: device) + + DispatchQueue.main.async { self.isPrepared = true } + } + } + + private func setupPipelineStates(with device: MTLDevice) { + let library = try! device.makeDefaultLibrary(bundle: .module) + + let particleVertexFunction = library.makeFunction(name: "PTS_ParticleVertex")! + let particleFragmentFunction = library.makeFunction(name: "PTS_ParticleFragment")! + let updateParticlesFunction = library.makeFunction(name: "PTS_UpdateParticles")! + + let renderPipelineDescriptor = createRenderPipelineDescriptor( + vertexFunction: particleVertexFunction, + fragmentFunction: particleFragmentFunction + ) + + do { + renderPipeline = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) + computePipeline = try device.makeComputePipelineState(function: updateParticlesFunction) + } catch { + fatalError("failed to create pipeline states: \(error)") + } + } + + private func createRenderPipelineDescriptor( + vertexFunction: MTLFunction, + fragmentFunction: MTLFunction + ) -> MTLRenderPipelineDescriptor { + let descriptor = MTLRenderPipelineDescriptor() + descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + descriptor.colorAttachments[0].isBlendingEnabled = true + descriptor.colorAttachments[0].rgbBlendOperation = .add + descriptor.colorAttachments[0].alphaBlendOperation = .add + descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha + descriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha + descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha + descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha + descriptor.vertexFunction = vertexFunction + descriptor.fragmentFunction = fragmentFunction + return descriptor + } + + func mtkView(_: MTKView, drawableSizeWillChange _: CGSize) { + // No-op since view is not subject to resize + } + + func draw(in view: MTKView) { + guard isPrepared else { return } + + updateParticles() + + if checkAllParticlesDead() { + DispatchQueue.main.async { [weak self] in + self?.onComplete?() + } + return + } + + renderParticles(in: view) + + if !hasRenderedFirstFrame { + hasRenderedFirstFrame = true + DispatchQueue.main.async { [weak self] in + self?.onFirstFrameRendered?() + } + } + } + + private func updateParticles() { + let maxThreadsPerThreadgroup = computePipeline.maxTotalThreadsPerThreadgroup + let threadgroupSize = min(maxThreadsPerThreadgroup, 2048) + let threadgroupCount = (particleCount + threadgroupSize - 1) / threadgroupSize + + let computeCommandBuffer = commandQueue.makeCommandBuffer()! + + let computeCommandEncoder = computeCommandBuffer.makeComputeCommandEncoder()! + computeCommandEncoder.setComputePipelineState(computePipeline) + computeCommandEncoder.setBuffer(particleBuffer, offset: 0, index: 0) + computeCommandEncoder.dispatchThreadgroups( + .init(width: threadgroupCount, height: 1, depth: 1), + threadsPerThreadgroup: .init(width: threadgroupSize, height: 1, depth: 1) + ) + computeCommandEncoder.endEncoding() + + computeCommandBuffer.commit() + } + + private func checkAllParticlesDead() -> Bool { + let particleData = particleBuffer + .contents() + .bindMemory(to: Particle.self, capacity: particleCount) + + for i in 0 ..< particleCount { + if particleData[i].life >= 0 { + return false + } + } + return true + } + + private func renderParticles(in view: MTKView) { + let viewCGSize = view.bounds.size + var viewSize = simd_float2(Float(viewCGSize.width), Float(viewCGSize.height)) + + let renderCommandBuffer = commandQueue.makeCommandBuffer()! + + guard let renderPassDescriptor = view.currentRenderPassDescriptor else { return } + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].clearColor = .init(red: 0, green: 0, blue: 0, alpha: 0) + + let renderCommandEncoder = renderCommandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + renderCommandEncoder.setRenderPipelineState(renderPipeline) + renderCommandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + withUnsafeBytes(of: &viewSize) { pointer in + renderCommandEncoder.setVertexBytes( + pointer.baseAddress!, + length: MemoryLayout.size, + index: 1 + ) + } + renderCommandEncoder.setVertexBuffer(particleBuffer, offset: 0, index: 2) + withUnsafeBytes(of: &targetFrameSize) { pointer in + renderCommandEncoder.setVertexBytes( + pointer.baseAddress!, + length: MemoryLayout.size, + index: 3 + ) + } + withUnsafeBytes(of: &stepSize) { pointer in + renderCommandEncoder.setVertexBytes( + pointer.baseAddress!, + length: MemoryLayout.size, + index: 4 + ) + } + renderCommandEncoder.setFragmentTexture(texture, index: 0) + + setupSampler(renderCommandEncoder: renderCommandEncoder) + + renderCommandEncoder.drawPrimitives( + type: .triangleStrip, + vertexStart: 0, + vertexCount: 4, + instanceCount: particleCount + ) + renderCommandEncoder.endEncoding() + + renderCommandBuffer.present(view.currentDrawable!) + renderCommandBuffer.commit() + } + + private func setupSampler(renderCommandEncoder: MTLRenderCommandEncoder) { + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.mipFilter = .notMipmapped + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + let samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + renderCommandEncoder.setFragmentSamplerState(samplerState, index: 0) + } + } +} + +extension ParticleView.Renderer { + private func setupVertexBuffer(with device: MTLDevice) { + let vertices: [Vertex] = [ + .init(position: .init(0, 0, 0, 1), uv: .init(0, 0), opacity: .zero), + .init(position: .init(1, 0, 0, 1), uv: .init(1, 0), opacity: .zero), + .init(position: .init(0, 1, 0, 1), uv: .init(0, 1), opacity: .zero), + .init(position: .init(1, 1, 0, 1), uv: .init(1, 1), opacity: .zero), + ] + let vertexBuffer = vertices.withUnsafeBytes { pointer in + device.makeBuffer( + bytes: pointer.baseAddress!, + length: MemoryLayout.stride * vertices.count, + options: .storageModeShared + ) + } + self.vertexBuffer = vertexBuffer! + } + + private func setupParticleSystem(targetFrame: CGRect, device: MTLDevice) { + var particles = [Particle]() + let targetFrameHeight = Float(targetFrame.height) + let targetFrameWidth = Float(targetFrame.width) + let particleStep = 1 + + let estimatedParticleCount = 1 + * Int(targetFrameWidth / Float(particleStep)) + * Int(targetFrameHeight / Float(particleStep)) + let pixelMultiplier = 1 + particles.reserveCapacity(estimatedParticleCount * pixelMultiplier) + + for y in stride(from: 0, to: Int(targetFrameHeight), by: particleStep) { + for x in stride(from: 0, to: Int(targetFrameWidth), by: particleStep) { + let particle = createParticle(x: x, y: y, step: particleStep) + for _ in 0 ..< pixelMultiplier { + particles.append(particle) + } + } + } + + particleCount = particles.count + let particleBuffer = particles.withUnsafeBytes { pointer in + device.makeBuffer( + bytes: pointer.baseAddress!, + length: MemoryLayout.stride * particles.count, + options: .storageModeShared + ) + } + self.particleBuffer = particleBuffer! + stepSize = Float(particleStep) + } + + private func createParticle(x: Int, y: Int, step: Int) -> Particle { + let particleDuration: Float = .random(in: 20 ... 60) + let initialX = Float(x) + Float(step) / 2.0 + let initialY = Float(y) + Float(step) / 2.0 + return .init( + position: .init(initialX, initialY), + velocity: .init( + cos(Float.random(in: 0 ... (2 * Float.pi))) * Float.random(in: 1 ... 4), + sin(Float.random(in: 0 ... (2 * Float.pi))) * Float.random(in: 1 ... 4) - 2.5 + ), + life: simd_float1(particleDuration), + duration: simd_float1(particleDuration) + ) + } + + private func setupTexture(from image: CGImage, device: MTLDevice) { + let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) + + guard let context = CGContext( + data: nil, + width: image.width, + height: image.height, + bitsPerComponent: 8, + bytesPerRow: 0, + space: colorSpace, + bitmapInfo: bitmapInfo.rawValue + ) else { return } + context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) + + guard let convertedImage = context.makeImage() else { return } + + let textureLoader = MTKTextureLoader(device: device) + let textureLoaderOptions: [MTKTextureLoader.Option: Any] = [ + .textureStorageMode: MTLStorageMode.private.rawValue, + .SRGB: false, + ] + guard let texture = try? textureLoader.newTexture( + cgImage: convertedImage, + options: textureLoaderOptions + ) else { return } + + self.texture = texture + } + + private func finalizeSetup(targetFrame: CGRect, device: MTLDevice) { + let targetFrameWidth = Float(targetFrame.width) + let targetFrameHeight = Float(targetFrame.height) + targetFrameSize = .init(targetFrameWidth, targetFrameHeight) + commandQueue = device.makeCommandQueue()! + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView.swift new file mode 100644 index 000000000..f2c8b666e --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/ParticleView.swift @@ -0,0 +1,79 @@ +// +// ParticleView.swift +// TrollNFC +// +// Created by 砍砍 on 6/8/25. +// + +import MetalKit +import simd + +import UIKit + +class ParticleView: UIView { + private var device: MTLDevice! + private var metalView: MTKView! + private var renderer = Renderer() + + override init(frame: CGRect) { + super.init(frame: frame) + setupMetalDevice() + setupMetalView() + setupViewProperties() + } + + private func setupMetalDevice() { + guard let device = Self.createSystemDefaultDevice() else { + fatalError("failed to create Metal device") + } + self.device = device + } + + private func setupMetalView() { + metalView = MTKView(frame: .zero, device: device) + configureMetalView() + addSubview(metalView) + } + + private func configureMetalView() { + metalView.layer.isOpaque = false + metalView.backgroundColor = UIColor.clear + metalView.delegate = renderer + } + + private func setupViewProperties() { + clipsToBounds = false + metalView.clipsToBounds = false + } + + private static func createSystemDefaultDevice() -> MTLDevice? { + MTLCreateSystemDefaultDevice() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + func beginWith( + _ image: CGImage, + targetFrame: CGRect, + onComplete: @escaping () -> Void, + onFirstFrameRendered: @escaping () -> Void + ) { + renderer.prepareResources( + with: device, + image: image, + targetFrame: targetFrame, + onComplete: onComplete, + onFirstFrameRendered: onFirstFrameRendered + ) + metalView.draw() + } + + override func layoutSubviews() { + super.layoutSubviews() + let expandedBounds = bounds.insetBy(dx: -bounds.width, dy: -bounds.height) + metalView.frame = expandedBounds + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/UIView+createViewSnapshot.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/UIView+createViewSnapshot.swift new file mode 100644 index 000000000..a36639178 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Effect/UIView+createViewSnapshot.swift @@ -0,0 +1,22 @@ +// +// UIView+createViewSnapshot.swift +// Intelligents +// +// Created by 秋星桥 on 6/19/25. +// + +import UIKit + +public extension UIView { + func createViewSnapshot() -> UIImage { + let renderer = UIGraphicsImageRenderer(bounds: bounds) + return renderer.image { context in + // clear the background + context.cgContext.setFillColor(UIColor.clear.cgColor) + context.cgContext.fill(bounds) + + // MUST USE DRAW HIERARCHY TO RENDER VISUAL EFFECT VIEW + self.drawHierarchy(in: bounds, afterScreenUpdates: false) + } + } +} 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 e659ff48d..000000000 --- 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 54babfff8..000000000 --- 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 73fca1899..000000000 --- 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 9634cff63..000000000 --- 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 27fe21a14..000000000 --- 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 181161a3b..000000000 --- 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 7b28e6ba4..000000000 --- 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 c61f54856..000000000 --- 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 203ac3da7..000000000 --- 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 298430a7e..000000000 --- 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 4a991cbfa..000000000 --- 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 4eb2b4012..000000000 --- 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 ea84f2770..000000000 --- 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 c21bdbeb3..000000000 --- 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 370a9fea0..000000000 --- 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 bc494d6f6..000000000 --- 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 b5a3a750a..000000000 --- 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 dae48e388..000000000 --- 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 f2877e4f9..000000000 --- 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 315e8cb3d..000000000 --- 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 045596f19..000000000 --- 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 f9562f9b5..000000000 --- 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 9ba608100..000000000 --- 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 fae017aff..000000000 --- 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 29baa8ef1..000000000 --- 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 cabbb3e6f..000000000 --- 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 6794ffe43..000000000 --- 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 e257762f6..000000000 --- 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 eb13eff51..000000000 --- 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 3ba0b0622..000000000 --- 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 9216be4a3..000000000 --- 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 ee0a881d0..000000000 --- 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/Interface/Supplement/AccentColor.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/AccentColor.swift new file mode 100644 index 000000000..eec75f6f0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/AccentColor.swift @@ -0,0 +1,12 @@ +// +// AccentColor.swift +// Intelligents +// +// Created by 秋星桥 on 6/19/25. +// + +import UIKit + +extension UIColor { + static let accent: UIColor = .init(red: 30 / 255, green: 150 / 255, blue: 235 / 255, alpha: 1.0) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift new file mode 100644 index 000000000..bac7c40b8 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift @@ -0,0 +1,23 @@ +// +// Animation.swift +// Intelligents +// +// Created by 秋星桥 on 6/19/25. +// + +import UIKit + +func performWithAnimation( + animations: @escaping () -> Void, + completion: @escaping (Bool) -> Void = { _ in } +) { + UIView.animate( + withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.8, + options: [.beginFromCurrentState, .allowAnimatedContent, .curveEaseInOut], + animations: animations, + completion: completion + ) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/UIHostingView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/UIHostingView.swift deleted file mode 100644 index 86422f9ec..000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/UIHostingView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// UIHostingView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/13. -// - -import SwiftUI -import UIKit - -class UIHostingView: UIView { - private let hostingViewController: UIHostingController - - var rootView: Content { - get { hostingViewController.rootView } - set { hostingViewController.rootView = newValue } - } - - override var intrinsicContentSize: CGSize { - hostingViewController.view.intrinsicContentSize - } - - init(rootView: Content) { - hostingViewController = UIHostingController(rootView: rootView) - hostingViewController.edgesForExtendedLayout = [] - hostingViewController.extendedLayoutIncludesOpaqueBars = false - super.init(frame: .zero) - - hostingViewController.view?.translatesAutoresizingMaskIntoConstraints = false - addSubview(hostingViewController.view) - if let view = hostingViewController.view { - view.removeFromSuperview() - view.backgroundColor = .clear - view.isOpaque = false - addSubview(view) - let constraints = [ - view.topAnchor.constraint(equalTo: topAnchor), - view.bottomAnchor.constraint(equalTo: bottomAnchor), - view.leftAnchor.constraint(equalTo: leftAnchor), - view.rightAnchor.constraint(equalTo: rightAnchor), - ] - NSLayoutConstraint.activate(constraints) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func sizeThatFits(_ size: CGSize) -> CGSize { - hostingViewController.sizeThatFits(in: size) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift new file mode 100644 index 000000000..3b9745259 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift @@ -0,0 +1,287 @@ +import Combine +import SnapKit +import Then +import UIKit + +class InputBox: UIView { + weak var delegate: InputBoxDelegate? + + public let viewModel = InputBoxViewModel() + var cancellables = Set() + + lazy var containerView = UIView().then { + $0.backgroundColor = UIColor.affineLayerBackgroundPrimary + $0.layer.cornerRadius = 16 + $0.layer.cornerCurve = .continuous + $0.layer.borderWidth = 1 + $0.layer.shadowColor = UIColor.black.cgColor + $0.layer.shadowOffset = CGSize(width: 0, height: 0) + $0.layer.shadowRadius = 12 + $0.layer.shadowOpacity = 0.075 + $0.clipsToBounds = false + } + + lazy var textView = UITextView().then { + $0.backgroundColor = .clear + $0.font = .systemFont(ofSize: 16) + $0.textColor = UIColor.affineTextPrimary + $0.isScrollEnabled = false + $0.textContainer.lineFragmentPadding = 0 + $0.textContainerInset = .zero + $0.delegate = self + $0.text = "" + } + + lazy var placeholderLabel = UILabel().then { + $0.text = "Write your message..." + $0.font = .systemFont(ofSize: 16) + $0.textColor = UIColor.affineTextPlaceholder + $0.isHidden = true + } + + lazy var functionBar = InputBoxFunctionBar().then { + $0.delegate = self + } + + lazy var imageBar = InputBoxImageBar().then { + $0.imageBarDelegate = self + } + + lazy var mainStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 16 + $0.alignment = .fill + $0.clipsToBounds = true + $0.addArrangedSubview(imageBar) + $0.addArrangedSubview(textView) + $0.addArrangedSubview(functionBar) + } + + var textViewHeightConstraint: Constraint? + let minTextViewHeight: CGFloat = 22 + let maxTextViewHeight: CGFloat = 100 + + var text: String { + get { textView.text ?? "" } + set { + textView.text = newValue + updatePlaceholderVisibility() + updateTextViewHeight() + } + } + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + + backgroundColor = .clear + addSubview(containerView) + containerView.addSubview(mainStackView) + containerView.addSubview(placeholderLabel) + imageBar.isHidden = true + + containerView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(16) + } + + mainStackView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(16) + } + + imageBar.snp.makeConstraints { make in + make.left.right.equalToSuperview() + } + + textView.snp.makeConstraints { make in + textViewHeightConstraint = make.height.equalTo(minTextViewHeight).constraint + } + + placeholderLabel.snp.makeConstraints { make in + make.left.right.equalTo(textView) + make.top.equalTo(textView) + } + + setupBindings() + updatePlaceholderVisibility() + updateColors() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + updateColors() + } + } + + func setupBindings() { + // 绑定 ViewModel 到 UI + viewModel.$inputText + .removeDuplicates() + .sink { [weak self] text in + if self?.textView.text != text { + self?.textView.text = text + self?.updatePlaceholderVisibility() + self?.updateTextViewHeight() + } + } + .store(in: &cancellables) + + viewModel.$isToolEnabled + .removeDuplicates() + .sink { [weak self] enabled in + self?.functionBar.updateToolState(isEnabled: enabled) + } + .store(in: &cancellables) + + viewModel.$isNetworkEnabled + .removeDuplicates() + .sink { [weak self] enabled in + self?.functionBar.updateNetworkState(isEnabled: enabled) + } + .store(in: &cancellables) + + viewModel.$isDeepThinkingEnabled + .removeDuplicates() + .sink { [weak self] enabled in + self?.functionBar.updateDeepThinkingState(isEnabled: enabled) + } + .store(in: &cancellables) + + viewModel.$canSend + .removeDuplicates() + .sink { [weak self] canSend in + self?.functionBar.updateSendState(canSend: canSend) + } + .store(in: &cancellables) + + viewModel.$hasAttachments + .dropFirst() // for view setup + .removeDuplicates() + .sink { [weak self] hasAttachments in + performWithAnimation { + self?.updateImageBarVisibility(hasAttachments) + self?.layoutIfNeeded() + } + } + .store(in: &cancellables) + + viewModel.$attachments + .removeDuplicates() + .sink { [weak self] attachments in + self?.updateImageBarContent(attachments) + } + .store(in: &cancellables) + } + + func updateTextViewHeight() { + let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: CGFloat.greatestFiniteMagnitude)) + let newHeight = max(minTextViewHeight, min(maxTextViewHeight, size.height)) + + let height = textView.frame.height + guard height != newHeight else { return } + + textViewHeightConstraint?.update(offset: newHeight) + textView.isScrollEnabled = size.height > maxTextViewHeight + + if height == 0 || superview == nil || window == nil || isHidden { return } + + performWithAnimation { + self.layoutIfNeeded() + self.superview?.layoutIfNeeded() + } + } + + func updatePlaceholderVisibility() { + placeholderLabel.isHidden = !textView.text.isEmpty + } + + func updateImageBarVisibility(_ hasAttachments: Bool) { + imageBar.isHidden = !hasAttachments + } + + func updateImageBarContent(_ attachments: [InputAttachment]) { + imageBar.updateImageBarContent(attachments) + } + + func updateColors() { + containerView.layer.borderColor = UIColor.affineLayerBorder.cgColor + } + + // MARK: - Public Methods + + public func addImageAttachment(_ image: UIImage) { + guard let imageData = image.jpegData(compressionQuality: 0.8) else { return } + + let attachment = InputAttachment( + type: .image, + data: imageData, + name: "image.jpg", + size: Int64(imageData.count) + ) + + performWithAnimation { [self] in + viewModel.addAttachment(attachment) + layoutIfNeeded() + } + } + + public func addFileAttachment(_ url: URL) { + guard let fileData = try? Data(contentsOf: url) else { return } + + let attachment = InputAttachment( + type: .file, + data: fileData, + name: url.lastPathComponent, + size: Int64(fileData.count) + ) + + performWithAnimation { [self] in + viewModel.addAttachment(attachment) + layoutIfNeeded() + } + } + + public var inputBoxData: InputBoxData { + viewModel.prepareSendData() + } +} + +// MARK: - InputBoxFunctionBarDelegate + +extension InputBox: InputBoxFunctionBarDelegate { + func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectTakePhoto(self) + } + + func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectPhotoLibrary(self) + } + + func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectAttachFiles(self) + } + + func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectEmbedDocs(self) + } + + func functionBarDidTapTool(_: InputBoxFunctionBar) { + viewModel.toggleTool() + } + + func functionBarDidTapNetwork(_: InputBoxFunctionBar) { + viewModel.toggleNetwork() + } + + func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) { + viewModel.toggleDeepThinking() + } + + func functionBarDidTapSend(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSend(self) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.xcassets/InputBoxBackground.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.xcassets/InputBoxBackground.colorset/Contents.json new file mode 100644 index 000000000..0cba25aa6 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.xcassets/InputBoxBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "32", + "green" : "32", + "red" : "32" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift new file mode 100644 index 000000000..b89494913 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift @@ -0,0 +1,35 @@ +// +// InputBoxDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/18/25. +// + +import UIKit + +protocol InputBoxDelegate: AnyObject { + func inputBoxDidSelectTakePhoto(_ inputBox: InputBox) + func inputBoxDidSelectPhotoLibrary(_ inputBox: InputBox) + func inputBoxDidSelectAttachFiles(_ inputBox: InputBox) + func inputBoxDidSelectEmbedDocs(_ inputBox: InputBox) + func inputBoxDidSend(_ inputBox: InputBox) + func inputBoxTextDidChange(_ text: String) +} + +extension InputBox: InputBoxImageBarDelegate { + func inputBoxImageBar(_: InputBoxImageBar, didRemoveImageWithId id: InputAttachment.ID) { + performWithAnimation { [self] in + viewModel.removeAttachment(withId: id) + layoutIfNeeded() + } + } +} + +extension InputBox: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + viewModel.updateText(textView.text ?? "") + delegate?.inputBoxTextDidChange(textView.text ?? "") + updatePlaceholderVisibility() + updateTextViewHeight() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift new file mode 100644 index 000000000..cdd917670 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift @@ -0,0 +1,204 @@ +import SnapKit +import Then +import UIKit + +protocol InputBoxFunctionBarDelegate: AnyObject { + func functionBarDidTapTakePhoto(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapPhotoLibrary(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapAttachFiles(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapEmbedDocs(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar) +} + +private let unselectedColor: UIColor = UIColor.affineIconPrimary +private let selectedColor: UIColor = UIColor.affineIconActivated + +class InputBoxFunctionBar: UIView { + weak var delegate: InputBoxFunctionBarDelegate? + + lazy var attachmentButton = UIButton(type: .system).then { + $0.setImage(UIImage.affinePlus, for: .normal) + $0.tintColor = unselectedColor + $0.layer.borderWidth = 1 + $0.layer.cornerRadius = 4 + $0.imageView?.contentMode = .scaleAspectFit + $0.showsMenuAsPrimaryAction = true + $0.menu = createAttachmentMenu() + } + + lazy var toolButton = UIButton(type: .system).then { + $0.setImage(UIImage.affineTools, for: .normal) + $0.tintColor = unselectedColor + $0.imageView?.contentMode = .scaleAspectFit + $0.addTarget(self, action: #selector(toolButtonTapped), for: .touchUpInside) + } + + lazy var networkButton = UIButton(type: .system).then { + $0.setImage(UIImage.affineWeb, for: .normal) + $0.tintColor = unselectedColor + $0.imageView?.contentMode = .scaleAspectFit + $0.addTarget(self, action: #selector(networkButtonTapped), for: .touchUpInside) + } + + lazy var deepThinkingButton = UIButton(type: .system).then { + $0.setImage(UIImage.affineThink, for: .normal) + $0.tintColor = unselectedColor + $0.imageView?.contentMode = .scaleAspectFit + $0.addTarget(self, action: #selector(deepThinkingButtonTapped), for: .touchUpInside) + } + + lazy var sendButton = UIButton(type: .system).then { + $0.setImage(UIImage.affineArrowUpBig, for: .normal) + $0.tintColor = UIColor.affineTextPureWhite + $0.backgroundColor = UIColor.affineButtonPrimary + $0.imageView?.contentMode = .scaleAspectFit + $0.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside) + $0.clipsToBounds = true + } + + lazy var leftButtonsStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 16 + $0.alignment = .center + $0.addArrangedSubview(attachmentButton) + } + + lazy var rightButtonsStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 16 + $0.alignment = .center + $0.addArrangedSubview(toolButton) + $0.addArrangedSubview(networkButton) + $0.addArrangedSubview(deepThinkingButton) + $0.addArrangedSubview(sendButton) + } + + lazy var stackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 12 + $0.alignment = .center + $0.addArrangedSubview(leftButtonsStackView) + $0.addArrangedSubview(UIView()) // spacer + $0.addArrangedSubview(rightButtonsStackView) + } + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + for button in [attachmentButton, toolButton, networkButton, deepThinkingButton, sendButton] { + button.snp.makeConstraints { make in + make.width.height.equalTo(32) + } + } + updateColors() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + sendButton.layer.cornerRadius = sendButton.bounds.height / 2 + for button in [toolButton, networkButton, deepThinkingButton] { + button.layer.cornerRadius = button.bounds.height / 2 + } + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + updateColors() + } + } + + // MARK: - Public Methods + + func updateToolState(isEnabled: Bool) { + toolButton.tintColor = isEnabled ? selectedColor : unselectedColor + } + + func updateNetworkState(isEnabled: Bool) { + networkButton.tintColor = isEnabled ? selectedColor : unselectedColor + } + + func updateDeepThinkingState(isEnabled: Bool) { + deepThinkingButton.tintColor = isEnabled ? selectedColor : unselectedColor + } + + func updateSendState(canSend: Bool) { + sendButton.isEnabled = canSend + sendButton.alpha = canSend ? 1.0 : 0.5 + } + + // MARK: - Private Methods + + private func updateColors() { + attachmentButton.layer.borderColor = UIColor.affineLayerBorder.cgColor + } + + private func createAttachmentMenu() -> UIMenu { + let takePhotoAction = UIAction( + title: "Take Photo or Video", + image: UIImage.affineCamera + ) { [weak self] _ in + guard let self else { return } + delegate?.functionBarDidTapTakePhoto(self) + } + + let photoLibraryAction = UIAction( + title: "Photo Library", + image: UIImage.affineImage + ) { [weak self] _ in + guard let self else { return } + delegate?.functionBarDidTapPhotoLibrary(self) + } + + let attachFilesAction = UIAction( + title: "Attach Files (pdf, txt, csv)", + image: UIImage.affineUpload + ) { [weak self] _ in + guard let self else { return } + delegate?.functionBarDidTapAttachFiles(self) + } + + let embedDocsAction = UIAction( + title: "Embed AFFINE Docs", + image: UIImage.affinePage + ) { [weak self] _ in + guard let self else { return } + delegate?.functionBarDidTapEmbedDocs(self) + } + + return UIMenu( + options: [.displayInline], + children: [takePhotoAction, photoLibraryAction, attachFilesAction, embedDocsAction].reversed() + ) + } + + // MARK: - Actions + + @objc private func toolButtonTapped() { + delegate?.functionBarDidTapTool(self) + } + + @objc private func networkButtonTapped() { + delegate?.functionBarDidTapNetwork(self) + } + + @objc private func deepThinkingButtonTapped() { + delegate?.functionBarDidTapDeepThinking(self) + } + + @objc private func sendButtonTapped() { + delegate?.functionBarDidTapSend(self) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift new file mode 100644 index 000000000..2afaf18fe --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxImageBar.swift @@ -0,0 +1,180 @@ +// +// InputBoxImageBar.swift +// Intelligents +// +// Created by 秋星桥 on 6/18/25. +// + +import SnapKit +import Then +import UIKit + +protocol InputBoxImageBarDelegate: AnyObject { + func inputBoxImageBar(_ imageBar: InputBoxImageBar, didRemoveImageWithId id: UUID) +} + +private class AttachmentViewModel { + let attachment: InputAttachment + let imageCell: InputBoxImageBar.ImageCell + + init(attachment: InputAttachment, imageCell: InputBoxImageBar.ImageCell) { + self.attachment = attachment + self.imageCell = imageCell + } +} + +class InputBoxImageBar: UIScrollView { + weak var imageBarDelegate: InputBoxImageBarDelegate? + + private var attachmentViewModels: [AttachmentViewModel] = [] + private let cellSpacing: CGFloat = 8 + private let constantHeight: CGFloat = 80 + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + + snp.makeConstraints { make in + make.height.equalTo(constantHeight) + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + func updateImageBarContent(_ attachments: [InputAttachment]) { + let currentIds = Set(attachmentViewModels.map(\.attachment.id)) + let imageAttachments = attachments.filter { $0.type == .image } + let newIds = Set(imageAttachments.map(\.id)) + + // 移除不再存在的附件 + let idsToRemove = currentIds.subtracting(newIds) + for id in idsToRemove { + if let index = attachmentViewModels.firstIndex(where: { $0.attachment.id == id }) { + let viewModel = attachmentViewModels.remove(at: index) + viewModel.imageCell.removeFromSuperview() + } + } + + // 添加新的附件 + let idsToAdd = newIds.subtracting(currentIds) + var initialXOffset = attachmentViewModels.reduce(0) { $0 + $1.imageCell.frame.width + cellSpacing } + for attachment in imageAttachments { + if idsToAdd.contains(attachment.id), + let data = attachment.data, + let image = UIImage(data: data) + { + let imageCell = ImageCell( + // for animation to work + frame: .init(x: initialXOffset, y: 0, width: constantHeight, height: constantHeight), + image: image, + attachmentId: attachment.id + ) + initialXOffset += constantHeight + cellSpacing + imageCell.onRemove = { [weak self] cell in + self?.removeImageCell(cell) + } + imageCell.alpha = 0 + DispatchQueue.main.async { + performWithAnimation { imageCell.alpha = 1 } + } + + let viewModel = AttachmentViewModel(attachment: attachment, imageCell: imageCell) + attachmentViewModels.append(viewModel) + addSubview(imageCell) + } + } + + layoutImageCells() + } + + func removeImageCell(_ cell: ImageCell) { + if let index = attachmentViewModels.firstIndex(where: { $0.imageCell === cell }) { + let viewModel = attachmentViewModels.remove(at: index) + viewModel.imageCell.removeFromSuperviewWithExplodeEffect() + imageBarDelegate?.inputBoxImageBar(self, didRemoveImageWithId: cell.attachmentId) + layoutImageCells() + } + } + + func clear() { + for viewModel in attachmentViewModels { + viewModel.imageCell.removeFromSuperview() + } + attachmentViewModels.removeAll() + contentSize = .zero + } + + private func layoutImageCells() { + var xOffset: CGFloat = 0 + + for viewModel in attachmentViewModels { + viewModel.imageCell.frame = CGRect(x: xOffset, y: 0, width: constantHeight, height: constantHeight) + xOffset += constantHeight + cellSpacing + } + + // Update content size + let totalWidth = max(0, xOffset - cellSpacing) + contentSize = CGSize(width: totalWidth, height: constantHeight) + } +} + +extension InputBoxImageBar { + class ImageCell: UIView { + let attachmentId: UUID + var onRemove: ((ImageCell) -> Void)? + + private lazy var imageView = UIImageView(frame: bounds).then { + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + $0.layer.cornerRadius = 12 + $0.layer.cornerCurve = .continuous + $0.backgroundColor = .systemGray6 + } + + private lazy var removeButton = DeleteButtonView(frame: removeButtonFrame).then { + $0.onTapped = { [weak self] in + self?.removeButtonTapped() + } + } + + init(frame: CGRect, image: UIImage, attachmentId: UUID) { + self.attachmentId = attachmentId + super.init(frame: frame) + addSubview(imageView) + addSubview(removeButton) + imageView.image = image + } + + var removeButtonFrame: CGRect { + let buttonSize: CGFloat = 18 + let buttonInset: CGFloat = 6 + return CGRect( + x: bounds.width - buttonSize - buttonInset, + y: buttonInset, + width: buttonSize, + height: buttonSize + ) + } + + override func layoutSubviews() { + super.layoutSubviews() + + imageView.frame = bounds + + removeButton.frame = removeButtonFrame + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + @objc private func removeButtonTapped() { + onRemove?(self) + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift new file mode 100644 index 000000000..9a3a24912 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxViewModel.swift @@ -0,0 +1,164 @@ +// +// InputBoxViewModel.swift +// Intelligents +// +// Created by AI Assistant on 6/17/25. +// + +import Combine +import Foundation + +// MARK: - Data Models + +public struct InputAttachment: Identifiable, Equatable, Hashable, Codable { + public var id: UUID = .init() + public var type: AttachmentType + public var data: Data? + public var url: URL? + public var name: String + public var size: Int64 + + public enum AttachmentType: String, Equatable, Hashable, Codable { + case image + case document + case file + } + + public init( + type: AttachmentType, + data: Data? = nil, + url: URL? = nil, + name: String, + size: Int64 = 0 + ) { + self.type = type + self.data = data + self.url = url + self.name = name + self.size = size + } +} + +public struct InputBoxData { + public var text: String + public var attachments: [InputAttachment] + public var isToolEnabled: Bool + public var isNetworkEnabled: Bool + public var isDeepThinkingEnabled: Bool + + public init( + text: String, + attachments: [InputAttachment], + isToolEnabled: Bool, + isNetworkEnabled: Bool, + isDeepThinkingEnabled: Bool + ) { + self.text = text + self.attachments = attachments + self.isToolEnabled = isToolEnabled + self.isNetworkEnabled = isNetworkEnabled + self.isDeepThinkingEnabled = isDeepThinkingEnabled + } +} + +// MARK: - View Model + +public class InputBoxViewModel: ObservableObject { + // MARK: - Published Properties + + @Published public var inputText: String = "" + @Published public var isToolEnabled: Bool = false + @Published public var isNetworkEnabled: Bool = false + @Published public var isDeepThinkingEnabled: Bool = false + @Published public var hasAttachments: Bool = false + @Published public var attachments: [InputAttachment] = [] + @Published public var canSend: Bool = false + + // MARK: - Private Properties + + private var cancellables = Set() + + // MARK: - Initialization + + public init() { + setupBindings() + } + + // MARK: - Private Methods + + private func setupBindings() { + // 监听文本变化,自动更新发送按钮状态 + $inputText + .map { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + .assign(to: \.canSend, on: self) + .store(in: &cancellables) + + // 监听附件变化 + $attachments + .map { !$0.isEmpty } + .assign(to: \.hasAttachments, on: self) + .store(in: &cancellables) + } +} + +// MARK: - Text Management + +public extension InputBoxViewModel { + func updateText(_ text: String) { + inputText = text + } +} + +// MARK: - Feature Toggles + +public extension InputBoxViewModel { + func toggleTool() { + isToolEnabled.toggle() + } + + func toggleNetwork() { + isNetworkEnabled.toggle() + } + + func toggleDeepThinking() { + isDeepThinkingEnabled.toggle() + } +} + +// MARK: - Attachment Management + +public extension InputBoxViewModel { + func addAttachment(_ attachment: InputAttachment) { + attachments.append(attachment) + } + + func removeAttachment(withId id: UUID) { + attachments.removeAll { $0.id == id } + } + + func clearAttachments() { + attachments.removeAll() + } +} + +// MARK: - Send Management + +public extension InputBoxViewModel { + func prepareSendData() -> InputBoxData { + InputBoxData( + text: inputText.trimmingCharacters(in: .whitespacesAndNewlines), + attachments: attachments, + isToolEnabled: isToolEnabled, + isNetworkEnabled: isNetworkEnabled, + isDeepThinkingEnabled: isDeepThinkingEnabled + ) + } + + func resetInput() { + inputText = "" + attachments.removeAll() + isToolEnabled = false + isNetworkEnabled = false + isDeepThinkingEnabled = false + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift similarity index 70% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift rename to packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift index 117c2c594..d389a056b 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Control.swift @@ -5,6 +5,7 @@ // Created by 秋星桥 on 2024/11/18. // +import SnapKit import UIKit public extension UIViewController { @@ -16,15 +17,15 @@ public extension UIViewController { let button = IntelligentsButton() view.addSubview(button) view.bringSubviewToFront(button) - button.translatesAutoresizingMaskIntoConstraints = false - [ - button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), - button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20 - Constant.affineTabbarHeight), - button.widthAnchor.constraint(equalToConstant: 50), - button.heightAnchor.constraint(equalToConstant: 50), - ].forEach { $0.isActive = true } + button.snp.makeConstraints { make in + make.trailing.equalTo(view.safeAreaLayoutGuide).offset(-20) + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20 - 44) + make.width.height.equalTo(50) + } button.transform = .init(scaleX: 0, y: 0) - view.layoutIfNeeded() + if view.frame != .zero { + view.layoutIfNeeded() + } return button } @@ -47,12 +48,7 @@ public extension UIViewController { button.stopProgress() view.layoutIfNeeded() - UIView.animate( - withDuration: 0.5, - delay: 0, - usingSpringWithDamping: 1.0, - initialSpringVelocity: 0.8 - ) { + performWithAnimation { button.alpha = 1 button.transform = .identity button.setNeedsLayout() @@ -77,12 +73,7 @@ public extension UIViewController { button.stopProgress() button.setNeedsLayout() view.layoutIfNeeded() - UIView.animate( - withDuration: 0.5, - delay: 0, - usingSpringWithDamping: 1.0, - initialSpringVelocity: 0.8 - ) { + performWithAnimation { button.alpha = 0 button.transform = .init(scaleX: 0, y: 0) button.setNeedsLayout() diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Delegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Delegate.swift similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Delegate.swift rename to packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton+Delegate.swift diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift similarity index 51% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift rename to packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift index 60572c95b..a633a5c12 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/IntelligentsButton/IntelligentsButton.swift @@ -5,13 +5,26 @@ // Created by 秋星桥 on 2024/11/18. // +import SnapKit +import SwifterSwift +import Then import UIKit // floating button to open intelligent panel public class IntelligentsButton: UIView { - let image = UIImageView() - let background = UIView() - let activityIndicator = UIActivityIndicatorView() + lazy var image = UIImageView().then { + $0.image = .init(named: "spark", in: .module, with: .none) + $0.contentMode = .scaleAspectFit + } + + lazy var background = UIView().then { + $0.backgroundColor = .init( + light: .systemBackground, + dark: .darkGray.withAlphaComponent(0.25) + ) + } + + lazy var activityIndicator = UIActivityIndicatorView() public weak var delegate: (any IntelligentsButtonDelegate)? = nil { didSet { assert(Thread.isMainThread) } @@ -19,45 +32,10 @@ public class IntelligentsButton: UIView { public init() { super.init(frame: .zero) - - background.backgroundColor = .white - addSubview(background) - background.translatesAutoresizingMaskIntoConstraints = false - [ - background.leadingAnchor.constraint(equalTo: leadingAnchor), - background.trailingAnchor.constraint(equalTo: trailingAnchor), - background.topAnchor.constraint(equalTo: topAnchor), - background.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - - image.image = .init(named: "spark", in: .module, with: .none) - image.contentMode = .scaleAspectFit - image.tintColor = .accent - addSubview(image) - let imageInsetValue: CGFloat = 12 - image.translatesAutoresizingMaskIntoConstraints = false - [ - image.leadingAnchor.constraint(equalTo: leadingAnchor, constant: imageInsetValue), - image.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -imageInsetValue), - image.topAnchor.constraint(equalTo: topAnchor, constant: imageInsetValue), - image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue), - ].forEach { $0.isActive = true } - - activityIndicator.translatesAutoresizingMaskIntoConstraints = false - addSubview(activityIndicator) - [ - activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), - ].forEach { $0.isActive = true } - - clipsToBounds = true - layer.borderWidth = 2 - layer.borderColor = UIColor.gray.withAlphaComponent(0.1).cgColor - - let tap = UITapGestureRecognizer(target: self, action: #selector(tapped)) - addGestureRecognizer(tap) - isUserInteractionEnabled = true - + setupViews() + setupConstraints() + setupGesture() + setupAppearance() stopProgress() } @@ -97,3 +75,39 @@ public class IntelligentsButton: UIView { image.isHidden = false } } + +// MARK: - Setup Methods + +private extension IntelligentsButton { + func setupViews() { + addSubview(background) + addSubview(image) + addSubview(activityIndicator) + } + + func setupConstraints() { + background.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + image.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(12) + } + + activityIndicator.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + func setupGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(tapped)) + addGestureRecognizer(tap) + isUserInteractionEnabled = true + } + + func setupAppearance() { + clipsToBounds = true + layer.borderWidth = 2 + layer.borderColor = UIColor.gray.withAlphaComponent(0.1).cgColor + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift new file mode 100644 index 000000000..c9859acd1 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift @@ -0,0 +1,121 @@ +import SnapKit +import Then +import UIKit + +protocol MainHeaderViewDelegate: AnyObject { + func mainHeaderViewDidTapClose() + func mainHeaderViewDidTapDropdown() + func mainHeaderViewDidTapMenu() +} + +class MainHeaderView: UIView { + weak var delegate: MainHeaderViewDelegate? + + private lazy var closeButton = UIButton(type: .system).then { + $0.imageView?.contentMode = .scaleAspectFit + $0.setImage(UIImage.affineClose, for: .normal) + $0.tintColor = UIColor.affineIconPrimary + $0.backgroundColor = UIColor.affineLayerBackgroundSecondary + $0.layer.cornerRadius = 8 + $0.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + $0.setContentHuggingPriority(.required, for: .horizontal) + } + + private lazy var titleLabel = UILabel().then { + $0.text = "AFFiNE AI" + $0.font = .systemFont(ofSize: 16, weight: .medium) + $0.textColor = UIColor.affineTextPrimary + $0.textAlignment = .center + } + + private lazy var dropdownButton = UIButton(type: .system).then { + $0.imageView?.contentMode = .scaleAspectFit + $0.setImage(UIImage.affineArrowDown, for: .normal) + $0.tintColor = UIColor.affineIconPrimary + $0.addTarget(self, action: #selector(dropdownButtonTapped), for: .touchUpInside) + } + + private lazy var centerStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 8 + $0.alignment = .center + $0.addArrangedSubview(titleLabel) + $0.addArrangedSubview(dropdownButton) + } + + private lazy var menuButton = UIButton(type: .system).then { + $0.imageView?.contentMode = .scaleAspectFit + $0.setImage(UIImage.affineMore, for: .normal) + $0.tintColor = UIColor.affineIconPrimary + $0.backgroundColor = UIColor.affineLayerBackgroundSecondary + $0.layer.cornerRadius = 8 + $0.addTarget(self, action: #selector(menuButtonTapped), for: .touchUpInside) + $0.setContentHuggingPriority(.required, for: .horizontal) + } + + private lazy var leftSpacerView = UIView() + + private lazy var rightSpacerView = UIView() + + private lazy var mainStackView = UIStackView().then { + $0.axis = .horizontal + $0.alignment = .center + $0.distribution = .fill + $0.spacing = 16 + $0.addArrangedSubview(closeButton) + $0.addArrangedSubview(leftSpacerView) + $0.addArrangedSubview(centerStackView) + $0.addArrangedSubview(rightSpacerView) + $0.addArrangedSubview(menuButton) + } + + init() { + super.init(frame: .zero) + + backgroundColor = UIColor.affineLayerBackgroundPrimary + addSubview(mainStackView) + + mainStackView.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(16) + make.centerY.equalToSuperview() + } + + closeButton.snp.makeConstraints { make in + make.size.equalTo(titleLabel.font.pointSize + 16) + } + + menuButton.snp.makeConstraints { make in + make.size.equalTo(titleLabel.font.pointSize + 16) + } + + dropdownButton.snp.makeConstraints { make in + make.size.equalTo(titleLabel.font.pointSize + 16) + } + + // ensure center stack to be center + leftSpacerView.snp.makeConstraints { make in + make.width.equalTo(rightSpacerView) + } + + snp.makeConstraints { make in + make.height.equalTo(52) + } + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + fatalError() + } + + @objc private func closeButtonTapped() { + delegate?.mainHeaderViewDidTapClose() + } + + @objc private func dropdownButtonTapped() { + delegate?.mainHeaderViewDidTapDropdown() + } + + @objc private func menuButtonTapped() { + delegate?.mainHeaderViewDidTapMenu() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/CircleImageView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/CircleImageView.swift similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/CircleImageView.swift rename to packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/CircleImageView.swift diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/DarkActionButton.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DarkActionButton.swift similarity index 100% rename from packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/DarkActionButton.swift rename to packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DarkActionButton.swift diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DeleteButtonView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DeleteButtonView.swift new file mode 100644 index 000000000..33fbf1f29 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/SupplementView/DeleteButtonView.swift @@ -0,0 +1,56 @@ +import UIKit + +class DeleteButtonView: UIView { + let imageView = UIImageView(image: .init(systemName: "xmark")).then { + $0.tintColor = .white + $0.contentMode = .scaleAspectFit + } + + let blur = UIVisualEffectView( + effect: UIBlurEffect(style: .systemUltraThinMaterialDark) + ).then { + $0.clipsToBounds = true + } + + var onTapped: () -> Void = {} + + override init(frame: CGRect) { + super.init(frame: frame) + + isUserInteractionEnabled = true + + addSubview(blur) + addSubview(imageView) + + blur.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + imageView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(2) + } + + let gesture = UITapGestureRecognizer(target: self, action: #selector(tapped)) + addGestureRecognizer(gesture) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if bounds.width < 50 || bounds.height < 50 { + return bounds.insetBy(dx: -20, dy: -20).contains(point) + } + return super.point(inside: point, with: event) + } + + override func layoutSubviews() { + super.layoutSubviews() + blur.layer.cornerRadius = min(bounds.width, bounds.height) / 2 + } + + @objc func tapped() { + onTapped() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift new file mode 100644 index 000000000..605ff9261 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift @@ -0,0 +1,44 @@ +// +// IntelligentContext.swift +// Intelligents +// +// Created by 秋星桥 on 6/17/25. +// + +import Combine +import Foundation +import WebKit + +public class IntelligentContext { + // shared across the app, we expect our app to have a single context and webview + public static let shared = IntelligentContext() + + public var webView: WKWebView! + + public lazy var temporaryDirectory: URL = { + let tempDir = FileManager.default.temporaryDirectory + return tempDir.appendingPathComponent("IntelligentContext") + }() + + private init() {} + + public func preparePresent(_ completion: @escaping () -> Void) { + DispatchQueue.global(qos: .userInitiated).async { [self] in + prepareTemporaryDirectory() + // TODO: used to gathering information, populate content from webview, etc. + DispatchQueue.main.async { + completion() + } + } + } + + func prepareTemporaryDirectory() { + if FileManager.default.fileExists(atPath: temporaryDirectory.path) { + try? FileManager.default.removeItem(at: temporaryDirectory) + } + try? FileManager.default.createDirectory( + at: temporaryDirectory, + withIntermediateDirectories: true + ) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/UIColor+Affine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/UIColor+Affine.swift new file mode 100644 index 000000000..9b1594604 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/UIColor+Affine.swift @@ -0,0 +1,78 @@ +import UIKit + +extension UIColor { + /// Primary icon color + static var affineIconPrimary: UIColor { + UIColor(named: "affine.icon.primary", in: .module, compatibleWith: nil) ?? .black + } + + /// Primary background layer color + static var affineLayerBackgroundPrimary: UIColor { + UIColor(named: "affine.layer.background.primary", in: .module, compatibleWith: nil) ?? .white + } + + /// Secondary background layer color + static var affineLayerBackgroundSecondary: UIColor { + UIColor(named: "affine.layer.background.secondary", in: .module, compatibleWith: nil) ?? .systemGray6 + } + + /// Border layer color + static var affineLayerBorder: UIColor { + UIColor(named: "affine.layer.border", in: .module, compatibleWith: nil) ?? .gray + } + + /// Pure white layer color + static var affineLayerPureWhite: UIColor { + UIColor(named: "affine.layer.pureWhite", in: .module, compatibleWith: nil) ?? .white + } + + /// Primary button color + static var affineButtonPrimary: UIColor { + UIColor(named: "affine.button.primary", in: .module, compatibleWith: nil) ?? .blue + } + + /// Activated icon color + static var affineIconActivated: UIColor { + UIColor(named: "affine.icon.activated", in: .module, compatibleWith: nil) ?? .blue + } + + /// Text emphasis color + static var affineTextEmphasis: UIColor { + UIColor(named: "affine.text.emphasis", in: .module, compatibleWith: nil) ?? .blue + } + + /// Text link color + static var affineTextLink: UIColor { + UIColor(named: "affine.text.link", in: .module, compatibleWith: nil) ?? .blue + } + + /// List dot and number color + static var affineTextListDotAndNumber: UIColor { + UIColor(named: "affine.text.listDotAndNumber", in: .module, compatibleWith: nil) ?? .blue + } + + /// Placeholder text color + static var affineTextPlaceholder: UIColor { + UIColor(named: "affine.text.placeholder", in: .module, compatibleWith: nil) ?? .gray + } + + /// Primary text color + static var affineTextPrimary: UIColor { + UIColor(named: "affine.text.primary", in: .module, compatibleWith: nil) ?? .black + } + + /// Pure white text color + static var affineTextPureWhite: UIColor { + UIColor(named: "affine.text.pureWhite", in: .module, compatibleWith: nil) ?? .white + } + + /// Secondary text color + static var affineTextSecondary: UIColor { + UIColor(named: "affine.text.secondary", in: .module, compatibleWith: nil) ?? .gray + } + + /// Tertiary text color + static var affineTextTertiary: UIColor { + UIColor(named: "affine.text.tertiary", in: .module, compatibleWith: nil) ?? .gray + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/UIImage+Affine.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/UIImage+Affine.swift new file mode 100644 index 000000000..1468143f0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/UIImage+Affine.swift @@ -0,0 +1,93 @@ +import UIKit + +extension UIImage { + /// Check circle icon + static var affineCheckCircle: UIImage { + UIImage(named: "CheckCircle", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// More options icon + static var affineMore: UIImage { + UIImage(named: "More", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Arrow down icon + static var affineArrowDown: UIImage { + UIImage(named: "ArrowDown", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Tools icon + static var affineTools: UIImage { + UIImage(named: "Tools", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Box icon + static var affineBox: UIImage { + UIImage(named: "Box", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Think icon + static var affineThink: UIImage { + UIImage(named: "Think", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Web icon + static var affineWeb: UIImage { + UIImage(named: "Web", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Calendar icon + static var affineCalendar: UIImage { + UIImage(named: "Calendar", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Camera icon + static var affineCamera: UIImage { + UIImage(named: "Camera", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Close icon + static var affineClose: UIImage { + UIImage(named: "Close", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Big arrow up icon + static var affineArrowUpBig: UIImage { + UIImage(named: "ArrowUpBig", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Broom icon + static var affineBroom: UIImage { + UIImage(named: "Broom", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Bubble icon + static var affineBubble: UIImage { + UIImage(named: "Bubble", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Image icon + static var affineImage: UIImage { + UIImage(named: "Image", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Page icon + static var affinePage: UIImage { + UIImage(named: "Page", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Plus icon + static var affinePlus: UIImage { + UIImage(named: "Plus", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Settings icon + static var affineSettings: UIImage { + UIImage(named: "Settings", in: .module, compatibleWith: nil) ?? UIImage() + } + + /// Upload icon + static var affineUpload: UIImage { + UIImage(named: "Upload", in: .module, compatibleWith: nil) ?? UIImage() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json new file mode 100644 index 000000000..5cf53db92 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.button.primary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0x96", + "red" : "0x1E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0x96", + "red" : "0x1E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json new file mode 100644 index 000000000..5cf53db92 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.activated.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0x96", + "red" : "0x1E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0x96", + "red" : "0x1E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json new file mode 100644 index 000000000..097a07d04 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.icon.primary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x7A", + "red" : "0x7A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF3", + "green" : "0xF3", + "red" : "0xF3" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json new file mode 100644 index 000000000..ee2f44f71 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.primary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x14", + "green" : "0x14", + "red" : "0x14" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json new file mode 100644 index 000000000..d0a7f7fbb --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.background.secondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0xF5", + "red" : "0xF5" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x25", + "green" : "0x25", + "red" : "0x25" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json new file mode 100644 index 000000000..5867cb4b8 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.border.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE6", + "green" : "0xE6", + "red" : "0xE6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x41", + "green" : "0x41", + "red" : "0x41" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json new file mode 100644 index 000000000..b8b76883a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.layer.pureWhite.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json new file mode 100644 index 000000000..6fd68daf9 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.emphasis.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0x96", + "red" : "0x1E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0x96", + "red" : "0x1E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json new file mode 100644 index 000000000..38c33058a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.link.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x9F", + "green" : "0x5F", + "red" : "0x03" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x9F", + "green" : "0x5F", + "red" : "0x03" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json new file mode 100644 index 000000000..e651740bf --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.listDotAndNumber.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCB", + "green" : "0x77", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCB", + "green" : "0x77", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json new file mode 100644 index 000000000..3e6e1103e --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.placeholder.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x92", + "green" : "0x92", + "red" : "0x92" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x7A", + "red" : "0x7A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json new file mode 100644 index 000000000..8fd8401b2 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.primary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x14", + "green" : "0x14", + "red" : "0x14" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE6", + "green" : "0xE6", + "red" : "0xE6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json new file mode 100644 index 000000000..70a39895a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.pureWhite.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json new file mode 100644 index 000000000..df415be47 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.secondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x7A", + "red" : "0x7A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x92", + "green" : "0x92", + "red" : "0x92" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json new file mode 100644 index 000000000..3e6e1103e --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Colors.xcassets/affine.text.tertiary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x92", + "green" : "0x92", + "red" : "0x92" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x7A", + "red" : "0x7A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json new file mode 100644 index 000000000..65640bae4 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "More Options Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf new file mode 100644 index 000000000..06088d679 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowDown.imageset/More Options Icon.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf new file mode 100644 index 000000000..f47faca59 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/ArrowUpBig.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json new file mode 100644 index 000000000..be31150b1 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/ArrowUpBig.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ArrowUpBig.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Contents.json new file mode 100644 index 000000000..71c6462f7 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Left icon-5.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf new file mode 100644 index 000000000..460c5880e Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Box.imageset/Left icon-5.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Broom.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Broom.pdf new file mode 100644 index 000000000..cb873ca18 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Broom.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Contents.json new file mode 100644 index 000000000..eb6e250f2 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Broom.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Broom.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf new file mode 100644 index 000000000..63a63af10 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Bubble.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Contents.json new file mode 100644 index 000000000..36cb25248 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Bubble.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Bubble.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Contents.json new file mode 100644 index 000000000..3ace4add8 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Left icon-1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf new file mode 100644 index 000000000..087a564ce Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Calendar.imageset/Left icon-1.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Camera.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Camera.pdf new file mode 100644 index 000000000..078067349 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Camera.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Contents.json new file mode 100644 index 000000000..24372dd80 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Camera.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Camera.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json new file mode 100644 index 000000000..ed53e594e --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Left icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf new file mode 100644 index 000000000..9b120c3c6 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/CheckCircle.imageset/Left icon.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Close.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Close.pdf new file mode 100644 index 000000000..1cebcc33b Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Close.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Contents.json new file mode 100644 index 000000000..2e21e0ab3 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Close.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Close.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Contents.json new file mode 100644 index 000000000..ff4269155 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Image.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Image.pdf new file mode 100644 index 000000000..85ee0a910 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Image.imageset/Image.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/Contents.json new file mode 100644 index 000000000..c4324cb95 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "More Options Icon-1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf new file mode 100644 index 000000000..3fc56638f Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/More.imageset/More Options Icon-1.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Contents.json new file mode 100644 index 000000000..6f5c8f9cd --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Page.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Page.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Page.pdf new file mode 100644 index 000000000..cb33cb731 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Page.imageset/Page.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Contents.json new file mode 100644 index 000000000..406918a05 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Plus.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Plus.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Plus.pdf new file mode 100644 index 000000000..09bafbb4a Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Plus.imageset/Plus.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Contents.json new file mode 100644 index 000000000..769d51ae0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Settings.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Settings.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Settings.pdf new file mode 100644 index 000000000..521f4a142 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Settings.imageset/Settings.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/Contents.json new file mode 100644 index 000000000..2e3822aa8 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/icon-2.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/icon-2.pdf new file mode 100644 index 000000000..089429e05 Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Think.imageset/icon-2.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/Contents.json new file mode 100644 index 000000000..dd9e55cfc --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/icon.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/icon.pdf new file mode 100644 index 000000000..107a7743c Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Tools.imageset/icon.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Contents.json new file mode 100644 index 000000000..859f0a5b1 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Upload.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Upload.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Upload.pdf new file mode 100644 index 000000000..b0b940d7b Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Upload.imageset/Upload.pdf differ diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/Contents.json b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/Contents.json new file mode 100644 index 000000000..f2124a908 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/icon-1.pdf b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/icon-1.pdf new file mode 100644 index 000000000..fa4b9ab6a Binary files /dev/null and b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/Icons.xcassets/Web.imageset/icon-1.pdf differ 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 88701da42..000000000 --- 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/main.metal b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/main.metal new file mode 100644 index 000000000..d79a65f82 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Resources/main.metal @@ -0,0 +1,87 @@ +#include + +using namespace metal; + +namespace ParticleTransitionSystem { + + struct TrollParticle { + float2 position; + float2 velocity; + float life; + float duration; + }; + + struct TrollVertex { + float4 position [[position]]; + float2 uv; + float opacity; + }; + +} + +// 顶点着色器 负责将粒子数据转换为顶点数据 +vertex ParticleTransitionSystem::TrollVertex PTS_ParticleVertex(const device ParticleTransitionSystem::TrollVertex *vertices [[buffer(0)]], + const device float2 &resolution [[buffer(1)]], + const device ParticleTransitionSystem::TrollParticle *particles [[buffer(2)]], + const device float2 &targetFrameSize [[buffer(3)]], + const device float &stepSize [[buffer(4)]], + unsigned int vid [[vertex_id]], + unsigned int particleId [[instance_id]]) { + ParticleTransitionSystem::TrollVertex v = vertices[vid]; + ParticleTransitionSystem::TrollParticle p = particles[particleId]; + + int particlesPerRow = int(targetFrameSize.x / stepSize); + int row = particleId / particlesPerRow; + int col = particleId % particlesPerRow; + + float2 originalPos = float2(col * stepSize + stepSize / 2.0, row * stepSize + stepSize / 2.0); + float2 currentPos = p.position; + + // 计算目标帧在屏幕中的居中偏移 + float2 offset = (resolution - targetFrameSize) / 2.0; + + // 设置UV坐标用于纹理采样 + v.uv.x = originalPos.x / targetFrameSize.x; + v.uv.y = originalPos.y / targetFrameSize.y; + + // 计算最终的屏幕位置 + float particleSize = stepSize; + float2 worldPos = v.position.xy * particleSize + currentPos + offset; + + // 转换到NDC坐标系 (-1到1) + v.position.x = (worldPos.x / resolution.x) * 2.0 - 1.0; + v.position.y = 1.0 - (worldPos.y / resolution.y) * 2.0; + + // 逐渐消失 + v.opacity = p.life / p.duration; + + return v; +} + +// 片段着色器 负责将顶点数据转换为像素颜色 +fragment float4 PTS_ParticleFragment(ParticleTransitionSystem::TrollVertex in [[stage_in]], + const texture2d texture [[texture(0)]], + const sampler textureSampler [[sampler(0)]]) { + constexpr sampler samplr; + float4 color = texture.sample(samplr, in.uv); + float a = color.a * in.opacity; // apply texture alpha + color *= in.opacity; // apply opacity from vertex shader aka pre-multiplied alpha + color.a = a; + return color; +} + +// 计算粒子位置和速度的计算着色器 负责更新粒子的位置和速度 +kernel void PTS_UpdateParticles(device ParticleTransitionSystem::TrollParticle *particles [[buffer(0)]], + unsigned int index [[thread_position_in_grid]]) { + if (particles[index].life >= 0) { + particles[index].position += particles[index].velocity; + + // 模拟空气阻力,降低速度 x, y 分量 + particles[index].velocity.x *= 0.99; + particles[index].velocity.y *= 0.99; + + // 模拟重力影响,增加 y 分量 + particles[index].velocity.y += 0.1; + } + particles[index].life -= 1.0; +} 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 d89c4a6ce..000000000 --- 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 0023a5340..000000000 --- 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 de651b9d8..000000000 --- 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 65782460a..000000000 --- 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 919434a62..000000000 --- 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 684b9e8df..000000000 --- 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 c972267ba..000000000 --- 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 bb620e0c3..000000000 --- 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 03990f4a4..000000000 --- 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 1115c3c85..000000000 --- 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 07d6fa484..000000000 --- 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 6b7589798..000000000 --- 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 7352ae564..000000000 --- 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 89cb30a86..000000000 --- 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 fe5eedde2..000000000 --- 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 adf584d21..000000000 --- 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 08de046f8..000000000 --- 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 56cdb687d..000000000 --- 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 ff19cb197..000000000 --- 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 4ee0be808..000000000 --- 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 c65dd58c3..000000000 --- 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 371755465..000000000 --- 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 b3cbcf1c1..000000000 --- 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 9d7f1a797..000000000 --- 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 4c4e2cec4..000000000 --- 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 ac057c6b7..000000000 --- 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 4a6dabc22..000000000 --- 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 d94a0479c..000000000 --- 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 8c4074802..000000000 --- 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 945e304b9..000000000 --- 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 1720a51b7..000000000 --- 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 2b4951b94..000000000 --- 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 577d517b8..000000000 --- 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 350a0dc87..000000000 --- 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 e33c976fc..000000000 --- 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 0c7d70864..000000000 --- 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 120475eb5..000000000 --- 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 164831d90..000000000 --- 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 f8892a85f..000000000 --- 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 7767ad2af..000000000 --- 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 92fe2432b..000000000 --- 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 b7701bb36..000000000 --- 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 26f77bc7b..000000000 --- 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 8dc4bf622..000000000 --- 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 574acc045..000000000 --- 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 f1df4a5b0..000000000 --- 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 cf252773a..000000000 --- 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 1eddf7898..000000000 --- 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 451d56e58..000000000 --- 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. -} diff --git a/packages/frontend/apps/ios/codegen.ts b/packages/frontend/apps/ios/codegen.ts index 6cadbb169..c470578ae 100644 --- a/packages/frontend/apps/ios/codegen.ts +++ b/packages/frontend/apps/ios/codegen.ts @@ -11,7 +11,7 @@ console.log('[*] PackageRoot', PackageRoot); console.log('[*] graphql...'); execSync( - `${PackageRoot}/App/Packages/AffineGraphQL/apollo-ios-cli generate --path ${PackageRoot}/apollo-codegen-config.json`, + `${PackageRoot}/App/Packages/AffineGraphQL/apollo-ios-cli generate --path ${PackageRoot}/apollo-codegen-config.json --ignore-version-mismatch`, { stdio: 'inherit' } );