mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-23 01:07:12 +08:00
feat(ios): add intelligents button (#9281)
Co-authored-by: 砍砍 <git@qaq.wiki>
This commit is contained in:
@@ -133,12 +133,9 @@ events?.applicationMenu.onNewPageAction(() => {
|
||||
.get(GlobalContextService)
|
||||
.globalContext.workspaceId.get();
|
||||
const workspacesService = frameworkProvider.get(WorkspacesService);
|
||||
const workspaceMetadata = currentWorkspaceId
|
||||
? workspacesService.list.workspace$(currentWorkspaceId).value
|
||||
const workspaceRef = currentWorkspaceId
|
||||
? workspacesService.openByWorkspaceId(currentWorkspaceId)
|
||||
: null;
|
||||
const workspaceRef =
|
||||
workspaceMetadata &&
|
||||
workspacesService.open({ metadata: workspaceMetadata });
|
||||
if (!workspaceRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507513692D1924C600AD60C0 /* RootViewController.swift */; };
|
||||
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5075136D2D1925BC00AD60C0 /* IntelligentsPlugin.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -27,10 +33,15 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
507513692D1924C600AD60C0 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = "<group>"; };
|
||||
5075136D2D1925BC00AD60C0 /* IntelligentsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntelligentsPlugin.swift; sourceTree = "<group>"; };
|
||||
50802D5E2D112F7D00694021 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = "<group>"; };
|
||||
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
|
||||
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
|
||||
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
|
||||
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = "<group>"; };
|
||||
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineViewController.swift; sourceTree = "<group>"; };
|
||||
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
9D90BE1D2CCB9876006677DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
9D90BE1E2CCB9876006677DB /* capacitor.config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
@@ -55,6 +66,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C4C97C6E2D0304D100BC2AD1 /* libaffine_mobile_native.a in Frameworks */,
|
||||
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */,
|
||||
50802D612D112F8700694021 /* Intelligents in Frameworks */,
|
||||
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -77,6 +90,7 @@
|
||||
children = (
|
||||
C4C97C722D0307B700BC2AD1 /* uniffi */,
|
||||
9D90BE242CCB9876006677DB /* App */,
|
||||
50802D5F2D112F7D00694021 /* Packages */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
@@ -93,6 +107,14 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
50802D5F2D112F7D00694021 /* Packages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50802D5E2D112F7D00694021 /* Intelligents */,
|
||||
);
|
||||
path = Packages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -105,32 +127,37 @@
|
||||
9D90BE192CCB9876006677DB /* Cookie */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9D1C07262CEC3E8200E1C502 /* IntelligentsPlugin.swift */,
|
||||
9D90BE172CCB9876006677DB /* CookieManager.swift */,
|
||||
9D90BE182CCB9876006677DB /* CookiePlugin.swift */,
|
||||
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */,
|
||||
5075136D2D1925BC00AD60C0 /* IntelligentsPlugin.swift */,
|
||||
);
|
||||
path = Cookie;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9D90BE1A2CCB9876006677DB /* plugins */ = {
|
||||
9D90BE1A2CCB9876006677DB /* Plugins */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E93B276A2CED9298001409B8 /* NavigationGesture */,
|
||||
9D90BE192CCB9876006677DB /* Cookie */,
|
||||
);
|
||||
path = plugins;
|
||||
path = Plugins;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9D90BE242CCB9876006677DB /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9D90BE1A2CCB9876006677DB /* plugins */,
|
||||
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */,
|
||||
9D90BE1A2CCB9876006677DB /* Plugins */,
|
||||
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
|
||||
507513692D1924C600AD60C0 /* RootViewController.swift */,
|
||||
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */,
|
||||
9D90BE1D2CCB9876006677DB /* Assets.xcassets */,
|
||||
9D90BE1E2CCB9876006677DB /* capacitor.config.json */,
|
||||
9D90BE1F2CCB9876006677DB /* config.xml */,
|
||||
9D90BE202CCB9876006677DB /* Info.plist */,
|
||||
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */,
|
||||
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */,
|
||||
9D90BE222CCB9876006677DB /* Main.storyboard */,
|
||||
9D90BE232CCB9876006677DB /* public */,
|
||||
);
|
||||
@@ -218,6 +245,8 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */,
|
||||
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */,
|
||||
9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */,
|
||||
9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */,
|
||||
9D90BE2B2CCB9876006677DB /* config.xml in Resources */,
|
||||
@@ -287,6 +316,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */,
|
||||
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
|
||||
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
|
||||
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
|
||||
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,
|
||||
@@ -294,7 +325,7 @@
|
||||
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
|
||||
9D90BE262CCB9876006677DB /* CookiePlugin.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;
|
||||
@@ -441,7 +472,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.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -476,7 +507,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.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -521,6 +552,17 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
50802D602D112F8700694021 /* Intelligents */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Intelligents;
|
||||
};
|
||||
50A285DB2D112B24000D5A6D /* Intelligents */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Intelligents;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "msdisplaylink",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/MSDisplayLink",
|
||||
"state" : {
|
||||
"revision" : "c2fcd28cb99300d83acc30860ce252ef97c20b61",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "networkimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/NetworkImage",
|
||||
"state" : {
|
||||
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
|
||||
"version" : "6.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "springinterpolation",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/SpringInterpolation",
|
||||
"state" : {
|
||||
"revision" : "f9d1ee3d2466bdb00fd0ade7f256ed20229c8413",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cmark",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-cmark",
|
||||
"state" : {
|
||||
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-ui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
|
||||
"state" : {
|
||||
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "msdisplaylink",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/MSDisplayLink",
|
||||
"state" : {
|
||||
"revision" : "c2fcd28cb99300d83acc30860ce252ef97c20b61",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "networkimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/NetworkImage",
|
||||
"state" : {
|
||||
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
|
||||
"version" : "6.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "springinterpolation",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/SpringInterpolation",
|
||||
"state" : {
|
||||
"revision" : "f9d1ee3d2466bdb00fd0ade7f256ed20229c8413",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cmark",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-cmark",
|
||||
"state" : {
|
||||
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-ui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
|
||||
"state" : {
|
||||
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
class AFFiNEViewController: CAPBridgeViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// disable by default, enable manually when there is a "back" button in page-header
|
||||
webView?.allowsBackForwardNavigationGestures = false
|
||||
}
|
||||
|
||||
override func capacitorDidLoad() {
|
||||
bridge?.registerPluginInstance(CookiePlugin())
|
||||
bridge?.registerPluginInstance(HashcashPlugin())
|
||||
bridge?.registerPluginInstance(NavigationGesturePlugin())
|
||||
}
|
||||
}
|
||||
113
packages/frontend/apps/ios/App/App/AffineViewController.swift
Normal file
113
packages/frontend/apps/ios/App/App/AffineViewController.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
import Capacitor
|
||||
import Intelligents
|
||||
import UIKit
|
||||
|
||||
class AFFiNEViewController: CAPBridgeViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// disable by default, enable manually when there is a "back" button in page-header
|
||||
webView?.allowsBackForwardNavigationGestures = false
|
||||
navigationController?.navigationBar.isHidden = true
|
||||
extendedLayoutIncludesOpaqueBars = false
|
||||
edgesForExtendedLayout = []
|
||||
let intelligentsButton = installIntelligentsButton()
|
||||
intelligentsButton.delegate = self
|
||||
dismissIntelligentsButton()
|
||||
}
|
||||
|
||||
override func capacitorDidLoad() {
|
||||
let plugins: [CAPPlugin] = [
|
||||
CookiePlugin(),
|
||||
HashcashPlugin(),
|
||||
NavigationGesturePlugin(),
|
||||
IntelligentsPlugin(representController: self),
|
||||
]
|
||||
plugins.forEach { bridge?.registerPluginInstance($0) }
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
}
|
||||
}
|
||||
|
||||
extension AFFiNEViewController: IntelligentsButtonDelegate, IntelligentsFocusApertureViewDelegate {
|
||||
func onIntelligentsButtonTapped(_ button: IntelligentsButton) {
|
||||
guard let webView else {
|
||||
assertionFailure() // ? wdym ?
|
||||
return
|
||||
}
|
||||
|
||||
button.beginProgress()
|
||||
|
||||
let script = "return await window.getCurrentDocContentInMarkdown();"
|
||||
webView.callAsyncJavaScript(
|
||||
script,
|
||||
arguments: [:],
|
||||
in: nil,
|
||||
in: .page
|
||||
) { result in
|
||||
button.stopProgress()
|
||||
webView.resignFirstResponder()
|
||||
|
||||
if case let .failure(error) = result {
|
||||
print("[?] \(self) script error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
if case let .success(content) = result,
|
||||
let res = content as? String
|
||||
{
|
||||
print("[*] \(self) received document with \(res.count) characters")
|
||||
DispatchQueue.main.async {
|
||||
self.openIntelligentsSheet(withContext: res)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.openSimpleChat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openIntelligentsSheet(withContext context: String) {
|
||||
guard let view = webView?.subviews.first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
assert(view is UIScrollView)
|
||||
_ = context
|
||||
let focus = IntelligentsFocusApertureView()
|
||||
focus.prepareAnimationWith(
|
||||
capturingTargetContentView: view,
|
||||
coveringRootViewController: self
|
||||
)
|
||||
focus.delegate = self
|
||||
focus.executeAnimationKickIn()
|
||||
dismissIntelligentsButton()
|
||||
}
|
||||
|
||||
func openSimpleChat() {
|
||||
let targetController = IntelligentsChatController()
|
||||
presentIntoCurrentContext(withTargetController: targetController)
|
||||
}
|
||||
|
||||
func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType) {
|
||||
switch actionType {
|
||||
case .translateTo:
|
||||
fatalError("not implemented")
|
||||
case .summary:
|
||||
fatalError("not implemented")
|
||||
case .chatWithAI:
|
||||
let controller = IntelligentsChatController()
|
||||
presentIntoCurrentContext(withTargetController: controller)
|
||||
case .dismiss:
|
||||
presentIntelligentsButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,47 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var window: UIWindow?
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
func applicationWillResignActive(_: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
func applicationDidEnterBackground(_: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
func applicationWillEnterForeground(_: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
func applicationDidBecomeActive(_: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
func applicationWillTerminate(_: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--FiNE View Controller-->
|
||||
<!--Root View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="AFFiNEViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
|
||||
<viewController id="BYZ-38-t0r" customClass="RootViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="140" y="-1"/>
|
||||
|
||||
30
packages/frontend/apps/ios/App/App/InfoPlist.xcstrings
Normal file
30
packages/frontend/apps/ios/App/App/InfoPlist.xcstrings
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"CFBundleDisplayName" : {
|
||||
"comment" : "Bundle display name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "AFFiNE"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CFBundleName" : {
|
||||
"comment" : "Bundle name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "App"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
7
packages/frontend/apps/ios/App/App/Localizable.xcstrings
Normal file
7
packages/frontend/apps/ios/App/App/Localizable.xcstrings
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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(.v15),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "Intelligents", targets: ["Intelligents"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
|
||||
.package(url: "https://github.com/Lakr233/SpringInterpolation", from: "1.1.0"),
|
||||
.package(url: "https://github.com/Lakr233/MSDisplayLink", from: "1.1.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Intelligents", dependencies: [
|
||||
"SpringInterpolation",
|
||||
"MSDisplayLink",
|
||||
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
|
||||
]),
|
||||
]
|
||||
)
|
||||
@@ -7,24 +7,24 @@ public class CookieManager: NSObject {
|
||||
let jar = HTTPCookieStorage.shared
|
||||
guard let url = getServerUrl(urlString) else { return [:] }
|
||||
if let cookies = jar.cookies(for: url) {
|
||||
for cookie in cookies {
|
||||
cookiesMap[cookie.name] = cookie.value
|
||||
}
|
||||
for cookie in cookies {
|
||||
cookiesMap[cookie.name] = cookie.value
|
||||
}
|
||||
}
|
||||
return cookiesMap
|
||||
}
|
||||
|
||||
|
||||
private func isUrlSanitized(_ urlString: String) -> Bool {
|
||||
return urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
|
||||
urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
|
||||
}
|
||||
|
||||
|
||||
public func getServerUrl(_ urlString: String) -> URL? {
|
||||
let validUrlString = (isUrlSanitized(urlString)) ? urlString : "http://\(urlString)"
|
||||
let validUrlString = isUrlSanitized(urlString) ? urlString : "http://\(urlString)"
|
||||
|
||||
guard let url = URL(string: validUrlString) else {
|
||||
return nil
|
||||
}
|
||||
guard let url = URL(string: validUrlString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return url
|
||||
return url
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import Foundation
|
||||
|
||||
@objc(CookiePlugin)
|
||||
public class CookiePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "CookiePlugin"
|
||||
public let jsName = "Cookie"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise)
|
||||
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
|
||||
let cookieManager = CookieManager()
|
||||
|
||||
@objc public func getCookies(_ call: CAPPluginCall) {
|
||||
guard let url = call.getString("url") else {
|
||||
return call.resolve([:])
|
||||
}
|
||||
|
||||
|
||||
call.resolve(cookieManager.getCookies(url))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Capacitor
|
||||
|
||||
@objc(HashcashPlugin)
|
||||
public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "HashcashPlugin"
|
||||
public let jsName = "Hashcash"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
@objc func hash(_ call: CAPPluginCall) {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
let challenge = call.getString("challenge") ?? ""
|
||||
let bits = call.getInt("bits") ?? 20
|
||||
call.resolve(["value": hashcashMint(resource: challenge, bits: UInt32(bits))])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import Foundation
|
||||
|
||||
@objc(NavigationGesturePlugin)
|
||||
public class NavigationGesturePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "NavigationGesturePlugin"
|
||||
public let jsName = "NavigationGesture"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "isEnabled", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "enable", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "disable", returnType: CAPPluginReturnPromise)
|
||||
CAPPluginMethod(name: "isEnabled", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "enable", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "disable", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
|
||||
@objc func isEnabled(_ call: CAPPluginCall) {
|
||||
let enabled = self.bridge?.webView?.allowsBackForwardNavigationGestures ?? true
|
||||
let enabled = bridge?.webView?.allowsBackForwardNavigationGestures ?? true
|
||||
call.resolve(["value": enabled])
|
||||
}
|
||||
|
||||
|
||||
@objc func enable(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.sync {
|
||||
self.bridge?.webView?.allowsBackForwardNavigationGestures = true
|
||||
call.resolve([:])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func disable(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.sync {
|
||||
self.bridge?.webView?.allowsBackForwardNavigationGestures = false
|
||||
38
packages/frontend/apps/ios/App/App/RootViewController.swift
Normal file
38
packages/frontend/apps/ios/App/App/RootViewController.swift
Normal file
@@ -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 _: String?, bundle _: Bundle?) {
|
||||
fatalError() // "you are not allowed to call this"
|
||||
}
|
||||
|
||||
func commitInit() {
|
||||
assert(viewControllers.isEmpty)
|
||||
viewControllers = [AFFiNEViewController()]
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import Capacitor
|
||||
|
||||
@objc(HashcashPlugin)
|
||||
public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "HashcashPlugin"
|
||||
public let jsName = "Hashcash"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
@objc func hash(_ call: CAPPluginCall) {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
let challenge = call.getString("challenge") ?? ""
|
||||
let bits = call.getInt("bits") ?? 20;
|
||||
call.resolve(["value": hashcashMint(resource: challenge, bits: UInt32(bits))])
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/frontend/apps/ios/App/Packages/Intelligents/.gitignore
vendored
Normal file
8
packages/frontend/apps/ios/App/Packages/Intelligents/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "networkimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/NetworkImage",
|
||||
"state" : {
|
||||
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
|
||||
"version" : "6.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cmark",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-cmark",
|
||||
"state" : {
|
||||
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-ui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
|
||||
"state" : {
|
||||
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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(.v15),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "Intelligents", targets: ["Intelligents"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
|
||||
.package(url: "https://github.com/Lakr233/SpringInterpolation", from: "1.1.0"),
|
||||
.package(url: "https://github.com/Lakr233/MSDisplayLink", from: "1.1.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Intelligents", dependencies: [
|
||||
"SpringInterpolation",
|
||||
"MSDisplayLink",
|
||||
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
|
||||
]),
|
||||
]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Ext+UIColor.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/12/13.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
static var accent: UIColor {
|
||||
guard let color = UIColor(named: "accent", in: .module, compatibleWith: nil) else {
|
||||
assertionFailure()
|
||||
return .systemBlue
|
||||
}
|
||||
return color
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// The Swift Programming Language
|
||||
// https://docs.swift.org/swift-book
|
||||
|
||||
enum Intelligents {}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// 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()
|
||||
button.stopProgress()
|
||||
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.stopProgress()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// IntelligentsButton+Delegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol IntelligentsButtonDelegate: AnyObject {
|
||||
func onIntelligentsButtonTapped(_ button: IntelligentsButton)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// IntelligentsButton.swift
|
||||
//
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// floating button to open intelligent panel
|
||||
public class IntelligentsButton: UIView {
|
||||
let image = UIImageView()
|
||||
let background = UIView()
|
||||
let activityIndicator = UIActivityIndicatorView()
|
||||
|
||||
public weak var delegate: (any IntelligentsButtonDelegate)? = nil {
|
||||
didSet { assert(Thread.isMainThread) }
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
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
|
||||
|
||||
stopProgress()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
deinit {
|
||||
delegate = nil
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layer.cornerRadius = bounds.width / 2
|
||||
}
|
||||
|
||||
@objc func tapped() {
|
||||
delegate?.onIntelligentsButtonTapped(self)
|
||||
}
|
||||
|
||||
public func beginProgress() {
|
||||
activityIndicator.startAnimating()
|
||||
activityIndicator.isHidden = false
|
||||
}
|
||||
|
||||
public func stopProgress() {
|
||||
activityIndicator.stopAnimating()
|
||||
activityIndicator.isHidden = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// ChatTableView+BaseCell.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private let initialInsetValue: CGFloat = 24
|
||||
|
||||
extension ChatTableView {
|
||||
class BaseCell: UITableViewCell {
|
||||
var inset: UIEdgeInsets { // available for overrides
|
||||
.init(
|
||||
top: initialInsetValue / 2,
|
||||
left: initialInsetValue,
|
||||
bottom: initialInsetValue / 2,
|
||||
right: initialInsetValue
|
||||
)
|
||||
}
|
||||
|
||||
let containerView = UIView()
|
||||
let roundedBackgroundView = UIView()
|
||||
|
||||
var viewModel: AnyObject? {
|
||||
didSet { update(via: viewModel) }
|
||||
}
|
||||
|
||||
enum BackgroundColorType {
|
||||
case clear
|
||||
case highlight
|
||||
case warning
|
||||
case lightGray
|
||||
}
|
||||
|
||||
var backgroundColorType: BackgroundColorType = .clear {
|
||||
didSet {
|
||||
roundedBackgroundView.backgroundColor = backgroundColorType.color
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
roundedBackgroundView.clipsToBounds = true
|
||||
roundedBackgroundView.layer.cornerRadius = 8
|
||||
roundedBackgroundView.layer.masksToBounds = true
|
||||
|
||||
contentView.addSubview(roundedBackgroundView)
|
||||
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
[ // inset half of the container view
|
||||
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left / 2),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right / 2),
|
||||
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top / 2),
|
||||
roundedBackgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom / 2),
|
||||
].forEach { $0.isActive = true }
|
||||
|
||||
contentView.addSubview(containerView)
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left),
|
||||
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right),
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top),
|
||||
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom),
|
||||
].forEach { $0.isActive = true }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
viewModel = nil
|
||||
}
|
||||
|
||||
func update(via object: AnyObject?) {
|
||||
_ = object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ChatTableView.BaseCell.BackgroundColorType {
|
||||
var color: UIColor {
|
||||
switch self {
|
||||
case .clear:
|
||||
.clear
|
||||
case .highlight:
|
||||
.accent
|
||||
case .warning:
|
||||
.systemRed.withAlphaComponent(0.1)
|
||||
case .lightGray:
|
||||
.systemGray.withAlphaComponent(0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// ChatTableView+ChatCell.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import MarkdownUI
|
||||
import UIKit
|
||||
|
||||
extension ChatTableView {
|
||||
class ChatCell: BaseCell {
|
||||
let avatarView = CircleImageView()
|
||||
let titleLabel = UILabel()
|
||||
let markdownContainer = UIView()
|
||||
|
||||
var markdownView: UIView?
|
||||
var removableConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
let spacingElement: CGFloat = 12
|
||||
let avatarSize: CGFloat = 24
|
||||
|
||||
containerView.addSubview(avatarView)
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
avatarView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
avatarView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
avatarView.widthAnchor.constraint(equalToConstant: avatarSize),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: avatarSize),
|
||||
].forEach { $0.isActive = true }
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .bold)
|
||||
containerView.addSubview(titleLabel)
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: spacingElement),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
].forEach { $0.isActive = true }
|
||||
|
||||
containerView.addSubview(markdownContainer)
|
||||
markdownContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: avatarView.bottomAnchor, constant: spacingElement),
|
||||
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: spacingElement),
|
||||
markdownContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0),
|
||||
markdownContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0),
|
||||
markdownContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
|
||||
].forEach { $0.isActive = true }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func update(via object: AnyObject?) {
|
||||
super.update(via: object)
|
||||
|
||||
guard let viewModel = object as? ViewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
switch viewModel.participant {
|
||||
case .system:
|
||||
avatarView.image = UIImage(systemName: "gearshape.fill")
|
||||
titleLabel.text = "System".localized()
|
||||
backgroundColorType = .warning
|
||||
case .assistant:
|
||||
avatarView.image = UIImage(named: "spark", in: .module, with: .none)
|
||||
titleLabel.text = "AFFiNE AI".localized()
|
||||
backgroundColorType = .lightGray
|
||||
case .user:
|
||||
avatarView.image = UIImage(systemName: "person.fill")
|
||||
titleLabel.text = "You".localized()
|
||||
backgroundColorType = .clear
|
||||
}
|
||||
|
||||
removableConstraints.forEach { $0.isActive = false }
|
||||
if let markdownView { markdownView.removeFromSuperview() }
|
||||
markdownContainer.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
let hostingView: UIView = UIHostingView(
|
||||
rootView: Markdown(.init(viewModel.markdownDocument))
|
||||
)
|
||||
defer { markdownView = hostingView }
|
||||
markdownContainer.addSubview(hostingView)
|
||||
|
||||
hostingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
hostingView.topAnchor.constraint(equalTo: markdownContainer.topAnchor),
|
||||
hostingView.leadingAnchor.constraint(equalTo: markdownContainer.leadingAnchor),
|
||||
hostingView.trailingAnchor.constraint(lessThanOrEqualTo: markdownContainer.trailingAnchor),
|
||||
hostingView.bottomAnchor.constraint(equalTo: markdownContainer.bottomAnchor),
|
||||
].forEach {
|
||||
$0.isActive = true
|
||||
removableConstraints.append($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ChatTableView.ChatCell {
|
||||
class ViewModel {
|
||||
let participant: Participant
|
||||
let markdownDocument: String
|
||||
|
||||
init(participant: Participant, markdownDocument: String) {
|
||||
self.participant = participant
|
||||
self.markdownDocument = markdownDocument
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ChatTableView.ChatCell.ViewModel {
|
||||
enum Participant {
|
||||
case user
|
||||
case assistant
|
||||
case system
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// ChatTableView+Data.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension ChatTableView {
|
||||
struct DataElement {
|
||||
enum CellType: String, CaseIterable {
|
||||
case base
|
||||
case chat
|
||||
}
|
||||
|
||||
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
|
||||
case .chat:
|
||||
ChatTableView.ChatCell.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// ChatTableView+Scroll.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/12/23.
|
||||
//
|
||||
|
||||
import MSDisplayLink
|
||||
import SpringInterpolation
|
||||
import UIKit
|
||||
|
||||
extension ChatTableView: UIScrollViewDelegate, DisplayLinkDelegate {
|
||||
func synchronization() {
|
||||
let now = Date()
|
||||
defer { scrollAnimationDeltaTimeHolder = now }
|
||||
var deltaTime = now.timeIntervalSince(scrollAnimationDeltaTimeHolder)
|
||||
if deltaTime > 0.5 { deltaTime = 0.5 }
|
||||
guard scrollToBottomEnabled else { return }
|
||||
DispatchQueue.main.async { self.tikVsync(deltaTime: deltaTime * 2) }
|
||||
}
|
||||
|
||||
var bottomLocationY: CGFloat {
|
||||
tableView.contentSize.height - tableView.bounds.height
|
||||
}
|
||||
|
||||
private func tikVsync(deltaTime: TimeInterval) {
|
||||
guard scrollToBottomEnabled else { return }
|
||||
guard scrollToBottomAllowed else { return }
|
||||
scrollAnimationContext.setTarget(bottomLocationY)
|
||||
scrollAnimationContext.update(withDeltaTime: deltaTime)
|
||||
tableView.contentOffset.y = scrollAnimationContext.value
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func resetAnimationContext(to offset: CGFloat) {
|
||||
scrollAnimationContext.context = .init(
|
||||
currentPos: offset,
|
||||
currentVel: 0,
|
||||
targetPos: offset
|
||||
)
|
||||
}
|
||||
|
||||
func presentScrollBottomIfNeeded() {
|
||||
let visible = !scrollToBottomEnabled && scrollToBottomAllowed
|
||||
UIView.animate(withDuration: 0.25) { [self] in
|
||||
scrollDownButton.alpha = visible ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
func animationEnabledToggleDidSet(oldValue: Bool) {
|
||||
assert(Thread.isMainThread)
|
||||
guard scrollToBottomEnabled != oldValue else { return }
|
||||
resetAnimationContext(to: tableView.contentOffset.y)
|
||||
}
|
||||
|
||||
func animationAllowedToggleDidSet(oldValue: Bool) {
|
||||
assert(Thread.isMainThread)
|
||||
guard scrollToBottomAllowed != oldValue else { return }
|
||||
resetAnimationContext(to: tableView.contentOffset.y)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
processScrollView(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate _: Bool) {
|
||||
processScrollView(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
processScrollView(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
processScrollView(scrollView)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func processScrollView(_ scrollView: UIScrollView) {
|
||||
guard let tableView = scrollView as? UITableView else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
processTableViewMovements(tableView)
|
||||
}
|
||||
|
||||
private func processTableViewMovements(_ tableView: UITableView) {
|
||||
defer { presentScrollBottomIfNeeded() }
|
||||
if tableView.isDragging {
|
||||
scrollToBottomEnabled = false
|
||||
scrollToBottomAllowed = false
|
||||
} else {
|
||||
// 如果没在减速 就开启 允许滚动
|
||||
scrollToBottomAllowed = !tableView.isDecelerating
|
||||
// 如果滚到底了 就开启 自动滚动
|
||||
let isBottom = tableView.contentOffset.y >= bottomLocationY
|
||||
if isBottom { scrollToBottomEnabled = true }
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToBottom() {
|
||||
scrollToBottomEnabled = true
|
||||
scrollToBottomAllowed = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// ChatTableView.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import MSDisplayLink
|
||||
import SpringInterpolation
|
||||
import UIKit
|
||||
|
||||
class ChatTableView: UIView {
|
||||
let tableView = UITableView()
|
||||
let footerView = UIView()
|
||||
let scrollDownButton = ScrollBottom()
|
||||
|
||||
var dataSource: [DataElement] = []
|
||||
|
||||
// 控制自动滚动
|
||||
var scrollToBottomEnabled = false {
|
||||
didSet { animationEnabledToggleDidSet(oldValue: oldValue) }
|
||||
}
|
||||
|
||||
// 要让系统自己的动画完成以后再帮用户去挪
|
||||
var scrollToBottomAllowed = false {
|
||||
didSet { animationAllowedToggleDidSet(oldValue: oldValue) }
|
||||
}
|
||||
|
||||
var scrollAnimationController: DisplayLink = .init()
|
||||
var scrollAnimationContext: SpringInterpolation = .init()
|
||||
var scrollAnimationDeltaTimeHolder: Date = .init()
|
||||
|
||||
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 }
|
||||
|
||||
footerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
footerView.heightAnchor.constraint(equalToConstant: 128).isActive = true
|
||||
footerView.widthAnchor.constraint(equalToConstant: 128).isActive = true
|
||||
tableView.tableFooterView = footerView
|
||||
tableView.separatorStyle = .none
|
||||
|
||||
addSubview(scrollDownButton)
|
||||
scrollDownButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
// right bottom inset 16 size 32x32
|
||||
scrollDownButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
||||
scrollDownButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32),
|
||||
scrollDownButton.widthAnchor.constraint(equalToConstant: 32),
|
||||
scrollDownButton.heightAnchor.constraint(equalToConstant: 32),
|
||||
].forEach { $0.isActive = true }
|
||||
scrollDownButton.alpha = 0
|
||||
scrollDownButton.onTap = { [weak self] in
|
||||
self?.scrollToBottom()
|
||||
}
|
||||
|
||||
scrollAnimationController.delegatingObject(self)
|
||||
putMockData()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
extension ChatTableView {
|
||||
func putMockData() {
|
||||
DispatchQueue.main.async {
|
||||
let json: [String: Any] = ["query": """
|
||||
{
|
||||
currentUser {
|
||||
email
|
||||
name
|
||||
}
|
||||
}
|
||||
""", "variables": [:]]
|
||||
|
||||
let jsonData = try? JSONSerialization.data(withJSONObject: json)
|
||||
|
||||
let url = URL(string: "https://affine.fail/graphql")!
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = [
|
||||
"content-type": "application/json",
|
||||
]
|
||||
request.httpBody = jsonData
|
||||
URLSession.shared.dataTask(with: request) { v1, _, _ in
|
||||
guard let data = v1 else { return }
|
||||
let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
|
||||
print(json)
|
||||
}.resume()
|
||||
|
||||
self.tableView.reloadData()
|
||||
self.scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// ScrollBottom.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/12/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class ScrollBottom: UIView {
|
||||
let imageView = UIImageView()
|
||||
let backgroundView: UIView = UIVisualEffectView(
|
||||
effect: UIBlurEffect(style: .systemUltraThinMaterialDark)
|
||||
)
|
||||
|
||||
var onTap: (() -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(backgroundView)
|
||||
addSubview(imageView)
|
||||
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.image = UIImage(systemName: "arrow.down")
|
||||
imageView.tintColor = .accent
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
let tapGesture = UITapGestureRecognizer(
|
||||
target: self,
|
||||
action: #selector(tapAction)
|
||||
)
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
clipsToBounds = true
|
||||
|
||||
layer.cornerRadius = (bounds.width + bounds.height) / 4
|
||||
layer.masksToBounds = true
|
||||
|
||||
backgroundView.frame = bounds
|
||||
let imageInset = (bounds.width + bounds.height) / 8
|
||||
imageView.frame = CGRect(
|
||||
x: imageInset,
|
||||
y: imageInset,
|
||||
width: bounds.width - 2 * imageInset,
|
||||
height: bounds.height - 2 * imageInset
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func tapAction() {
|
||||
onTap?()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// 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 NSError(domain: "", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to compress image data",
|
||||
])
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// InputEditView+ViewModel.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
extension InputEditView {
|
||||
class ViewModel: ObservableObject {
|
||||
var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// InputEditView.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
viewModel.text = textView.text
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_: UITextView) {
|
||||
updatePlaceholderVisibility()
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_: 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
|
||||
}
|
||||
attachmentsEditor.rebuildViews()
|
||||
parentViewController?.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// PlainTextEditView.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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// IntelligentsChatController+Keyboard.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/12/6.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension IntelligentsChatController {
|
||||
@objc func keyboardWillAppear(_ notification: Notification) {
|
||||
let info = notification.userInfo ?? [:]
|
||||
let keyboardHeight = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?
|
||||
.cgRectValue
|
||||
.height ?? 0
|
||||
inputBoxKeyboardAdapterHeightConstraint.constant = keyboardHeight
|
||||
view.setNeedsUpdateConstraints()
|
||||
animateWithKeyboard(userInfo: info)
|
||||
}
|
||||
|
||||
@objc func keyboardWillDisappear(_ notification: Notification) {
|
||||
let info = notification.userInfo ?? [:]
|
||||
inputBoxKeyboardAdapterHeightConstraint.constant = 0
|
||||
view.setNeedsUpdateConstraints()
|
||||
animateWithKeyboard(userInfo: info)
|
||||
}
|
||||
|
||||
private func animateWithKeyboard(userInfo info: [AnyHashable: Any]) {
|
||||
let keyboardAnimationDuration = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?
|
||||
.doubleValue ?? 0
|
||||
let keyboardAnimationCurve = (info[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?
|
||||
.uintValue ?? 0
|
||||
UIView.animate(
|
||||
withDuration: keyboardAnimationDuration,
|
||||
delay: 0,
|
||||
options: UIView.AnimationOptions(rawValue: keyboardAnimationCurve),
|
||||
animations: {
|
||||
self.view.layoutIfNeeded()
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// IntelligentsChatController.swift
|
||||
//
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public class IntelligentsChatController: UIViewController {
|
||||
let header = Header()
|
||||
let inputBoxKeyboardAdapter = UIView()
|
||||
let inputBox = InputBox()
|
||||
let progressView = UIActivityIndicatorView()
|
||||
let tableView = ChatTableView()
|
||||
|
||||
var inputBoxKeyboardAdapterHeightConstraint = NSLayoutConstraint()
|
||||
|
||||
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
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardWillDisappear),
|
||||
name: UIResponder.keyboardWillHideNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardWillAppear),
|
||||
name: UIResponder.keyboardWillShowNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override public 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(inputBoxKeyboardAdapter)
|
||||
inputBoxKeyboardAdapter.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
inputBoxKeyboardAdapter.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
inputBoxKeyboardAdapter.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
inputBoxKeyboardAdapter.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
].forEach { $0.isActive = true }
|
||||
inputBoxKeyboardAdapterHeightConstraint = inputBoxKeyboardAdapter.heightAnchor.constraint(equalToConstant: 0)
|
||||
inputBoxKeyboardAdapterHeightConstraint.isActive = true
|
||||
inputBoxKeyboardAdapter.backgroundColor = inputBox.backgroundView.backgroundColor
|
||||
|
||||
view.addSubview(inputBox)
|
||||
inputBox.translatesAutoresizingMaskIntoConstraints = false
|
||||
[
|
||||
inputBox.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
inputBox.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
inputBox.bottomAnchor.constraint(equalTo: inputBoxKeyboardAdapter.topAnchor),
|
||||
].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, constant: 16),
|
||||
].forEach { $0.isActive = true }
|
||||
|
||||
view.addSubview(progressView)
|
||||
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
|
||||
|
||||
view.bringSubviewToFront(inputBox)
|
||||
inputBox.editor.controlBanner.sendButton.addTarget(
|
||||
self,
|
||||
action: #selector(send),
|
||||
for: .touchUpInside
|
||||
)
|
||||
}
|
||||
|
||||
@objc func send() {
|
||||
assert(Thread.isMainThread)
|
||||
inputBox.isUserInteractionEnabled = false
|
||||
progressView.startAnimating()
|
||||
progressView.isHidden = false
|
||||
progressView.alpha = 0
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.inputBox.editor.alpha = 0
|
||||
self.progressView.alpha = 1
|
||||
} completion: { _ in
|
||||
let viewModel = self.inputBox.editor.viewModel.duplicate()
|
||||
self.inputBox.editor.viewModel.reset()
|
||||
DispatchQueue.global().async {
|
||||
self.sendSyncEx(viewModel: viewModel)
|
||||
DispatchQueue.main.async {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.inputBox.editor.alpha = 1
|
||||
self.progressView.alpha = 0
|
||||
} completion: { _ in
|
||||
self.inputBox.isUserInteractionEnabled = true
|
||||
self.progressView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendSyncEx(viewModel: InputEditView.ViewModel) {
|
||||
let text = viewModel.text
|
||||
let images = viewModel.attachments
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// IntelligentsFocusApertureView+ActionButton.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension IntelligentsFocusApertureView.ControlButtonsPanel {
|
||||
class DarkActionButton: UIView {
|
||||
var iconSystemName: String {
|
||||
set { iconView.image = UIImage(systemName: newValue) }
|
||||
get { fatalError() }
|
||||
}
|
||||
|
||||
var title: String {
|
||||
set { titleLabel.text = newValue }
|
||||
get { titleLabel.text ?? "" }
|
||||
}
|
||||
|
||||
let titleLabel = UILabel()
|
||||
let iconView = UIImageView()
|
||||
var action: (() -> Void)? = nil
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white.withAlphaComponent(0.25)
|
||||
layer.cornerRadius = 12
|
||||
|
||||
let layoutGuide = UILayoutGuide()
|
||||
addLayoutGuide(layoutGuide)
|
||||
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .semibold)
|
||||
titleLabel.textColor = .white
|
||||
addSubview(titleLabel)
|
||||
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.tintColor = .white
|
||||
addSubview(iconView)
|
||||
|
||||
[
|
||||
layoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
layoutGuide.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
|
||||
iconView.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
|
||||
iconView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
|
||||
iconView.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
|
||||
iconView.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
|
||||
|
||||
titleLabel.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
|
||||
titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor),
|
||||
|
||||
titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
|
||||
].forEach { $0.isActive = true }
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
addGestureRecognizer(UITapGestureRecognizer(
|
||||
target: self,
|
||||
action: #selector(onTapped)
|
||||
))
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
@objc func onTapped() {
|
||||
action?()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// IntelligentsFocusApertureView+Capture.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension IntelligentsFocusApertureView {
|
||||
func captureImageBuffer(_ targetContentView: UIView) {
|
||||
let imageSize = targetContentView.frame.size
|
||||
let renderer = UIGraphicsImageRenderer(size: imageSize)
|
||||
let image = renderer.image { _ in
|
||||
targetContentView.drawHierarchy(
|
||||
in: targetContentView.bounds,
|
||||
afterScreenUpdates: false
|
||||
)
|
||||
}
|
||||
capturedImage = image
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// 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(actionType: IntelligentsFocusApertureViewActionType)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// 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 = [
|
||||
snapshotView.leftAnchor.constraint(equalTo: targetView.leftAnchor),
|
||||
snapshotView.rightAnchor.constraint(equalTo: targetView.rightAnchor),
|
||||
snapshotView.topAnchor.constraint(equalTo: targetView.topAnchor),
|
||||
snapshotView.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 = [
|
||||
snapshotView.leftAnchor.constraint(equalTo: leftAnchor, constant: sharedInset),
|
||||
snapshotView.rightAnchor.constraint(equalTo: rightAnchor, constant: -sharedInset),
|
||||
snapshotView.topAnchor.constraint(equalTo: topAnchor),
|
||||
snapshotView.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)
|
||||
|
||||
snapshotView.layer.cornerRadius = 0
|
||||
case .complete:
|
||||
NSLayoutConstraint.deactivate(contentBeginConstraints)
|
||||
NSLayoutConstraint.activate(contentFinalConstraints)
|
||||
|
||||
snapshotView.layer.cornerRadius = 32
|
||||
}
|
||||
let effectiveView = superview ?? self
|
||||
effectiveView.setNeedsUpdateConstraints()
|
||||
effectiveView.setNeedsLayout()
|
||||
updateConstraints()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// 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 = Constant.affineTintColor
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// IntelligentsFocusApertureView.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public class IntelligentsFocusApertureView: UIView {
|
||||
let backgroundView = UIView()
|
||||
let snapshotView = 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 { snapshotView.image }
|
||||
set { snapshotView.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
|
||||
backgroundView.addGestureRecognizer(UITapGestureRecognizer(
|
||||
target: self,
|
||||
action: #selector(dismissFocus)
|
||||
))
|
||||
|
||||
snapshotView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
snapshotView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
snapshotView.layer.contentsGravity = .top
|
||||
snapshotView.layer.masksToBounds = true
|
||||
snapshotView.contentMode = .scaleAspectFill
|
||||
snapshotView.isUserInteractionEnabled = true
|
||||
snapshotView.addGestureRecognizer(UITapGestureRecognizer(
|
||||
target: self,
|
||||
action: #selector(dismissFocus)
|
||||
))
|
||||
|
||||
addSubview(backgroundView)
|
||||
addSubview(controlButtonsPanel)
|
||||
addSubview(snapshotView)
|
||||
bringSubviewToFront(snapshotView)
|
||||
|
||||
controlButtonsPanel.translateButton.action = { [weak self] in
|
||||
self?.delegate?.focusApertureRequestAction(actionType: .translateTo)
|
||||
}
|
||||
controlButtonsPanel.summaryButton.action = { [weak self] in
|
||||
self?.delegate?.focusApertureRequestAction(actionType: .summary)
|
||||
}
|
||||
controlButtonsPanel.chatWithAIButton.action = { [weak self] in
|
||||
self?.delegate?.focusApertureRequestAction(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(actionType: .dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"color-space": "srgb",
|
||||
"components": {
|
||||
"alpha": "1.000",
|
||||
"blue": "228",
|
||||
"green": "148",
|
||||
"red": "72"
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "close.svg",
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.922363" y="0.5" width="17" height="17" rx="8.5" fill="#2A2A2A"/>
|
||||
<rect x="0.922363" y="0.5" width="17" height="17" rx="8.5" stroke="#E6E6E6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.1572 5.73483C6.30364 5.58839 6.54108 5.58839 6.68753 5.73483L9.42236 8.46967L12.1572 5.73484C12.3036 5.58839 12.5411 5.58839 12.6875 5.73484C12.834 5.88128 12.834 6.11872 12.6875 6.26517L9.95269 9L12.6875 11.7348C12.834 11.8813 12.834 12.1187 12.6875 12.2652C12.5411 12.4116 12.3036 12.4116 12.1572 12.2652L9.42236 9.53033L6.68753 12.2652C6.54108 12.4116 6.30364 12.4116 6.1572 12.2652C6.01075 12.1187 6.01075 11.8813 6.1572 11.7348L8.89203 9L6.1572 6.26516C6.01075 6.11872 6.01075 5.88128 6.1572 5.73483Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 831 B |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "spark.svg",
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
},
|
||||
"properties": {
|
||||
"template-rendering-intent": "template"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.1738 5.49104C12.1329 5.13024 11.8278 4.85751 11.4647 4.85714C11.1016 4.85677 10.796 5.12887 10.7544 5.48959C10.4839 7.83515 9.78261 9.48448 8.65126 10.6158C7.51992 11.7472 5.87058 12.4485 3.52502 12.719C3.16431 12.7606 2.89221 13.0662 2.89258 13.4293C2.89295 13.7924 3.16568 14.0975 3.52647 14.1383C5.83327 14.3996 7.51766 15.1006 8.67586 16.2379C9.82971 17.3709 10.5456 19.0198 10.7525 21.3489C10.7853 21.7178 11.0945 22.0004 11.4648 22C11.8351 21.9996 12.1437 21.7162 12.1756 21.3473C12.3739 19.0565 13.0892 17.3729 14.2487 16.2133C15.4083 15.0537 17.092 14.3385 19.3827 14.1402C19.7517 14.1083 20.035 13.7997 20.0354 13.4294C20.0359 13.0591 19.7532 12.7499 19.3843 12.7171C17.0553 12.5102 15.4063 11.7943 14.2733 10.6404C13.136 9.48222 12.435 7.79783 12.1738 5.49104Z"
|
||||
fill="#1E96EB" />
|
||||
<path
|
||||
d="M19.8353 2.24651C19.8194 2.1062 19.7007 2.00014 19.5595 2C19.4183 1.99986 19.2995 2.10567 19.2833 2.24595C19.1781 3.15811 18.9054 3.79952 18.4654 4.23949C18.0254 4.67946 17.384 4.95218 16.4719 5.05739C16.3316 5.07356 16.2258 5.19241 16.2259 5.33362C16.2261 5.47482 16.3321 5.59345 16.4724 5.60935C17.3695 5.71096 18.0246 5.98357 18.475 6.42584C18.9237 6.86644 19.2021 7.50771 19.2826 8.41347C19.2953 8.55691 19.4156 8.66683 19.5596 8.66667C19.7036 8.6665 19.8236 8.55632 19.836 8.41284C19.9131 7.52199 20.1913 6.86723 20.6422 6.41629C21.0931 5.96534 21.7479 5.68719 22.6388 5.61008C22.7822 5.59766 22.8924 5.47765 22.8926 5.33365C22.8927 5.18964 22.7828 5.06939 22.6394 5.05664C21.7336 4.97619 21.0924 4.69777 20.6517 4.24905C20.2095 3.79864 19.9369 3.1436 19.8353 2.24651Z"
|
||||
fill="#1E96EB" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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. */
|
||||
"AFFiNE AI" = "AFFiNE AI";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Translate" = "Translate";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Summary" = "Summary";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Summarize this article for me..." = "Summarize this article for me...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"System" = "System";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"AFFiNE AI" = "AFFiNE AI";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You" = "You";
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
Intelligents
|
||||
|
||||
Created by 秋星桥 on 2024/11/18.
|
||||
|
||||
*/
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat with AI" = "与 AI 聊天";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"AFFiNE AI" = "AFFiNE 人工智能";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Translate" = "翻译";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Summary" = "总结";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Summarize this article for me..." = "请为我总结这份文档...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"System" = "系统";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"AFFiNE AI" = "AFFiNE AI";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You" = "你";
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// CircleImageView.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/12/13.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class CircleImageView: UIImageView {
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
contentMode = .scaleAspectFill
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
clipsToBounds = true
|
||||
layer.cornerRadius = (bounds.width + bounds.height) / 2 / 2
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// UIHostingView.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/12/13.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
class UIHostingView<Content: View>: UIView {
|
||||
private let hostingViewController: UIHostingController<Content>
|
||||
|
||||
var rootView: Content {
|
||||
get { hostingViewController.rootView }
|
||||
set { hostingViewController.rootView = newValue }
|
||||
}
|
||||
|
||||
init(rootView: Content) {
|
||||
hostingViewController = UIHostingController(rootView: rootView)
|
||||
super.init(frame: .zero)
|
||||
|
||||
hostingViewController.view?.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(hostingViewController.view)
|
||||
if let view = hostingViewController.view {
|
||||
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)
|
||||
}
|
||||
}
|
||||
29
packages/frontend/apps/ios/setup.sh
Executable file
29
packages/frontend/apps/ios/setup.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/zsh
|
||||
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# packages/frontend/apps/ios/
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
cd ../../../../
|
||||
|
||||
if [ ! -d .git ]; then
|
||||
echo "[-] .git directory not found at project root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] setting up the project"
|
||||
|
||||
yarn install
|
||||
BUILD_TYPE=canary PUBLIC_PATH="/" yarn workspace @affine/ios build
|
||||
yarn workspace @affine/ios cap sync
|
||||
|
||||
rustup target add aarch64-apple-ios
|
||||
rustup target add aarch64-apple-ios-sim
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
echo "[+] setup complete"
|
||||
|
||||
yarn workspace @affine/ios cap open ios
|
||||
@@ -6,6 +6,7 @@ import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigatio
|
||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { AIButtonProvider } from '@affine/core/modules/ai-button';
|
||||
import {
|
||||
AuthService,
|
||||
DefaultServerService,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
ValidatorProvider,
|
||||
WebSocketAuthProvider,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { LifecycleService } from '@affine/core/modules/lifecycle';
|
||||
@@ -21,11 +23,18 @@ import { PopupWindowProvider } from '@affine/core/modules/url';
|
||||
import { ClientSchemeProvider } from '@affine/core/modules/url/providers/client-schema';
|
||||
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import {
|
||||
configureBrowserWorkspaceFlavours,
|
||||
configureIndexedDBWorkspaceEngineStorageProvider,
|
||||
} from '@affine/core/modules/workspace-engine';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import {
|
||||
docLinkBaseURLMiddleware,
|
||||
MarkdownAdapter,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { Job } from '@blocksuite/affine/store';
|
||||
import { App as CapacitorApp } from '@capacitor/app';
|
||||
import { Browser } from '@capacitor/browser';
|
||||
import { Haptics } from '@capacitor/haptics';
|
||||
@@ -39,6 +48,7 @@ import { configureFetchProvider } from './fetch';
|
||||
import { ModalConfigProvider } from './modal-config';
|
||||
import { Cookie } from './plugins/cookie';
|
||||
import { Hashcash } from './plugins/hashcash';
|
||||
import { Intelligents } from './plugins/intelligents';
|
||||
import { NavigationGesture } from './plugins/navigation-gesture';
|
||||
|
||||
const future = {
|
||||
@@ -109,6 +119,15 @@ framework.impl(HapticProvider, {
|
||||
selectionChanged: () => Haptics.selectionChanged(),
|
||||
selectionEnd: () => Haptics.selectionEnd(),
|
||||
});
|
||||
framework.impl(AIButtonProvider, {
|
||||
presentAIButton: () => {
|
||||
return Intelligents.presentIntelligentsButton();
|
||||
},
|
||||
dismissAIButton: () => {
|
||||
return Intelligents.dismissIntelligentsButton();
|
||||
},
|
||||
});
|
||||
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
// ------ some apis for native ------
|
||||
@@ -125,6 +144,51 @@ const frameworkProvider = framework.provider();
|
||||
(window as any).getCurrentI18nLocale = () => {
|
||||
return I18n.language;
|
||||
};
|
||||
(window as any).getCurrentDocContentInMarkdown = async () => {
|
||||
const globalContextService = frameworkProvider.get(GlobalContextService);
|
||||
const currentWorkspaceId =
|
||||
globalContextService.globalContext.workspaceId.get();
|
||||
const currentDocId = globalContextService.globalContext.docId.get();
|
||||
const workspacesService = frameworkProvider.get(WorkspacesService);
|
||||
const workspaceRef = currentWorkspaceId
|
||||
? workspacesService.openByWorkspaceId(currentWorkspaceId)
|
||||
: null;
|
||||
if (!workspaceRef) {
|
||||
return;
|
||||
}
|
||||
const { workspace, dispose: disposeWorkspace } = workspaceRef;
|
||||
|
||||
const docsService = workspace.scope.get(DocsService);
|
||||
const docRef = currentDocId ? docsService.open(currentDocId) : null;
|
||||
if (!docRef) {
|
||||
return;
|
||||
}
|
||||
const { doc, release: disposeDoc } = docRef;
|
||||
|
||||
try {
|
||||
const blockSuiteDoc = doc.blockSuiteDoc;
|
||||
|
||||
const job = new Job({
|
||||
collection: blockSuiteDoc.collection,
|
||||
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
|
||||
});
|
||||
const snapshot = job.docToSnapshot(blockSuiteDoc);
|
||||
|
||||
const adapter = new MarkdownAdapter(job);
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdownResult = await adapter.fromDocSnapshot({
|
||||
snapshot,
|
||||
assets: job.assetsManager,
|
||||
});
|
||||
return markdownResult.file;
|
||||
} finally {
|
||||
disposeDoc();
|
||||
disposeWorkspace();
|
||||
}
|
||||
};
|
||||
|
||||
// setup application lifecycle events, and emit application start event
|
||||
window.addEventListener('focus', () => {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IntelligentsPlugin {
|
||||
presentIntelligentsButton(): Promise<void>;
|
||||
dismissIntelligentsButton(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
import type { IntelligentsPlugin } from './definitions';
|
||||
|
||||
const Intelligents = registerPlugin<IntelligentsPlugin>('Intelligents');
|
||||
|
||||
export * from './definitions';
|
||||
export { Intelligents };
|
||||
Reference in New Issue
Block a user