mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 09:52:49 +08:00
WIP: 完成 AI 框架嵌入文档的动画
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// TextEditView.swift
|
||||
// PlainTextEditView.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 2024/11/18.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
guard let url = getServerUrl(urlString) else { return [:] }
|
||||
if let cookies = jar.cookies(for: url) {
|
||||
for cookie in cookies {
|
||||
cookiesMap[cookie.name] = cookie.value
|
||||
}
|
||||
for cookie in cookies {
|
||||
cookiesMap[cookie.name] = cookie.value
|
||||
}
|
||||
}
|
||||
return cookiesMap
|
||||
}
|
||||
|
||||
|
||||
private func isUrlSanitized(_ urlString: String) -> Bool {
|
||||
return urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
|
||||
urlString.hasPrefix("http://") || urlString.hasPrefix("https://")
|
||||
}
|
||||
|
||||
|
||||
public func getServerUrl(_ urlString: String) -> URL? {
|
||||
let validUrlString = (isUrlSanitized(urlString)) ? urlString : "http://\(urlString)"
|
||||
let validUrlString = isUrlSanitized(urlString) ? urlString : "http://\(urlString)"
|
||||
|
||||
guard let url = URL(string: validUrlString) else {
|
||||
return nil
|
||||
}
|
||||
guard let url = URL(string: validUrlString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return url
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import Foundation
|
||||
|
||||
@objc(CookiePlugin)
|
||||
public class CookiePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "CookiePlugin"
|
||||
public let jsName = "Cookie"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise)
|
||||
CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
|
||||
let cookieManager = CookieManager()
|
||||
|
||||
@objc public func getCookies(_ call: CAPPluginCall) {
|
||||
guard let url = call.getString("url") else {
|
||||
return call.resolve([:])
|
||||
}
|
||||
|
||||
|
||||
call.resolve(cookieManager.getCookies(url))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,4 +44,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
Reference in New Issue
Block a user