feat(ios): add intelligents button (#9281)

Co-authored-by: 砍砍 <git@qaq.wiki>
This commit is contained in:
EYHN
2024-12-24 15:51:11 +08:00
committed by GitHub
parent fbe3e08769
commit 3cf4bcf651
78 changed files with 3283 additions and 114 deletions

View File

@@ -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())
}
}

View 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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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"/>

View 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"
}

View File

@@ -0,0 +1,7 @@
{
"sourceLanguage" : "en",
"strings" : {
},
"version" : "1.0"
}

View File

@@ -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"),
]),
]
)

View File

@@ -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
}
}

View File

@@ -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))
}
}

View File

@@ -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))])
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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

View 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
}
}

View File

@@ -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))])
}
}
}