WIP: 完成 AI 框架嵌入文档的动画

This commit is contained in:
砍砍
2024-11-21 19:10:03 +08:00
parent e68070186a
commit 9d0de52609
24 changed files with 576 additions and 233 deletions
@@ -1,28 +1,92 @@
import UIKit
import Capacitor import Capacitor
import Intelligents import Intelligents
import UIKit
class AFFiNEViewController: CAPBridgeViewController { class AFFiNEViewController: CAPBridgeViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true webView?.allowsBackForwardNavigationGestures = true
self.navigationController?.navigationBar.isHidden = true navigationController?.navigationBar.isHidden = true
installIntelligentsButton() extendedLayoutIncludesOpaqueBars = false
edgesForExtendedLayout = []
let intelligentsButton = installIntelligentsButton()
intelligentsButton.delegate = self
dismissIntelligentsButton()
} }
override func capacitorDidLoad() { override func capacitorDidLoad() {
bridge?.registerPluginInstance(CookiePlugin()) let plugins: [CAPPlugin] = [
bridge?.registerPluginInstance(HashcashPlugin()) CookiePlugin(),
bridge?.registerPluginInstance(IntelligentsPlugin(ui: self)) HashcashPlugin(),
IntelligentsPlugin(representController: self),
]
plugins.forEach { bridge?.registerPluginInstance($0) }
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated) navigationController?.setNavigationBarHidden(false, animated: animated)
self.dismissIntelligentsButton()
} }
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
} }
} }
extension AFFiNEViewController: IntelligentsButtonDelegate {
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)")
}
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 else {
assertionFailure()
return
}
_ = context
let focus = IntelligentsFocusApertureView()
focus.prepareAnimationWith(
capturingTargetContentView: view,
coveringRootViewController: self
)
focus.executeAnimationKickIn()
}
func openSimpleChat() {
let targetController = IntelligentsChatController()
presentIntoCurrentContext(withTargetController: targetController)
}
}
@@ -1,49 +1,47 @@
import UIKit
import Capacitor import Capacitor
import UIKit
@UIApplicationMain @main
class AppDelegate: UIResponder, UIApplicationDelegate { 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 { func applicationWillResignActive(_: UIApplication) {
// Override point for customization after application launch. // 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.
return true // 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) { func applicationDidEnterBackground(_: 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 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.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
} }
func applicationDidEnterBackground(_ application: UIApplication) { func applicationWillEnterForeground(_: 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. // 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.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. }
}
func applicationWillEnterForeground(_ application: UIApplication) { func applicationDidBecomeActive(_: 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. // 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) { func applicationWillTerminate(_: 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. // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
} }
func applicationWillTerminate(_ application: UIApplication) { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // 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(_ 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(_ 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)
}
} }
@@ -18,20 +18,20 @@ extension UIView {
} }
return nil return nil
} }
#if DEBUG #if DEBUG
func debugFrame() { func debugFrame() {
layer.borderWidth = 1 layer.borderWidth = 1
layer.borderColor = [ layer.borderColor = [
UIColor.red, UIColor.red,
.green, .green,
.blue, .blue,
.yellow, .yellow,
.cyan, .cyan,
.magenta, .magenta,
.orange, .orange,
].map(\.cgColor).randomElement() ].map(\.cgColor).randomElement()
subviews.forEach { $0.debugFrame() } subviews.forEach { $0.debugFrame() }
} }
#endif #endif
} }
@@ -7,7 +7,7 @@
import UIKit import UIKit
extension UIViewController { public extension UIViewController {
func presentIntoCurrentContext(withTargetController targetController: UIViewController, animated: Bool = true) { func presentIntoCurrentContext(withTargetController targetController: UIViewController, animated: Bool = true) {
if let nav = self as? UINavigationController { if let nav = self as? UINavigationController {
nav.pushViewController(targetController, animated: animated) nav.pushViewController(targetController, animated: animated)
@@ -44,6 +44,7 @@ public extension UIViewController {
button.alpha = 0 button.alpha = 0
button.isHidden = false button.isHidden = false
button.setNeedsLayout() button.setNeedsLayout()
button.stopProgress()
view.layoutIfNeeded() view.layoutIfNeeded()
UIView.animate( UIView.animate(
@@ -63,6 +64,7 @@ public extension UIViewController {
guard let button = findIntelligentsButton() else { return } guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)") print("[*] \(button) is calling \(#function)")
button.stopProgress()
button.setNeedsLayout() button.setNeedsLayout()
view.layoutIfNeeded() view.layoutIfNeeded()
UIView.animate( UIView.animate(
@@ -0,0 +1,12 @@
//
// IntelligentsButton+Delegate.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import Foundation
public protocol IntelligentsButtonDelegate: AnyObject {
func onIntelligentsButtonTapped(_ button: IntelligentsButton)
}
@@ -11,6 +11,11 @@ import UIKit
public class IntelligentsButton: UIView { public class IntelligentsButton: UIView {
let image = UIImageView() let image = UIImageView()
let background = UIView() let background = UIView()
let activityIndicator = UIActivityIndicatorView()
public weak var delegate: (any IntelligentsButtonDelegate)? = nil {
didSet { assert(Thread.isMainThread) }
}
public init() { public init() {
super.init(frame: .zero) super.init(frame: .zero)
@@ -38,9 +43,11 @@ public class IntelligentsButton: UIView {
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue), image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue),
].forEach { $0.isActive = true } ].forEach { $0.isActive = true }
// layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor addSubview(activityIndicator)
// layer.shadowOffset = CGSize(width: 0, height: 0) [
// layer.shadowRadius = 8 activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
].forEach { $0.isActive = true }
clipsToBounds = true clipsToBounds = true
layer.borderWidth = 2 layer.borderWidth = 2
@@ -49,6 +56,8 @@ public class IntelligentsButton: UIView {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped)) let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
addGestureRecognizer(tap) addGestureRecognizer(tap)
isUserInteractionEnabled = true isUserInteractionEnabled = true
stopProgress()
} }
@available(*, unavailable) @available(*, unavailable)
@@ -56,18 +65,26 @@ public class IntelligentsButton: UIView {
fatalError() fatalError()
} }
deinit {
delegate = nil
}
override public func layoutSubviews() { override public func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
layer.cornerRadius = bounds.width / 2 layer.cornerRadius = bounds.width / 2
} }
@objc func tapped() { @objc func tapped() {
guard let controller = parentViewController else { delegate?.onIntelligentsButtonTapped(self)
assertionFailure() }
return
} public func beginProgress() {
let targetController = IntelligentsChatController() activityIndicator.startAnimating()
controller.presentIntoCurrentContext(withTargetController: targetController) activityIndicator.isHidden = false
}
public func stopProgress() {
activityIndicator.stopAnimating()
activityIndicator.isHidden = true
} }
} }
@@ -25,7 +25,7 @@ class AttachmentBannerView: UIScrollView {
height: attachmentSize height: attachmentSize
) )
} }
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
@@ -33,7 +33,7 @@ class AttachmentBannerView: UIScrollView {
showsHorizontalScrollIndicator = false showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false showsVerticalScrollIndicator = false
rebuildViews() rebuildViews()
} }
@@ -41,7 +41,7 @@ class AttachmentBannerView: UIScrollView {
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {
fatalError() fatalError()
} }
func rebuildViews() { func rebuildViews() {
subviews.forEach { $0.removeFromSuperview() } subviews.forEach { $0.removeFromSuperview() }
for (index, attachment) in attachments.enumerated() { for (index, attachment) in attachments.enumerated() {
@@ -1,24 +1,22 @@
// //
// File.swift // InputEditView+ViewModel.swift
// Intelligents // Intelligents
// //
// Created by on 2024/11/18. // Created by on 2024/11/18.
// //
import UIKit
import Combine import Combine
import UIKit
extension InputEditView { extension InputEditView {
class ViewModel: ObservableObject { class ViewModel: ObservableObject {
var cancellables: Set<AnyCancellable> = [] var cancellables: Set<AnyCancellable> = []
@Published var text: String = "" @Published var text: String = ""
@Published var attachments: [UIImage] = [] @Published var attachments: [UIImage] = []
init() { init() {}
}
deinit { deinit {
cancellables.forEach { $0.cancel() } cancellables.forEach { $0.cancel() }
cancellables.removeAll() cancellables.removeAll()
@@ -31,7 +29,7 @@ extension InputEditView.ViewModel: Hashable, Equatable {
hasher.combine(text) hasher.combine(text)
hasher.combine(attachments) hasher.combine(attachments)
} }
static func == (lhs: InputEditView.ViewModel, rhs: InputEditView.ViewModel) -> Bool { static func == (lhs: InputEditView.ViewModel, rhs: InputEditView.ViewModel) -> Bool {
lhs.hashValue == rhs.hashValue lhs.hashValue == rhs.hashValue
} }
@@ -5,8 +5,8 @@
// Created by on 2024/11/18. // Created by on 2024/11/18.
// //
import UIKit
import Combine import Combine
import UIKit
class InputEditView: UIView, UITextViewDelegate { class InputEditView: UIView, UITextViewDelegate {
let mainStack = UIStackView() let mainStack = UIStackView()
@@ -14,14 +14,14 @@ class InputEditView: UIView, UITextViewDelegate {
let textEditor = PlainTextEditView() let textEditor = PlainTextEditView()
let placeholderLabel = UILabel() let placeholderLabel = UILabel()
let controlBanner = TextEditControlBanner() let controlBanner = TextEditControlBanner()
let viewModel = ViewModel() let viewModel = ViewModel()
var placeholderText: String = "" { var placeholderText: String = "" {
didSet { didSet {
placeholderLabel.text = placeholderText placeholderLabel.text = placeholderText
} }
} }
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
@@ -51,7 +51,7 @@ class InputEditView: UIView, UITextViewDelegate {
$0.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor), $0.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor),
].forEach { $0.isActive = true } ].forEach { $0.isActive = true }
} }
textEditor.addSubview(placeholderLabel) textEditor.addSubview(placeholderLabel)
placeholderLabel.textColor = .label.withAlphaComponent(0.25) placeholderLabel.textColor = .label.withAlphaComponent(0.25)
placeholderLabel.font = textEditor.font placeholderLabel.font = textEditor.font
@@ -61,7 +61,7 @@ class InputEditView: UIView, UITextViewDelegate {
placeholderLabel.trailingAnchor.constraint(equalTo: textEditor.trailingAnchor, constant: -2), placeholderLabel.trailingAnchor.constraint(equalTo: textEditor.trailingAnchor, constant: -2),
placeholderLabel.topAnchor.constraint(equalTo: textEditor.topAnchor, constant: 0), placeholderLabel.topAnchor.constraint(equalTo: textEditor.topAnchor, constant: 0),
].forEach { $0.isActive = true } ].forEach { $0.isActive = true }
viewModel.objectWillChange viewModel.objectWillChange
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
@@ -76,26 +76,26 @@ class InputEditView: UIView, UITextViewDelegate {
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {
fatalError() fatalError()
} }
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
viewModel.text = textView.text viewModel.text = textView.text
} }
func textViewDidBeginEditing(_ textView: UITextView) { func textViewDidBeginEditing(_: UITextView) {
updatePlaceholderVisibility() updatePlaceholderVisibility()
} }
func textViewDidEndEditing(_ textView: UITextView) { func textViewDidEndEditing(_: UITextView) {
updatePlaceholderVisibility() updatePlaceholderVisibility()
} }
func updatePlaceholderVisibility() { func updatePlaceholderVisibility() {
let visible = viewModel.text.isEmpty && !textEditor.isFirstResponder let visible = viewModel.text.isEmpty && !textEditor.isFirstResponder
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
self.placeholderLabel.alpha = visible ? 1 : 0 self.placeholderLabel.alpha = visible ? 1 : 0
} }
} }
func updateValues() { func updateValues() {
UIView.animate( UIView.animate(
withDuration: 0.5, withDuration: 0.5,
@@ -109,7 +109,7 @@ class InputEditView: UIView, UITextViewDelegate {
if attachmentsEditor.attachments != viewModel.attachments { if attachmentsEditor.attachments != viewModel.attachments {
attachmentsEditor.attachments = viewModel.attachments attachmentsEditor.attachments = viewModel.attachments
} }
self.parentViewController?.view.layoutIfNeeded() parentViewController?.view.layoutIfNeeded()
} }
} }
} }
@@ -1,5 +1,5 @@
// //
// TextEditView.swift // PlainTextEditView.swift
// Intelligents // Intelligents
// //
// Created by on 2024/11/18. // Created by on 2024/11/18.
@@ -45,12 +45,12 @@ class TextEditControlBanner: UIStackView {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
addArrangedSubview($0) addArrangedSubview($0)
} }
cameraButton.setImage(.init(systemName: "camera"), for: .normal) cameraButton.setImage(.init(systemName: "camera"), for: .normal)
cameraButton.tintColor = .label cameraButton.tintColor = .label
photoButton.setImage(.init(systemName: "photo"), for: .normal) photoButton.setImage(.init(systemName: "photo"), for: .normal)
photoButton.tintColor = .label photoButton.tintColor = .label
sendButton.setImage(.init(systemName: "paperplane.fill"), for: .normal) sendButton.setImage(.init(systemName: "paperplane.fill"), for: .normal)
sendButton.tintColor = .label sendButton.tintColor = .label
} }
@@ -20,7 +20,6 @@ extension IntelligentsChatController {
editor.textEditor.font = UIFont.systemFont(ofSize: UIFont.labelFontSize) editor.textEditor.font = UIFont.systemFont(ofSize: UIFont.labelFontSize)
editor.placeholderText = "Summarize this article for me...".localized() editor.placeholderText = "Summarize this article for me...".localized()
backgroundView.backgroundColor = .systemBackground backgroundView.backgroundColor = .systemBackground
backgroundView.layer.cornerRadius = 16 backgroundView.layer.cornerRadius = 16
backgroundView.layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor backgroundView.layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor
@@ -39,10 +38,10 @@ private extension IntelligentsChatController.InputBox {
func setupLayout() { func setupLayout() {
addSubview(backgroundView) addSubview(backgroundView)
backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(editor) addSubview(editor)
editor.translatesAutoresizingMaskIntoConstraints = false editor.translatesAutoresizingMaskIntoConstraints = false
let inset: CGFloat = 16 let inset: CGFloat = 16
[ [
@@ -7,12 +7,12 @@
import UIKit import UIKit
class IntelligentsChatController: UIViewController { public class IntelligentsChatController: UIViewController {
let header = Header() let header = Header()
let inputBox = InputBox() let inputBox = InputBox()
let tableView = ChatTableView() let tableView = ChatTableView()
override var title: String? { override public var title: String? {
set { set {
super.title = newValue super.title = newValue
header.titleLabel.text = newValue header.titleLabel.text = newValue
@@ -22,7 +22,7 @@ class IntelligentsChatController: UIViewController {
} }
} }
init() { public init() {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
title = "Chat with AI".localized() title = "Chat with AI".localized()
} }
@@ -32,7 +32,7 @@ class IntelligentsChatController: UIViewController {
fatalError() fatalError()
} }
override func viewDidLoad() { override public func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
assert(navigationController != nil) assert(navigationController != nil)
view.backgroundColor = .secondarySystemBackground view.backgroundColor = .secondarySystemBackground
@@ -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,88 @@
//
// 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,24 @@
//
// IntelligentsFocusApertureView+Panel.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import UIKit
extension IntelligentsFocusApertureView {
class ControlButtonsPanel: UIView {
init() {
super.init(frame: .zero)
backgroundColor = .red
heightAnchor.constraint(equalToConstant: 256).isActive = true
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
}
@@ -0,0 +1,117 @@
//
// 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 init() {
super.init(frame: .zero)
let tap = UITapGestureRecognizer(
target: self,
action: #selector(dismissFocus)
)
backgroundView.backgroundColor = .black
backgroundView.isUserInteractionEnabled = true
backgroundView.addGestureRecognizer(tap)
snapshotView.layer.masksToBounds = true
snapshotView.contentMode = .scaleAspectFill
snapshotView.isUserInteractionEnabled = true
snapshotView.addGestureRecognizer(tap)
addSubview(backgroundView)
addSubview(controlButtonsPanel)
addSubview(snapshotView)
bringSubviewToFront(snapshotView)
var views: [UIView] = [self]
while let view = views.first {
views.removeFirst()
view.translatesAutoresizingMaskIntoConstraints = false
view.subviews.forEach { views.append($0) }
}
}
@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()
}
}
}
@@ -7,24 +7,24 @@ public class CookieManager: NSObject {
let jar = HTTPCookieStorage.shared let jar = HTTPCookieStorage.shared
guard let url = getServerUrl(urlString) else { return [:] } guard let url = getServerUrl(urlString) else { return [:] }
if let cookies = jar.cookies(for: url) { if let cookies = jar.cookies(for: url) {
for cookie in cookies { for cookie in cookies {
cookiesMap[cookie.name] = cookie.value cookiesMap[cookie.name] = cookie.value
} }
} }
return cookiesMap return cookiesMap
} }
private func isUrlSanitized(_ urlString: String) -> Bool { 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? { 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 { guard let url = URL(string: validUrlString) else {
return nil return nil
} }
return url return url
} }
} }
@@ -1,21 +1,21 @@
import Foundation
import Capacitor import Capacitor
import Foundation
@objc(CookiePlugin) @objc(CookiePlugin)
public class CookiePlugin: CAPPlugin, CAPBridgedPlugin { public class CookiePlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CookiePlugin" public let identifier = "CookiePlugin"
public let jsName = "Cookie" public let jsName = "Cookie"
public let pluginMethods: [CAPPluginMethod] = [ public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
] ]
let cookieManager = CookieManager() let cookieManager = CookieManager()
@objc public func getCookies(_ call: CAPPluginCall) { @objc public func getCookies(_ call: CAPPluginCall) {
guard let url = call.getString("url") else { guard let url = call.getString("url") else {
return call.resolve([:]) return call.resolve([:])
} }
call.resolve(cookieManager.getCookies(url)) call.resolve(cookieManager.getCookies(url))
} }
} }
@@ -3,105 +3,105 @@ import CryptoSwift
@objc(HashcashPlugin) @objc(HashcashPlugin)
public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin { public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "HashcashPlugin" public let identifier = "HashcashPlugin"
public let jsName = "Hashcash" public let jsName = "Hashcash"
public let pluginMethods: [CAPPluginMethod] = [ public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise),
] ]
@objc func hash(_ call: CAPPluginCall) { @objc func hash(_ call: CAPPluginCall) {
DispatchQueue.global(qos: .default).async { DispatchQueue.global(qos: .default).async {
let challenge = call.getString("challenge") ?? "" let challenge = call.getString("challenge") ?? ""
let bits = call.getInt("bits") ?? 20; let bits = call.getInt("bits") ?? 20
call.resolve(["value": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()]) call.resolve(["value": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()])
}
} }
}
} }
let SALT_LENGTH = 16 let SALT_LENGTH = 16
struct Stamp { struct Stamp {
let version: String let version: String
let claim: UInt32 let claim: UInt32
let ts: String let ts: String
let resource: String let resource: String
let ext: String let ext: String
let rand: String let rand: String
let counter: String let counter: String
func checkExpiration() -> Bool { func checkExpiration() -> Bool {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss" dateFormatter.dateFormat = "yyyyMMddHHmmss"
guard let date = dateFormatter.date(from: ts) else { return false } guard let date = dateFormatter.date(from: ts) else { return false }
return Date().addingTimeInterval(5 * 60) <= date return Date().addingTimeInterval(5 * 60) <= date
}
func check(bits: UInt32, resource: String) -> Bool {
if version == "1", bits <= claim, checkExpiration(), self.resource == resource {
let hexDigits = Int(floor(Float(claim) / 4.0))
// Check challenge
let formatted = format()
let result = formatted.data(using: .utf8)!.sha3(.sha256).compactMap { String(format: "%02x", $0) }.joined()
return result.prefix(hexDigits) == String(repeating: "0", count: hexDigits)
} else {
return false
} }
}
func check(bits: UInt32, resource: String) -> Bool {
if version == "1" && bits <= claim && checkExpiration() && self.resource == resource { func format() -> String {
let hexDigits = Int(floor(Float(claim) / 4.0)) "\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)"
}
// Check challenge
let formatted = format() static func mint(resource: String, bits: UInt32? = nil) -> Stamp {
let result = formatted.data(using: .utf8)!.sha3(.sha256).compactMap { String(format: "%02x", $0) }.joined() let version = "1"
return result.prefix(hexDigits) == String(repeating: "0", count: hexDigits) let now = Date()
} else { let dateFormatter = DateFormatter()
return false dateFormatter.dateFormat = "yyyyMMddHHmmss"
} let ts = dateFormatter.string(from: now)
} let bits = bits ?? 20
let rand = String((0 ..< SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! })
func format() -> String { let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"
return "\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)"
} let hexDigits = Int(ceil(Float(bits) / 4.0))
let zeros = String(repeating: "0", count: hexDigits)
static func mint(resource: String, bits: UInt32? = nil) -> Stamp { var counter = 0
let version = "1" var counterHex = ""
let now = Date() var hasher = SHA3(variant: .sha256)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss" while true {
let ts = dateFormatter.string(from: now) let toHash = "\(challenge):\(String(format: "%x", counter))"
let bits = bits ?? 20 let hashed = try! hasher.finish(withBytes: toHash.data(using: .utf8)!.bytes)
let rand = String((0..<SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! }) let result = hashed.compactMap { String(format: "%02x", $0) }.joined()
let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"
if result.prefix(hexDigits) == zeros {
let hexDigits = Int(ceil(Float(bits) / 4.0)) counterHex = String(format: "%x", counter)
let zeros = String(repeating: "0", count: hexDigits) break
var counter = 0 }
var counterHex = "" counter += 1
var hasher = SHA3(variant: .sha256)
while true {
let toHash = "\(challenge):\(String(format: "%x", counter))"
let hashed = try! hasher.finish(withBytes: toHash.data(using: .utf8)!.bytes)
let result = hashed.compactMap { String(format: "%02x", $0) }.joined()
if result.prefix(hexDigits) == zeros {
counterHex = String(format: "%x", counter)
break
}
counter += 1
}
return Stamp(version: version, claim: bits, ts: ts, resource: resource, ext: "", rand: rand, counter: counterHex)
} }
return Stamp(version: version, claim: bits, ts: ts, resource: resource, ext: "", rand: rand, counter: counterHex)
}
} }
extension Stamp { extension Stamp {
init?(from string: String) throws { init?(from string: String) throws {
let parts = string.split(separator: ":") let parts = string.split(separator: ":")
guard parts.count == 7 else { guard parts.count == 7 else {
throw NSError(domain: "StampError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp, expected 7 parts, got \(parts.count)"]) throw NSError(domain: "StampError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp, expected 7 parts, got \(parts.count)"])
}
guard let claim = UInt32(parts[1]) else {
throw NSError(domain: "StampError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp"])
}
self.version = String(parts[0])
self.claim = claim
self.ts = String(parts[2])
self.resource = String(parts[3])
self.ext = String(parts[4])
self.rand = String(parts[5])
self.counter = String(parts[6])
} }
guard let claim = UInt32(parts[1]) else {
throw NSError(domain: "StampError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp"])
}
version = String(parts[0])
self.claim = claim
ts = String(parts[2])
resource = String(parts[3])
ext = String(parts[4])
rand = String(parts[5])
counter = String(parts[6])
}
} }
@@ -1,5 +1,5 @@
import Foundation
import Capacitor import Capacitor
import Foundation
@objc(IntelligentsPlugin) @objc(IntelligentsPlugin)
public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin { public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
@@ -7,27 +7,29 @@ public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
public let jsName = "Intelligents" public let jsName = "Intelligents"
public let pluginMethods: [CAPPluginMethod] = [ public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise),
] ]
public let ui: UIViewController public private(set) weak var representController: UIViewController?
init(ui: UIViewController) { init(representController: UIViewController) {
self.ui = ui self.representController = representController
super.init() super.init()
} }
deinit {
representController = nil
}
@objc public func presentIntelligentsButton(_ call: CAPPluginCall) { @objc public func presentIntelligentsButton(_ call: CAPPluginCall) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.ui.presentIntelligentsButton() self.representController?.presentIntelligentsButton()
print("!!!!!!!!!!!!present")
call.resolve() call.resolve()
} }
} }
@objc public func dismissIntelligentsButton(_ call: CAPPluginCall) { @objc public func dismissIntelligentsButton(_ call: CAPPluginCall) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.ui.dismissIntelligentsButton() self.representController?.dismissIntelligentsButton()
print("!!!!!!!!!!!!dismiss")
call.resolve() call.resolve()
} }
} }
@@ -9,28 +9,28 @@ import UIKit
@objc @objc
class RootViewController: UINavigationController { class RootViewController: UINavigationController {
override init(rootViewController: UIViewController) { override init(rootViewController _: UIViewController) {
fatalError() // "you are not allowed to call this" fatalError() // "you are not allowed to call this"
} }
override init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?) { override init(navigationBarClass _: AnyClass?, toolbarClass _: AnyClass?) {
fatalError() // "you are not allowed to call this" fatalError() // "you are not allowed to call this"
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) super.init(coder: aDecoder)
commitInit() commitInit()
} }
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { override init(nibName _: String?, bundle _: Bundle?) {
fatalError() // "you are not allowed to call this" fatalError() // "you are not allowed to call this"
} }
func commitInit() { func commitInit() {
assert(viewControllers.isEmpty) assert(viewControllers.isEmpty)
viewControllers = [AFFiNEViewController()] viewControllers = [AFFiNEViewController()]
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground view.backgroundColor = .systemBackground
+1 -1
View File
@@ -44,4 +44,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698 PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2