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

View File

@@ -1,28 +1,92 @@
import UIKit
import Capacitor
import Intelligents
import UIKit
class AFFiNEViewController: CAPBridgeViewController {
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
self.navigationController?.navigationBar.isHidden = true
installIntelligentsButton()
navigationController?.navigationBar.isHidden = true
extendedLayoutIncludesOpaqueBars = false
edgesForExtendedLayout = []
let intelligentsButton = installIntelligentsButton()
intelligentsButton.delegate = self
dismissIntelligentsButton()
}
override func capacitorDidLoad() {
bridge?.registerPluginInstance(CookiePlugin())
bridge?.registerPluginInstance(HashcashPlugin())
bridge?.registerPluginInstance(IntelligentsPlugin(ui: self))
let plugins: [CAPPlugin] = [
CookiePlugin(),
HashcashPlugin(),
IntelligentsPlugin(representController: self),
]
plugins.forEach { bridge?.registerPluginInstance($0) }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
self.dismissIntelligentsButton()
}
override func viewDidDisappear(_ animated: Bool) {
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)
}
}

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

@@ -18,20 +18,20 @@ extension UIView {
}
return nil
}
#if DEBUG
func debugFrame() {
layer.borderWidth = 1
layer.borderColor = [
UIColor.red,
.green,
.blue,
.yellow,
.cyan,
.magenta,
.orange,
].map(\.cgColor).randomElement()
subviews.forEach { $0.debugFrame() }
}
#endif
#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
}

View File

@@ -7,7 +7,7 @@
import UIKit
extension UIViewController {
public extension UIViewController {
func presentIntoCurrentContext(withTargetController targetController: UIViewController, animated: Bool = true) {
if let nav = self as? UINavigationController {
nav.pushViewController(targetController, animated: animated)

View File

@@ -44,6 +44,7 @@ public extension UIViewController {
button.alpha = 0
button.isHidden = false
button.setNeedsLayout()
button.stopProgress()
view.layoutIfNeeded()
UIView.animate(
@@ -63,6 +64,7 @@ public extension UIViewController {
guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)")
button.stopProgress()
button.setNeedsLayout()
view.layoutIfNeeded()
UIView.animate(

View File

@@ -0,0 +1,12 @@
//
// IntelligentsButton+Delegate.swift
// Intelligents
//
// Created by on 2024/11/21.
//
import Foundation
public protocol IntelligentsButtonDelegate: AnyObject {
func onIntelligentsButtonTapped(_ button: IntelligentsButton)
}

View File

@@ -11,6 +11,11 @@ import UIKit
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)
@@ -38,9 +43,11 @@ public class IntelligentsButton: UIView {
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue),
].forEach { $0.isActive = true }
// layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
// layer.shadowOffset = CGSize(width: 0, height: 0)
// layer.shadowRadius = 8
addSubview(activityIndicator)
[
activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
].forEach { $0.isActive = true }
clipsToBounds = true
layer.borderWidth = 2
@@ -49,6 +56,8 @@ public class IntelligentsButton: UIView {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
addGestureRecognizer(tap)
isUserInteractionEnabled = true
stopProgress()
}
@available(*, unavailable)
@@ -56,18 +65,26 @@ public class IntelligentsButton: UIView {
fatalError()
}
deinit {
delegate = nil
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.width / 2
}
@objc func tapped() {
guard let controller = parentViewController else {
assertionFailure()
return
}
let targetController = IntelligentsChatController()
controller.presentIntoCurrentContext(withTargetController: targetController)
delegate?.onIntelligentsButtonTapped(self)
}
public func beginProgress() {
activityIndicator.startAnimating()
activityIndicator.isHidden = false
}
public func stopProgress() {
activityIndicator.stopAnimating()
activityIndicator.isHidden = true
}
}

View File

@@ -25,7 +25,7 @@ class AttachmentBannerView: UIScrollView {
height: attachmentSize
)
}
init() {
super.init(frame: .zero)
@@ -33,7 +33,7 @@ class AttachmentBannerView: UIScrollView {
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
rebuildViews()
}
@@ -41,7 +41,7 @@ class AttachmentBannerView: UIScrollView {
required init?(coder _: NSCoder) {
fatalError()
}
func rebuildViews() {
subviews.forEach { $0.removeFromSuperview() }
for (index, attachment) in attachments.enumerated() {

View File

@@ -1,24 +1,22 @@
//
// File.swift
// InputEditView+ViewModel.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
import Combine
import UIKit
extension InputEditView {
class ViewModel: ObservableObject {
var cancellables: Set<AnyCancellable> = []
@Published var text: String = ""
@Published var attachments: [UIImage] = []
init() {
}
init() {}
deinit {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
@@ -31,7 +29,7 @@ extension InputEditView.ViewModel: Hashable, Equatable {
hasher.combine(text)
hasher.combine(attachments)
}
static func == (lhs: InputEditView.ViewModel, rhs: InputEditView.ViewModel) -> Bool {
lhs.hashValue == rhs.hashValue
}

View File

@@ -5,8 +5,8 @@
// Created by on 2024/11/18.
//
import UIKit
import Combine
import UIKit
class InputEditView: UIView, UITextViewDelegate {
let mainStack = UIStackView()
@@ -14,14 +14,14 @@ class InputEditView: UIView, UITextViewDelegate {
let textEditor = PlainTextEditView()
let placeholderLabel = UILabel()
let controlBanner = TextEditControlBanner()
let viewModel = ViewModel()
var placeholderText: String = "" {
didSet {
placeholderLabel.text = placeholderText
}
}
init() {
super.init(frame: .zero)
@@ -51,7 +51,7 @@ class InputEditView: UIView, UITextViewDelegate {
$0.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor),
].forEach { $0.isActive = true }
}
textEditor.addSubview(placeholderLabel)
placeholderLabel.textColor = .label.withAlphaComponent(0.25)
placeholderLabel.font = textEditor.font
@@ -61,7 +61,7 @@ class InputEditView: UIView, UITextViewDelegate {
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
@@ -76,26 +76,26 @@ class InputEditView: UIView, UITextViewDelegate {
required init?(coder _: NSCoder) {
fatalError()
}
func textViewDidChange(_ textView: UITextView) {
viewModel.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
func textViewDidBeginEditing(_: UITextView) {
updatePlaceholderVisibility()
}
func textViewDidEndEditing(_ textView: UITextView) {
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,
@@ -109,7 +109,7 @@ class InputEditView: UIView, UITextViewDelegate {
if attachmentsEditor.attachments != viewModel.attachments {
attachmentsEditor.attachments = viewModel.attachments
}
self.parentViewController?.view.layoutIfNeeded()
parentViewController?.view.layoutIfNeeded()
}
}
}

View File

@@ -1,5 +1,5 @@
//
// TextEditView.swift
// PlainTextEditView.swift
// Intelligents
//
// Created by on 2024/11/18.

View File

@@ -45,12 +45,12 @@ class TextEditControlBanner: UIStackView {
$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
}

View File

@@ -20,7 +20,6 @@ extension IntelligentsChatController {
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
@@ -39,10 +38,10 @@ private extension IntelligentsChatController.InputBox {
func setupLayout() {
addSubview(backgroundView)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(editor)
editor.translatesAutoresizingMaskIntoConstraints = false
let inset: CGFloat = 16
[

View File

@@ -7,12 +7,12 @@
import UIKit
class IntelligentsChatController: UIViewController {
public class IntelligentsChatController: UIViewController {
let header = Header()
let inputBox = InputBox()
let tableView = ChatTableView()
override var title: String? {
override public var title: String? {
set {
super.title = newValue
header.titleLabel.text = newValue
@@ -22,7 +22,7 @@ class IntelligentsChatController: UIViewController {
}
}
init() {
public init() {
super.init(nibName: nil, bundle: nil)
title = "Chat with AI".localized()
}
@@ -32,7 +32,7 @@ class IntelligentsChatController: UIViewController {
fatalError()
}
override func viewDidLoad() {
override public func viewDidLoad() {
super.viewDidLoad()
assert(navigationController != nil)
view.backgroundColor = .secondarySystemBackground

View File

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

View File

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

View File

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

View File

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

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

@@ -3,105 +3,105 @@ import CryptoSwift
@objc(HashcashPlugin)
public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "HashcashPlugin"
public let jsName = "Hashcash"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise)
]
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": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()])
}
@objc func hash(_ call: CAPPluginCall) {
DispatchQueue.global(qos: .default).async {
let challenge = call.getString("challenge") ?? ""
let bits = call.getInt("bits") ?? 20
call.resolve(["value": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()])
}
}
}
let SALT_LENGTH = 16
struct Stamp {
let version: String
let claim: UInt32
let ts: String
let resource: String
let ext: String
let rand: String
let counter: String
func checkExpiration() -> Bool {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
guard let date = dateFormatter.date(from: ts) else { return false }
return Date().addingTimeInterval(5 * 60) <= date
let version: String
let claim: UInt32
let ts: String
let resource: String
let ext: String
let rand: String
let counter: String
func checkExpiration() -> Bool {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
guard let date = dateFormatter.date(from: ts) else { return false }
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 {
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 format() -> String {
return "\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)"
}
static func mint(resource: String, bits: UInt32? = nil) -> Stamp {
let version = "1"
let now = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
let ts = dateFormatter.string(from: now)
let bits = bits ?? 20
let rand = String((0..<SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! })
let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"
let hexDigits = Int(ceil(Float(bits) / 4.0))
let zeros = String(repeating: "0", count: hexDigits)
var counter = 0
var counterHex = ""
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)
}
func format() -> String {
"\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)"
}
static func mint(resource: String, bits: UInt32? = nil) -> Stamp {
let version = "1"
let now = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmss"
let ts = dateFormatter.string(from: now)
let bits = bits ?? 20
let rand = String((0 ..< SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! })
let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"
let hexDigits = Int(ceil(Float(bits) / 4.0))
let zeros = String(repeating: "0", count: hexDigits)
var counter = 0
var counterHex = ""
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)
}
}
extension Stamp {
init?(from string: String) throws {
let parts = string.split(separator: ":")
guard parts.count == 7 else {
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])
init?(from string: String) throws {
let parts = string.split(separator: ":")
guard parts.count == 7 else {
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"])
}
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])
}
}

View File

@@ -1,5 +1,5 @@
import Foundation
import Capacitor
import Foundation
@objc(IntelligentsPlugin)
public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
@@ -7,27 +7,29 @@ public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
public let jsName = "Intelligents"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "presentIntelligentsButton", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise)
CAPPluginMethod(name: "dismissIntelligentsButton", returnType: CAPPluginReturnPromise),
]
public let ui: UIViewController
init(ui: UIViewController) {
self.ui = ui
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.ui.presentIntelligentsButton()
print("!!!!!!!!!!!!present")
self.representController?.presentIntelligentsButton()
call.resolve()
}
}
@objc public func dismissIntelligentsButton(_ call: CAPPluginCall) {
DispatchQueue.main.async {
self.ui.dismissIntelligentsButton()
print("!!!!!!!!!!!!dismiss")
self.representController?.dismissIntelligentsButton()
call.resolve()
}
}

View File

@@ -9,28 +9,28 @@ import UIKit
@objc
class RootViewController: UINavigationController {
override init(rootViewController: UIViewController) {
override init(rootViewController _: UIViewController) {
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"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commitInit()
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
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

@@ -44,4 +44,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2