diff --git a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved index bd7eb5f1b5..6c76aeb265 100644 --- a/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -44,6 +44,15 @@ "revision" : "b626d3002773b1a1304166643e7f118f724b2132", "version" : "1.0.4" } + }, + { + "identity" : "then", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devxoul/Then", + "state" : { + "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", + "version" : "3.0.0" + } } ], "version" : 2 diff --git a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift index b2421c76b5..eedfaaa68c 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift @@ -10,12 +10,13 @@ import UIKit extension AFFiNEViewController: IntelligentsButtonDelegate { func onIntelligentsButtonTapped(_ button: IntelligentsButton) { - guard let webView else { - assertionFailure() // ? wdym ? - return - } - + IntelligentContext.shared.webView = webView! button.beginProgress() - + + IntelligentContext.shared.preparePresent() { + button.stopProgress() + let controller = IntelligentsController() + self.present(controller, animated: true) + } } } diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index 5e8c3b75bd..ddc1208580 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -11,7 +11,7 @@ class AFFiNEViewController: CAPBridgeViewController { edgesForExtendedLayout = [] let intelligentsButton = installIntelligentsButton() intelligentsButton.delegate = self - dismissIntelligentsButton() + presentIntelligentsButton() // from v2.0 always visible } override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration { @@ -29,7 +29,7 @@ class AFFiNEViewController: CAPBridgeViewController { CookiePlugin(), HashcashPlugin(), NavigationGesturePlugin(), - IntelligentsPlugin(representController: self), + // IntelligentsPlugin(representController: self), // no longer put in use NbStorePlugin(), ] plugins.forEach { bridge?.registerPluginInstance($0) } @@ -39,15 +39,6 @@ class AFFiNEViewController: CAPBridgeViewController { super.viewDidAppear(animated) navigationController?.setNavigationBarHidden(false, animated: animated) } - -#if DEBUG - override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - super.motionEnded(motion, with: event) - if motion == .motionShake { - presentIntelligentsButton() - } - } -#endif } diff --git a/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift index f348b06294..cff51cf720 100644 --- a/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift +++ b/packages/frontend/apps/ios/App/App/Plugins/Cookie/IntelligentsPlugin.swift @@ -1,36 +1,36 @@ import Capacitor import Foundation -@objc(IntelligentsPlugin) -public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin { - public let identifier = "IntelligentsPlugin" - public let jsName = "Intelligents" - public let pluginMethods: [CAPPluginMethod] = [ - CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise), - ] - public private(set) weak var representController: UIViewController? - - init(representController: UIViewController) { - self.representController = representController - super.init() - } - - deinit { - representController = nil - } - - @objc public func presentIntelligentsButton(_ call: CAPPluginCall) { - DispatchQueue.main.async { - self.representController?.presentIntelligentsButton() - call.resolve() - } - } - - @objc public func dismissIntelligentsButton(_ call: CAPPluginCall) { - DispatchQueue.main.async { - self.representController?.dismissIntelligentsButton() - call.resolve() - } - } -} +//@objc(IntelligentsPlugin) +//public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin { +// public let identifier = "IntelligentsPlugin" +// public let jsName = "Intelligents" +// public let pluginMethods: [CAPPluginMethod] = [ +// CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise), +// CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise), +// ] +// public private(set) weak var representController: UIViewController? +// +// init(representController: UIViewController) { +// self.representController = representController +// super.init() +// } +// +// deinit { +// representController = nil +// } +// +// @objc public func presentIntelligentsButton(_ call: CAPPluginCall) { +// DispatchQueue.main.async { +// self.representController?.presentIntelligentsButton() +// call.resolve() +// } +// } +// +// @objc public func dismissIntelligentsButton(_ call: CAPPluginCall) { +// DispatchQueue.main.async { +// self.representController?.dismissIntelligentsButton() +// call.resolve() +// } +// } +//} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift index eeb6e098d6..9cdb8cc65d 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Package.swift @@ -16,12 +16,14 @@ let package = Package( .package(path: "../AffineGraphQL"), .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.18.0"), .package(url: "https://github.com/apple/swift-collections", from: "1.2.0"), - .package(url: "https://github.com/SnapKit/SnapKit.git", .upToNextMajor(from: "5.0.1")) + .package(url: "https://github.com/devxoul/Then", from: "3.0.0"), + .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.0.1"), ], targets: [ .target(name: "Intelligents", dependencies: [ "AffineGraphQL", "SnapKit", + "Then", .product(name: "Apollo", package: "apollo-ios"), .product(name: "OrderedCollections", package: "swift-collections"), ]), diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift index bfe0701cdd..f07f81b7e0 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Intelligents.swift @@ -5,8 +5,7 @@ import AffineGraphQL import Apollo import Foundation -public enum Intelligents { -} +public enum Intelligents {} private extension Intelligents { private final class URLSessionCookieClient: URLSessionClient { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift index fc2d44e94e..9dae17c6c3 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton+Control.swift @@ -5,6 +5,7 @@ // Created by 秋星桥 on 2024/11/18. // +import SnapKit import UIKit public extension UIViewController { @@ -16,15 +17,15 @@ public extension UIViewController { let button = IntelligentsButton() view.addSubview(button) view.bringSubviewToFront(button) - button.translatesAutoresizingMaskIntoConstraints = false - [ - button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), - button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20 - 44), - button.widthAnchor.constraint(equalToConstant: 50), - button.heightAnchor.constraint(equalToConstant: 50), - ].forEach { $0.isActive = true } + button.snp.makeConstraints { make in + make.trailing.equalTo(view.safeAreaLayoutGuide).offset(-20) + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20 - 44) + make.width.height.equalTo(50) + } button.transform = .init(scaleX: 0, y: 0) - view.layoutIfNeeded() + if view.frame != .zero { + view.layoutIfNeeded() + } return button } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift index daa5dcac62..59c1c32d01 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsButton/IntelligentsButton.swift @@ -5,13 +5,22 @@ // Created by 秋星桥 on 2024/11/18. // +import SnapKit +import Then import UIKit // floating button to open intelligent panel public class IntelligentsButton: UIView { - let image = UIImageView() - let background = UIView() - let activityIndicator = UIActivityIndicatorView() + lazy var image = UIImageView().then { + $0.image = .init(named: "spark", in: .module, with: .none) + $0.contentMode = .scaleAspectFit + } + + lazy var background = UIView().then { + $0.backgroundColor = .white + } + + lazy var activityIndicator = UIActivityIndicatorView() public weak var delegate: (any IntelligentsButtonDelegate)? = nil { didSet { assert(Thread.isMainThread) } @@ -19,44 +28,10 @@ public class IntelligentsButton: UIView { public init() { super.init(frame: .zero) - - background.backgroundColor = .white - addSubview(background) - background.translatesAutoresizingMaskIntoConstraints = false - [ - background.leadingAnchor.constraint(equalTo: leadingAnchor), - background.trailingAnchor.constraint(equalTo: trailingAnchor), - background.topAnchor.constraint(equalTo: topAnchor), - background.bottomAnchor.constraint(equalTo: bottomAnchor), - ].forEach { $0.isActive = true } - - image.image = .init(named: "spark", in: .module, with: .none) - image.contentMode = .scaleAspectFit - addSubview(image) - let imageInsetValue: CGFloat = 12 - image.translatesAutoresizingMaskIntoConstraints = false - [ - image.leadingAnchor.constraint(equalTo: leadingAnchor, constant: imageInsetValue), - image.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -imageInsetValue), - image.topAnchor.constraint(equalTo: topAnchor, constant: imageInsetValue), - image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue), - ].forEach { $0.isActive = true } - - activityIndicator.translatesAutoresizingMaskIntoConstraints = false - addSubview(activityIndicator) - [ - activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), - ].forEach { $0.isActive = true } - - clipsToBounds = true - layer.borderWidth = 2 - layer.borderColor = UIColor.gray.withAlphaComponent(0.1).cgColor - - let tap = UITapGestureRecognizer(target: self, action: #selector(tapped)) - addGestureRecognizer(tap) - isUserInteractionEnabled = true - + setupViews() + setupConstraints() + setupGesture() + setupAppearance() stopProgress() } @@ -96,3 +71,39 @@ public class IntelligentsButton: UIView { image.isHidden = false } } + +// MARK: - Setup Methods + +private extension IntelligentsButton { + func setupViews() { + addSubview(background) + addSubview(image) + addSubview(activityIndicator) + } + + func setupConstraints() { + background.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + image.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(12) + } + + activityIndicator.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + func setupGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(tapped)) + addGestureRecognizer(tap) + isUserInteractionEnabled = true + } + + func setupAppearance() { + clipsToBounds = true + layer.borderWidth = 2 + layer.borderColor = UIColor.gray.withAlphaComponent(0.1).cgColor + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsController/BlurTransition.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsController/BlurTransition.swift new file mode 100644 index 0000000000..02587fa28a --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsController/BlurTransition.swift @@ -0,0 +1,168 @@ +// +// BlurTransition.swift +// BlurTransition +// +// Created by 秋星桥 on 6/16/23. +// + +import UIKit + +extension UIViewController { + func presentWithFullScreenBlurTransition(_ viewController: UIViewController) { + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = BlurTransitioningDelegate.shared + present(viewController, animated: true) + } +} + +class BlurTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { + static let shared = BlurTransitioningDelegate() + + func animationController( + forPresented _: UIViewController, + presenting _: UIViewController, + source _: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + BlurTransitionAnimator(presenting: true) + } + + func animationController( + forDismissed _: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + BlurTransitionAnimator(presenting: false) + } +} + +class BlurTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { + private let presenting: Bool + private let duration: TimeInterval = 0.5 + + private let snapshotViewTag = "snapshotView".hashValue + private let blurViewTag = "blurView".hashValue + + init(presenting: Bool) { + self.presenting = presenting + super.init() + } + + private func performAnimation( + animations: @escaping () -> Void, + completion: @escaping (Bool) -> Void + ) { + UIView.animate( + withDuration: duration, + delay: 0, + usingSpringWithDamping: 0.75, + initialSpringVelocity: 0.75, + options: [.beginFromCurrentState, .allowAnimatedContent, .curveEaseInOut], + animations: animations, + completion: completion + ) + } + + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + duration + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + if presenting { + animatePresentation(using: transitionContext) + } else { + animateDismissal(using: transitionContext) + } + } + + private func animatePresentation(using transitionContext: UIViewControllerContextTransitioning) { + guard let toViewController = transitionContext.viewController(forKey: .to), + let fromViewController = transitionContext.viewController(forKey: .from) + else { + transitionContext.completeTransition(false) + assertionFailure() + return + } + + let toView = toViewController.view! + let fromView = fromViewController.view! + + let containerView = transitionContext.containerView + + guard let fromViewSnapshot = fromView.snapshotView(afterScreenUpdates: false) else { + transitionContext.completeTransition(false) + assertionFailure() + return + } + fromViewSnapshot.frame = fromView.frame + fromViewSnapshot.tag = snapshotViewTag + containerView.addSubview(fromViewSnapshot) + fromView.isHidden = true + + let blurEffectView = UIVisualEffectView() + blurEffectView.frame = containerView.bounds + blurEffectView.tag = blurViewTag + containerView.addSubview(blurEffectView) + + toView.frame = containerView.bounds + toView.alpha = 0 + toView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + containerView.addSubview(toView) + + performAnimation(animations: { + blurEffectView.effect = UIBlurEffect(style: .systemMaterial) + fromViewSnapshot.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + toView.alpha = 1 + toView.transform = .identity + }) { _ in + let success = !transitionContext.transitionWasCancelled + if !success { + assertionFailure() + fromView.isHidden = false + fromViewSnapshot.removeFromSuperview() + blurEffectView.removeFromSuperview() + toView.removeFromSuperview() + } + transitionContext.completeTransition(success) + } + } + + private func animateDismissal(using transitionContext: UIViewControllerContextTransitioning) { + guard let fromViewController = transitionContext.viewController(forKey: .from), + let toViewController = transitionContext.viewController(forKey: .to) + else { + transitionContext.completeTransition(false) + assertionFailure() + return + } + + let fromView = fromViewController.view! + let toView = toViewController.view! + let containerView = transitionContext.containerView + + guard let fromViewSnapshot = containerView.viewWithTag(snapshotViewTag), + let blurEffectView = containerView.viewWithTag(blurViewTag) as? UIVisualEffectView + else { + toView.isHidden = false + assertionFailure() + transitionContext.completeTransition(true) + return + } + + performAnimation(animations: { + fromViewSnapshot.transform = .identity + blurEffectView.effect = nil + fromView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + fromView.alpha = 0 + }) { _ in + let success = !transitionContext.transitionWasCancelled + if success { + toView.isHidden = false + fromViewSnapshot.removeFromSuperview() + blurEffectView.removeFromSuperview() + } else { + assertionFailure() + fromView.transform = .identity + fromView.alpha = 1 + } + transitionContext.completeTransition(success) + } + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsController/IntelligentsController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsController/IntelligentsController.swift new file mode 100644 index 0000000000..52ef5270dc --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/IntelligentsController/IntelligentsController.swift @@ -0,0 +1,37 @@ +// +// IntelligentsController.swift +// Intelligents +// +// Created by 秋星桥 on 6/17/25. +// + +import UIKit + +public class IntelligentsController: UINavigationController { + public init() { + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .custom + transitioningDelegate = BlurTransitioningDelegate.shared + setNavigationBarHidden(true, animated: false) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + override public func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setNavigationBarHidden(true, animated: animated) + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + setNavigationBarHidden(false, animated: animated) + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/UIHostingView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/UIHostingView.swift deleted file mode 100644 index 86422f9ecf..0000000000 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/SupplementView/UIHostingView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// UIHostingView.swift -// Intelligents -// -// Created by 秋星桥 on 2024/12/13. -// - -import SwiftUI -import UIKit - -class UIHostingView: UIView { - private let hostingViewController: UIHostingController - - var rootView: Content { - get { hostingViewController.rootView } - set { hostingViewController.rootView = newValue } - } - - override var intrinsicContentSize: CGSize { - hostingViewController.view.intrinsicContentSize - } - - init(rootView: Content) { - hostingViewController = UIHostingController(rootView: rootView) - hostingViewController.edgesForExtendedLayout = [] - hostingViewController.extendedLayoutIncludesOpaqueBars = false - super.init(frame: .zero) - - hostingViewController.view?.translatesAutoresizingMaskIntoConstraints = false - addSubview(hostingViewController.view) - if let view = hostingViewController.view { - view.removeFromSuperview() - view.backgroundColor = .clear - view.isOpaque = false - addSubview(view) - let constraints = [ - view.topAnchor.constraint(equalTo: topAnchor), - view.bottomAnchor.constraint(equalTo: bottomAnchor), - view.leftAnchor.constraint(equalTo: leftAnchor), - view.rightAnchor.constraint(equalTo: rightAnchor), - ] - NSLayoutConstraint.activate(constraints) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func sizeThatFits(_ size: CGSize) -> CGSize { - hostingViewController.sizeThatFits(in: size) - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift new file mode 100644 index 0000000000..369c707c1b --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Model/IntelligentContext.swift @@ -0,0 +1,24 @@ +// +// IntelligentContext.swift +// Intelligents +// +// Created by 秋星桥 on 6/17/25. +// + +import Foundation +import WebKit + +public class IntelligentContext { + // shared across the app, we expect our app to have a single context and webview + public static let shared = IntelligentContext() + + public var webView: WKWebView! + + private init() {} + + public func preparePresent(_ completion: @escaping () -> Void) { + // used to gathering information, populate content from webview, etc. + // TODO: if needed + completion() + } +}