From 60275046b4801e44528c80a0533549f0f9dfe8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=8D=E7=A0=8D?= Date: Mon, 18 Nov 2024 22:06:39 +0800 Subject: [PATCH] =?UTF-8?q?WIP:=20=E6=89=93=E4=B8=AA=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E5=85=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ios/App/App.xcodeproj/project.pbxproj | 43 +++++-- .../ios/App/App/AFFiNEViewController.swift | 15 --- .../ios/App/App/AffineViewController.swift | 28 +++++ .../ios/App/App/Base.lproj/Main.storyboard | 8 +- packages/frontend/apps/ios/App/App/Info.plist | 2 + .../App/App/Packages/Intelligents/.gitignore | 8 ++ .../App/Packages/Intelligents/Package.swift | 19 +++ .../Intelligents/Constant/Constant.swift | 13 ++ .../Intelligents/Extension/Ext+String.swift | 19 +++ .../Intelligents/Extension/Ext+UIView.swift | 37 ++++++ .../Extension/Ext+UIViewController.swift | 38 ++++++ .../Intelligents/Extension/Ext+print.swift | 14 +++ .../Sources/Intelligents/Intelligents.swift | 4 + .../IntelligentsButton+Control.swift | 82 +++++++++++++ .../IntelligentsButton.swift | 74 +++++++++++ .../Cell/ChatTableView+BaseCell.swift | 39 ++++++ .../ChatTableView/ChatTableView+Data.swift | 54 ++++++++ .../ChatTableView/ChatTableView.swift | 48 ++++++++ .../InputEditView/AttachmentBannerView.swift | 114 +++++++++++++++++ .../InputEditView+ViewModel.swift | 38 ++++++ .../InputEditView/InputEditView.swift | 115 ++++++++++++++++++ .../InputEditView/PlainTextEditView.swift | 37 ++++++ .../InputEditView/TextEditControlBanner.swift | 62 ++++++++++ .../IntelligentsChatController+Header.swift | 108 ++++++++++++++++ .../IntelligentsChatController+InputBox.swift | 62 ++++++++++ .../IntelligentsChatController.swift | 71 +++++++++++ .../Sources/Intelligents/Model/Chat.swift | 27 ++++ .../Resources/Media.xcassets/Contents.json | 6 + .../close.imageset/Contents.json | 12 ++ .../Media.xcassets/close.imageset/close.svg | 5 + .../spark.imageset/Contents.json | 15 +++ .../Media.xcassets/spark.imageset/spark.svg | 8 ++ .../Resources/en.lproj/Localizable.strings | 13 ++ .../zh-Hans.lproj/Localizable.strings | 13 ++ .../Cookie/CookieManager.swift | 0 .../Cookie/CookiePlugin.swift | 0 .../Cookie/HashcashPlugin.swift | 0 .../apps/ios/App/App/RootViewController.swift | 38 ++++++ .../ios/App/Packages/Intelligents/.gitignore | 8 ++ .../App/Packages/Intelligents/Package.swift | 14 +++ .../Sources/Intelligents/Intelligents.swift | 4 + .../Intelligents/IntelligentsButton.swift | 13 ++ .../IntelligentsChatController.swift | 14 +++ 43 files changed, 1312 insertions(+), 30 deletions(-) delete mode 100644 packages/frontend/apps/ios/App/App/AFFiNEViewController.swift create mode 100644 packages/frontend/apps/ios/App/App/AffineViewController.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/.gitignore create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Package.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Constant/Constant.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton+Control.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/Cell/ChatTableView+BaseCell.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView+Data.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/AttachmentBannerView.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/PlainTextEditView.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/TextEditControlBanner.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+Header.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+InputBox.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Model/Chat.swift create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/Contents.json create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/close.svg create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/Contents.json create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/spark.svg create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings create mode 100644 packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings rename packages/frontend/apps/ios/App/App/{plugins => Plugins}/Cookie/CookieManager.swift (100%) rename packages/frontend/apps/ios/App/App/{plugins => Plugins}/Cookie/CookiePlugin.swift (100%) rename packages/frontend/apps/ios/App/App/{plugins => Plugins}/Cookie/HashcashPlugin.swift (100%) create mode 100644 packages/frontend/apps/ios/App/App/RootViewController.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/.gitignore create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController.swift diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index c7453edb67..a10bf64a2d 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -7,10 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 505B0A342CEB3FB10092FC35 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 505B0A332CEB3FB10092FC35 /* Intelligents */; }; + 505B0A362CEB48B10092FC35 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505B0A352CEB48B10092FC35 /* RootViewController.swift */; }; 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; }; 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; }; 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; }; - 9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; }; + 9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */; }; 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */; }; 9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1D2CCB9876006677DB /* Assets.xcassets */; }; 9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1E2CCB9876006677DB /* capacitor.config.json */; }; @@ -22,10 +24,12 @@ /* Begin PBXFileReference section */ 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 505B0A312CEB3FAB0092FC35 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = ""; }; + 505B0A352CEB48B10092FC35 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = ""; }; 9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = ""; }; 9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = ""; }; - 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = ""; }; + 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineViewController.swift; sourceTree = ""; }; 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9D90BE1D2CCB9876006677DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9D90BE1E2CCB9876006677DB /* capacitor.config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; @@ -43,6 +47,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 505B0A342CEB3FB10092FC35 /* Intelligents in Frameworks */, C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -65,9 +70,7 @@ 504EC3051FED79650016851F /* Products */, 7F8756D8B27F46E3366F6CEA /* Pods */, 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, - 9D6A85312CCF6D6B00DAB35F /* Recovered References */, ); - indentWidth = 2; sourceTree = ""; tabWidth = 2; }; @@ -79,6 +82,14 @@ name = Products; sourceTree = ""; }; + 505B0A322CEB3FAB0092FC35 /* Packages */ = { + isa = PBXGroup; + children = ( + 505B0A312CEB3FAB0092FC35 /* Intelligents */, + ); + path = Packages; + sourceTree = ""; + }; 7F8756D8B27F46E3366F6CEA /* Pods */ = { isa = PBXGroup; children = ( @@ -98,19 +109,21 @@ path = Cookie; sourceTree = ""; }; - 9D90BE1A2CCB9876006677DB /* plugins */ = { + 9D90BE1A2CCB9876006677DB /* Plugins */ = { isa = PBXGroup; children = ( 9D90BE192CCB9876006677DB /* Cookie */, ); - path = plugins; + path = Plugins; sourceTree = ""; }; 9D90BE242CCB9876006677DB /* App */ = { isa = PBXGroup; children = ( - 9D90BE1A2CCB9876006677DB /* plugins */, - 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */, + 505B0A322CEB3FAB0092FC35 /* Packages */, + 9D90BE1A2CCB9876006677DB /* Plugins */, + 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */, + 505B0A352CEB48B10092FC35 /* RootViewController.swift */, 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */, 9D90BE1D2CCB9876006677DB /* Assets.xcassets */, 9D90BE1E2CCB9876006677DB /* capacitor.config.json */, @@ -236,8 +249,9 @@ files = ( 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */, 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */, + 505B0A362CEB48B10092FC35 /* RootViewController.swift in Sources */, 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */, - 9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */, + 9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */, 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -384,7 +398,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -414,7 +428,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -454,6 +468,13 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 505B0A332CEB3FB10092FC35 /* Intelligents */ = { + isa = XCSwiftPackageProductDependency; + productName = Intelligents; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 504EC2FC1FED79650016851F /* Project object */; } diff --git a/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift b/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift deleted file mode 100644 index bddedfec4b..0000000000 --- a/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit -import Capacitor - -class AFFiNEViewController: CAPBridgeViewController { - - override func viewDidLoad() { - super.viewDidLoad() - webView?.allowsBackForwardNavigationGestures = true - } - - override func capacitorDidLoad() { - bridge?.registerPluginInstance(CookiePlugin()) - bridge?.registerPluginInstance(HashcashPlugin()) - } -} diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift new file mode 100644 index 0000000000..861a6a7ee8 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -0,0 +1,28 @@ +import UIKit +import Capacitor +import Intelligents + +class AFFiNEViewController: CAPBridgeViewController { + override func viewDidLoad() { + super.viewDidLoad() + webView?.allowsBackForwardNavigationGestures = true + self.navigationController?.navigationBar.isHidden = true + installIntelligentsButton() + } + + override func capacitorDidLoad() { + bridge?.registerPluginInstance(CookiePlugin()) + bridge?.registerPluginInstance(HashcashPlugin()) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: animated) + self.presentIntelligentsButton() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + dismissIntelligentsButton() + } +} diff --git a/packages/frontend/apps/ios/App/App/Base.lproj/Main.storyboard b/packages/frontend/apps/ios/App/App/Base.lproj/Main.storyboard index c12077feea..999015b502 100644 --- a/packages/frontend/apps/ios/App/App/Base.lproj/Main.storyboard +++ b/packages/frontend/apps/ios/App/App/Base.lproj/Main.storyboard @@ -1,15 +1,15 @@ - + - + - + - + diff --git a/packages/frontend/apps/ios/App/App/Info.plist b/packages/frontend/apps/ios/App/App/Info.plist index 10b7d08c8b..b59812ac91 100644 --- a/packages/frontend/apps/ios/App/App/Info.plist +++ b/packages/frontend/apps/ios/App/App/Info.plist @@ -2,6 +2,8 @@ + CFBundleAllowMixedLocalizations + CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/.gitignore b/packages/frontend/apps/ios/App/App/Packages/Intelligents/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/.gitignore @@ -0,0 +1,8 @@ +.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/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Package.swift new file mode 100644 index 0000000000..2ee76032f0 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Package.swift @@ -0,0 +1,19 @@ +// 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: "Intelligents", + defaultLocalization: "en", + platforms: [ + .iOS(.v14), + .macCatalyst(.v14), + ], + products: [ + .library(name: "Intelligents", targets: ["Intelligents"]), + ], + targets: [ + .target(name: "Intelligents"), + ] +) diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Constant/Constant.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Constant/Constant.swift new file mode 100644 index 0000000000..a981a7929b --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Constant/Constant.swift @@ -0,0 +1,13 @@ +// +// 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) +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift new file mode 100644 index 0000000000..7feddb601e --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+String.swift @@ -0,0 +1,19 @@ +// +// 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/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift new file mode 100644 index 0000000000..361e88e8a7 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift @@ -0,0 +1,37 @@ +// +// 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 + } + +#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/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift new file mode 100644 index 0000000000..1c996b0a34 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIViewController.swift @@ -0,0 +1,38 @@ +// +// Ext+UIViewController.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +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) + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift new file mode 100644 index 0000000000..1959304e7e --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+print.swift @@ -0,0 +1,14 @@ +// +// 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/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift new file mode 100644 index 0000000000..35011f5825 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift @@ -0,0 +1,4 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +enum Intelligents {} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton+Control.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton+Control.swift new file mode 100644 index 0000000000..04271f96bb --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton+Control.swift @@ -0,0 +1,82 @@ +// +// IntelligentsButton+Control.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +public extension UIViewController { + @discardableResult + func installIntelligentsButton() -> IntelligentsButton { + print("[*] \(#function)") + if let button = findIntelligentsButton() { return button } + + 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.transform = .init(scaleX: 0, y: 0) + view.layoutIfNeeded() + return button + } + + private func findIntelligentsButton() -> IntelligentsButton? { + for subview in view.subviews { // for for depth 1 + if let button = subview as? IntelligentsButton { + return button + } + } + return nil + } + + func presentIntelligentsButton() { + guard let button = findIntelligentsButton() else { return } + print("[*] \(button) is calling \(#function)") + + button.alpha = 0 + button.isHidden = false + button.setNeedsLayout() + view.layoutIfNeeded() + + UIView.animate( + withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 1.0, + initialSpringVelocity: 0.8 + ) { + button.alpha = 1 + button.transform = .identity + button.setNeedsLayout() + self.view.layoutIfNeeded() + } + } + + func dismissIntelligentsButton() { + guard let button = findIntelligentsButton() else { return } + print("[*] \(button) is calling \(#function)") + + button.setNeedsLayout() + view.layoutIfNeeded() + UIView.animate( + withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 1.0, + initialSpringVelocity: 0.8 + ) { + button.alpha = 0 + button.transform = .init(scaleX: 0, y: 0) + button.setNeedsLayout() + self.view.layoutIfNeeded() + } completion: { _ in + button.isHidden = true + } + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton.swift new file mode 100644 index 0000000000..1dfe9ce25e --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton/IntelligentsButton.swift @@ -0,0 +1,74 @@ +// +// IntelligentsButton.swift +// +// +// Created by 秋星桥 on 2024/11/18. +// + +import ColorfulX +import UIKit + +// floating button to open intelligent panel +public class IntelligentsButton: UIView { + let image = UIImageView() + let background = 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 = Constant.affineTintColor + 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 } + +// layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor +// layer.shadowOffset = CGSize(width: 0, height: 0) +// layer.shadowRadius = 8 + + 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 + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + override public func layoutSubviews() { + super.layoutSubviews() + + layer.cornerRadius = bounds.width / 2 + } + + @objc func tapped() { + guard let controller = parentViewController else { + assertionFailure() + return + } + let targetController = IntelligentsChatController() + controller.presentIntoCurrentContext(withTargetController: targetController) + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/Cell/ChatTableView+BaseCell.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/Cell/ChatTableView+BaseCell.swift new file mode 100644 index 0000000000..d44770c21e --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/Cell/ChatTableView+BaseCell.swift @@ -0,0 +1,39 @@ +// +// ChatTableView+BaseCell.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +extension ChatTableView { + class BaseCell: UITableViewCell { + var inset: UIEdgeInsets = .zero { + didSet { setNeedsLayout() } + } + + let containerView = UIView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + contentView.addSubview(containerView) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + override func layoutSubviews() { + super.layoutSubviews() + containerView.frame = contentView.bounds.inset(by: inset) + } + + func update(via _: AnyObject?) { + assertionFailure() // "should be override" + } + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView+Data.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView+Data.swift new file mode 100644 index 0000000000..3a2903d7fb --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView+Data.swift @@ -0,0 +1,54 @@ +// +// ChatTableView+Data.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +extension ChatTableView { + struct DataElement { + enum CellType: String, CaseIterable { + case base + } + + let type: CellType + let object: AnyObject? + + init(type: CellType, object: AnyObject?) { + self.type = type + self.object = object + } + } +} + +extension ChatTableView.DataElement.CellType { + var cellClassType: ChatTableView.BaseCell.Type { + switch self { + case .base: + ChatTableView.BaseCell.self + } + } + + var cellIdentifier: String { + NSStringFromClass(cellClassType) + } +} + +extension ChatTableView: UITableViewDelegate, UITableViewDataSource { + func numberOfSections(in _: UITableView) -> Int { + 1 + } + + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + dataSource.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: dataSource[indexPath.row].type.cellIdentifier, for: indexPath) as! BaseCell + let object = dataSource[indexPath.row].object + cell.update(via: object) + return cell + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView.swift new file mode 100644 index 0000000000..e4b49be12b --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/ChatTableView/ChatTableView.swift @@ -0,0 +1,48 @@ +// +// ChatTableView.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +class ChatTableView: UIView { + let tableView = UITableView() + + var dataSource: [DataElement] = [] + + init() { + super.init(frame: .zero) + + for eachCase in DataElement.CellType.allCases { + let cellClass = eachCase.cellClassType + tableView.register(cellClass, forCellReuseIdentifier: eachCase.cellIdentifier) + } + + tableView.backgroundColor = .clear + + tableView.delegate = self + tableView.dataSource = self + addSubview(tableView) + + tableView.translatesAutoresizingMaskIntoConstraints = false + [ + tableView.topAnchor.constraint(equalTo: topAnchor), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: bottomAnchor), + ].forEach { $0.isActive = true } + + let foot = UIView() + foot.translatesAutoresizingMaskIntoConstraints = false + foot.heightAnchor.constraint(equalToConstant: 200).isActive = true + foot.widthAnchor.constraint(equalToConstant: 200).isActive = true + tableView.tableFooterView = foot + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/AttachmentBannerView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/AttachmentBannerView.swift new file mode 100644 index 0000000000..399a829d7e --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/AttachmentBannerView.swift @@ -0,0 +1,114 @@ +// +// 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 attachments: [UIImage] = [] { + didSet { + rebuildViews() + } + } + + override var intrinsicContentSize: CGSize { + if attachments.isEmpty { return .zero } + return .init( + width: (attachmentSize + attachmentSize) * CGFloat(attachments.count) + - attachmentSpacing, + height: attachmentSize + ) + } + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + + rebuildViews() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + func rebuildViews() { + subviews.forEach { $0.removeFromSuperview() } + for (index, attachment) in attachments.enumerated() { + let view = AttachmentPreviewView() + view.imageView.image = attachment + addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.frame = .init( + origin: .init( + x: (attachmentSize + attachmentSpacing) * CGFloat(index), + y: 0 + ), + size: .init(width: attachmentSize, height: attachmentSize) + ) + } + invalidateIntrinsicContentSize() + contentSize = intrinsicContentSize + } +} + +extension AttachmentBannerView { + class AttachmentPreviewView: UIView { + let imageView = UIImageView() + let deleteButton = UIButton() + + 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 } + + [ + widthAnchor.constraint(equalToConstant: attachmentSize), + heightAnchor.constraint(equalToConstant: attachmentSize), + ].forEach { $0.isActive = true } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift new file mode 100644 index 0000000000..3b57215804 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView+ViewModel.swift @@ -0,0 +1,38 @@ +// +// File.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit +import Combine + +extension InputEditView { + class ViewModel: ObservableObject { + var cancellables: Set = [] + + @Published var text: String = "" + @Published var attachments: [UIImage] = [] + + init() { + + } + + deinit { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + } +} + +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/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView.swift new file mode 100644 index 0000000000..6f4d05c213 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/InputEditView.swift @@ -0,0 +1,115 @@ +// +// InputEditView.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit +import Combine + +class InputEditView: UIView, UITextViewDelegate { + 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 + } + } + + 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.delegate = self + 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 } + } + + 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() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + func textViewDidChange(_ textView: UITextView) { + viewModel.text = textView.text + } + + func textViewDidBeginEditing(_ textView: UITextView) { + updatePlaceholderVisibility() + } + + func textViewDidEndEditing(_ textView: UITextView) { + updatePlaceholderVisibility() + } + + 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 + } + if attachmentsEditor.attachments != viewModel.attachments { + attachmentsEditor.attachments = viewModel.attachments + } + self.parentViewController?.view.layoutIfNeeded() + } + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/PlainTextEditView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/PlainTextEditView.swift new file mode 100644 index 0000000000..e6a47b5300 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/PlainTextEditView.swift @@ -0,0 +1,37 @@ +// +// TextEditView.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +class PlainTextEditView: UITextView, UITextViewDelegate { + init() { + super.init(frame: .zero, textContainer: nil) + + delegate = self + tintColor = Constant.affineTintColor + + 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() + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/TextEditControlBanner.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/TextEditControlBanner.swift new file mode 100644 index 0000000000..fdfe2f98c1 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/InputEditView/TextEditControlBanner.swift @@ -0,0 +1,62 @@ +// +// 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/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+Header.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+Header.swift new file mode 100644 index 0000000000..2d57f076a3 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+Header.swift @@ -0,0 +1,108 @@ +// +// 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() + } + + @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 = Constant.affineTintColor + 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 = Constant.affineTintColor + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+InputBox.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+InputBox.swift new file mode 100644 index 0000000000..879fd69140 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController+InputBox.swift @@ -0,0 +1,62 @@ +// +// 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 = .systemBackground + 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/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController.swift new file mode 100644 index 0000000000..5751273ad6 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController/IntelligentsChatController.swift @@ -0,0 +1,71 @@ +// +// IntelligentsChatController.swift +// +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +class IntelligentsChatController: UIViewController { + let header = Header() + let inputBox = InputBox() + let tableView = ChatTableView() + + override var title: String? { + set { + super.title = newValue + header.titleLabel.text = newValue + } + get { + super.title + } + } + + init() { + super.init(nibName: nil, bundle: nil) + title = "Chat with AI".localized() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + override func viewDidLoad() { + super.viewDidLoad() + assert(navigationController != nil) + view.backgroundColor = .secondarySystemBackground + + hideKeyboardWhenTappedAround() + setupLayout() + } + + func setupLayout() { + 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(inputBox) + inputBox.translatesAutoresizingMaskIntoConstraints = false + [ + inputBox.leadingAnchor.constraint(equalTo: view.leadingAnchor), + inputBox.trailingAnchor.constraint(equalTo: view.trailingAnchor), + inputBox.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ].forEach { $0.isActive = true } + + view.addSubview(tableView) + 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 } + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Model/Chat.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Model/Chat.swift new file mode 100644 index 0000000000..85fba639b6 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Model/Chat.swift @@ -0,0 +1,27 @@ +// +// 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/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/Contents.json b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/Contents.json b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/Contents.json new file mode 100644 index 0000000000..308f9c8fe4 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "close.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/close.svg b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/close.svg new file mode 100644 index 0000000000..2681b0ecf9 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/close.imageset/close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/Contents.json b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/Contents.json new file mode 100644 index 0000000000..4b83644bef --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "spark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/spark.svg b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/spark.svg new file mode 100644 index 0000000000..9be7fe670c --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/Media.xcassets/spark.imageset/spark.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000000..65b1b827a5 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings @@ -0,0 +1,13 @@ +/* + Localizable.strings + Intelligents + + Created by 秋星桥 on 2024/11/18. + +*/ + +/* No comment provided by engineer. */ +"Chat with AI" = "Chat with AI"; + +/* No comment provided by engineer. */ +"Summarize this article for me..." = "Summarize this article for me..."; diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..744e2d5db3 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,13 @@ +/* + Localizable.strings + Intelligents + + Created by 秋星桥 on 2024/11/18. + +*/ + +/* No comment provided by engineer. */ +"Chat with AI" = "与 AI 聊天"; + +/* No comment provided by engineer. */ +"Summarize this article for me..." = "请为我总结这份文档..."; diff --git a/packages/frontend/apps/ios/App/App/plugins/Cookie/CookieManager.swift b/packages/frontend/apps/ios/App/App/Plugins/Cookie/CookieManager.swift similarity index 100% rename from packages/frontend/apps/ios/App/App/plugins/Cookie/CookieManager.swift rename to packages/frontend/apps/ios/App/App/Plugins/Cookie/CookieManager.swift diff --git a/packages/frontend/apps/ios/App/App/plugins/Cookie/CookiePlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/Cookie/CookiePlugin.swift similarity index 100% rename from packages/frontend/apps/ios/App/App/plugins/Cookie/CookiePlugin.swift rename to packages/frontend/apps/ios/App/App/Plugins/Cookie/CookiePlugin.swift diff --git a/packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/Cookie/HashcashPlugin.swift similarity index 100% rename from packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift rename to packages/frontend/apps/ios/App/App/Plugins/Cookie/HashcashPlugin.swift diff --git a/packages/frontend/apps/ios/App/App/RootViewController.swift b/packages/frontend/apps/ios/App/App/RootViewController.swift new file mode 100644 index 0000000000..6d3200be5d --- /dev/null +++ b/packages/frontend/apps/ios/App/App/RootViewController.swift @@ -0,0 +1,38 @@ +// +// RootViewController.swift +// App +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +@objc +class RootViewController: UINavigationController { + override init(rootViewController: UIViewController) { + fatalError() // "you are not allowed to call this" + } + + override init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?) { + fatalError() // "you are not allowed to call this" + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commitInit() + } + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + fatalError() // "you are not allowed to call this" + } + + func commitInit() { + assert(viewControllers.isEmpty) + viewControllers = [AFFiNEViewController()] + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/.gitignore b/packages/frontend/apps/ios/App/Packages/Intelligents/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/.gitignore @@ -0,0 +1,8 @@ +.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/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift new file mode 100644 index 0000000000..2c1e1455eb --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -0,0 +1,14 @@ +// 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: "Intelligents", + products: [ + .library(name: "Intelligents", targets: ["Intelligents"]), + ], + targets: [ + .target(name: "Intelligents"), + ] +) 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 new file mode 100644 index 0000000000..2f32f1b8e0 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift @@ -0,0 +1,4 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +class Intelligents {} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton.swift new file mode 100644 index 0000000000..a050b9f2c3 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsButton.swift @@ -0,0 +1,13 @@ +// +// IntelligentsButton.swift +// +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +// floating button to open intelligent panel +class IntelligentsButton: UIView { + let image = UIImageView() +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController.swift new file mode 100644 index 0000000000..a73464a1d7 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentsChatController.swift @@ -0,0 +1,14 @@ +// +// IntelligentsChatController.swift +// +// +// Created by 秋星桥 on 2024/11/18. +// + +import UIKit + +class IntelligentsChatController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + } +}