|
|
|
@@ -5,8 +5,6 @@
|
|
|
|
|
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
//#if canImport(UIKit)
|
|
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
@@ -48,6 +46,59 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
|
|
|
|
|
let defaultConsoleSize = CGSize(width: 228, height: 142)
|
|
|
|
|
|
|
|
|
|
lazy var borderView = UIView()
|
|
|
|
|
|
|
|
|
|
var lumaWidthAnchor: NSLayoutConstraint!
|
|
|
|
|
var lumaHeightAnchor: NSLayoutConstraint!
|
|
|
|
|
|
|
|
|
|
lazy var lumaView: LumaView = {
|
|
|
|
|
let lumaView = LumaView()
|
|
|
|
|
lumaView.foregroundView.backgroundColor = .black
|
|
|
|
|
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
|
|
|
|
|
|
|
|
|
consoleView.addSubview(lumaView)
|
|
|
|
|
|
|
|
|
|
lumaView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
|
|
lumaWidthAnchor = lumaView.widthAnchor.constraint(equalTo: consoleView.widthAnchor)
|
|
|
|
|
lumaHeightAnchor = lumaView.heightAnchor.constraint(equalToConstant: consoleView.frame.size.height)
|
|
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
|
lumaWidthAnchor,
|
|
|
|
|
lumaHeightAnchor,
|
|
|
|
|
lumaView.centerXAnchor.constraint(equalTo: consoleView.centerXAnchor),
|
|
|
|
|
lumaView.centerYAnchor.constraint(equalTo: consoleView.centerYAnchor)
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return lumaView
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
lazy var hideButton: UIButton = {
|
|
|
|
|
let button = UIButton()
|
|
|
|
|
|
|
|
|
|
button.addAction(UIAction(handler: { [self] _ in
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
|
|
|
|
consoleView.center = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.dropLast())
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
grabberMode = false
|
|
|
|
|
}), for: .touchUpInside)
|
|
|
|
|
|
|
|
|
|
consoleView.addSubview(button)
|
|
|
|
|
|
|
|
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
|
button.widthAnchor.constraint(equalTo: consoleView.widthAnchor),
|
|
|
|
|
button.heightAnchor.constraint(equalTo: consoleView.heightAnchor),
|
|
|
|
|
button.centerXAnchor.constraint(equalTo: consoleView.centerXAnchor),
|
|
|
|
|
button.centerYAnchor.constraint(equalTo: consoleView.centerYAnchor)
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
button.isHidden = true
|
|
|
|
|
|
|
|
|
|
return button
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
/// The fixed size of the console view.
|
|
|
|
|
lazy var consoleSize = defaultConsoleSize {
|
|
|
|
|
didSet {
|
|
|
|
@@ -86,11 +137,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
var consoleWindow: ConsoleWindow?
|
|
|
|
|
|
|
|
|
|
// The console needs a parent view controller in order to display context menus.
|
|
|
|
|
let viewController = UIViewController()
|
|
|
|
|
lazy var viewController = UIViewController()
|
|
|
|
|
lazy var consoleView = viewController.view!
|
|
|
|
|
|
|
|
|
|
/// Text view that displays printed items.
|
|
|
|
|
let consoleTextView = InvertedTextView()
|
|
|
|
|
lazy var consoleTextView = InvertedTextView()
|
|
|
|
|
|
|
|
|
|
/// Button that reveals menu.
|
|
|
|
|
lazy var menuButton = UIButton()
|
|
|
|
@@ -99,27 +150,92 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
var scrollLocked = true
|
|
|
|
|
|
|
|
|
|
/// Feedback generator for the long press action.
|
|
|
|
|
let feedbackGenerator = UISelectionFeedbackGenerator()
|
|
|
|
|
lazy var feedbackGenerator = UISelectionFeedbackGenerator()
|
|
|
|
|
|
|
|
|
|
lazy var panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(consolePiPPanner(recognizer:)))
|
|
|
|
|
lazy var longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(recognizer:)))
|
|
|
|
|
|
|
|
|
|
/// Gesture endpoints. Each point represents a corner of the screen. TODO: Handle screen rotation.
|
|
|
|
|
var possibleEndpoints: [CGPoint] {
|
|
|
|
|
|
|
|
|
|
if consoleSize.width < UIScreen.portraitSize.width - 112 {
|
|
|
|
|
return [CGPoint(x: consoleSize.width / 2 + 12,
|
|
|
|
|
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
|
|
|
|
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
|
|
|
|
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
|
|
|
|
CGPoint(x: consoleSize.width / 2 + 12,
|
|
|
|
|
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
|
|
|
|
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
|
|
|
|
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
|
|
|
|
|
|
|
|
|
// Four endpoints, one for each corner.
|
|
|
|
|
var endpoints = [
|
|
|
|
|
|
|
|
|
|
// Top endpoints.
|
|
|
|
|
CGPoint(x: consoleSize.width / 2 + 12,
|
|
|
|
|
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
|
|
|
|
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
|
|
|
|
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
|
|
|
|
|
|
|
|
|
// Bottom endpoints.
|
|
|
|
|
CGPoint(x: consoleSize.width / 2 + 12,
|
|
|
|
|
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
|
|
|
|
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
|
|
|
|
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
|
|
|
|
|
|
|
|
|
if consoleView.frame.minX <= 0 {
|
|
|
|
|
|
|
|
|
|
// Left edge endpoints.
|
|
|
|
|
endpoints = [endpoints[0], endpoints[2]]
|
|
|
|
|
|
|
|
|
|
// Left edge hiding endpoints.
|
|
|
|
|
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
|
|
|
|
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
|
|
|
|
y: endpoints[0].y))
|
|
|
|
|
} else {
|
|
|
|
|
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
|
|
|
|
y: endpoints[1].y))
|
|
|
|
|
}
|
|
|
|
|
} else if consoleView.frame.maxX >= UIScreen.portraitSize.width {
|
|
|
|
|
|
|
|
|
|
// Right edge endpoints.
|
|
|
|
|
endpoints = [endpoints[1], endpoints[3]]
|
|
|
|
|
|
|
|
|
|
// Right edge hiding endpoints.
|
|
|
|
|
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
|
|
|
|
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
|
|
|
|
y: endpoints[0].y))
|
|
|
|
|
} else {
|
|
|
|
|
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
|
|
|
|
y: endpoints[1].y))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return endpoints
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
return [CGPoint(x: UIScreen.portraitSize.width / 2,
|
|
|
|
|
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
|
|
|
|
CGPoint(x: UIScreen.portraitSize.width / 2,
|
|
|
|
|
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
|
|
|
|
|
|
|
|
|
// Two endpoints, one for the top, one for the bottom..
|
|
|
|
|
var endpoints = [CGPoint(x: UIScreen.portraitSize.width / 2,
|
|
|
|
|
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
|
|
|
|
CGPoint(x: UIScreen.portraitSize.width / 2,
|
|
|
|
|
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
|
|
|
|
|
|
|
|
|
if consoleView.frame.minX <= 0 {
|
|
|
|
|
|
|
|
|
|
// Left edge hiding endpoints.
|
|
|
|
|
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
|
|
|
|
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
|
|
|
|
y: endpoints[0].y))
|
|
|
|
|
} else {
|
|
|
|
|
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
|
|
|
|
y: endpoints[1].y))
|
|
|
|
|
}
|
|
|
|
|
} else if consoleView.frame.maxX >= UIScreen.portraitSize.width {
|
|
|
|
|
|
|
|
|
|
// Right edge hiding endpoints.
|
|
|
|
|
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
|
|
|
|
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
|
|
|
|
y: endpoints[0].y))
|
|
|
|
|
} else {
|
|
|
|
|
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
|
|
|
|
y: endpoints[1].y))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return endpoints
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -129,21 +245,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
consoleSize = CGSize(width: UserDefaults.standard.object(forKey: "LocalConsole_Width") as? CGFloat ?? consoleSize.width,
|
|
|
|
|
height: UserDefaults.standard.object(forKey: "LocalConsole_Height") as? CGFloat ?? consoleSize.height)
|
|
|
|
|
|
|
|
|
|
consoleView.backgroundColor = .black
|
|
|
|
|
|
|
|
|
|
consoleView.layer.shadowRadius = 16
|
|
|
|
|
consoleView.layer.shadowOpacity = 0.5
|
|
|
|
|
consoleView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
|
|
|
|
consoleView.center = possibleEndpoints.first!
|
|
|
|
|
consoleView.alpha = 0
|
|
|
|
|
|
|
|
|
|
consoleView.layer.cornerRadius = 22
|
|
|
|
|
consoleView.layer.cornerCurve = .continuous
|
|
|
|
|
|
|
|
|
|
let borderView = UIView()
|
|
|
|
|
let _ = lumaView
|
|
|
|
|
|
|
|
|
|
borderView.frame = CGRect(x: -1, y: -1,
|
|
|
|
|
width: consoleSize.width + 2,
|
|
|
|
|
height: consoleSize.height + 2)
|
|
|
|
|
width: consoleSize.width + 2,
|
|
|
|
|
height: consoleSize.height + 2)
|
|
|
|
|
borderView.layer.borderWidth = 1
|
|
|
|
|
borderView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
|
|
|
|
|
borderView.layer.cornerRadius = consoleView.layer.cornerRadius + 1
|
|
|
|
@@ -199,7 +314,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
circle.isUserInteractionEnabled = false
|
|
|
|
|
menuButton.addSubview(circle)
|
|
|
|
|
|
|
|
|
|
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17)))
|
|
|
|
|
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis",
|
|
|
|
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .medium)))
|
|
|
|
|
ellipsisImage.frame.size = circle.bounds.size
|
|
|
|
|
ellipsisImage.contentMode = .center
|
|
|
|
|
circle.addSubview(ellipsisImage)
|
|
|
|
@@ -209,6 +325,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
menuButton.showsMenuAsPrimaryAction = true
|
|
|
|
|
consoleView.addSubview(menuButton)
|
|
|
|
|
|
|
|
|
|
let _ = hideButton
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
|
|
|
}
|
|
|
|
@@ -217,6 +335,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
func configureWindow() {
|
|
|
|
|
var windowSceneFound = false
|
|
|
|
|
|
|
|
|
|
// Update console cached based on last-cached origin.
|
|
|
|
|
func updateConsoleOrigin() {
|
|
|
|
|
let cachedConsolePosition = CGPoint(x: UserDefaults.standard.object(forKey: "LocalConsole_X") as? CGFloat ?? possibleEndpoints.first!.x,
|
|
|
|
|
y: UserDefaults.standard.object(forKey: "LocalConsole_Y") as? CGFloat ?? possibleEndpoints.first!.y)
|
|
|
|
|
|
|
|
|
|
consoleView.center = cachedConsolePosition // Update console center so possibleEndpoints are calculated correctly.
|
|
|
|
|
consoleView.center = nearestTargetTo(cachedConsolePosition, possibleTargets: possibleEndpoints)
|
|
|
|
|
|
|
|
|
|
if consoleView.center.x < 0 || consoleView.center.x > UIScreen.portraitSize.width {
|
|
|
|
|
grabberMode = true
|
|
|
|
|
scrollLocked = !grabberMode
|
|
|
|
|
|
|
|
|
|
consoleView.layer.removeAllAnimations()
|
|
|
|
|
lumaView.layer.removeAllAnimations()
|
|
|
|
|
menuButton.layer.removeAllAnimations()
|
|
|
|
|
consoleTextView.layer.removeAllAnimations()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure console window.
|
|
|
|
|
func fetchWindowScene() {
|
|
|
|
|
let windowScene = UIApplication.shared
|
|
|
|
@@ -235,11 +372,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
consoleWindow?.addSubview(consoleView)
|
|
|
|
|
|
|
|
|
|
UIWindow.swizzleStatusBarAppearanceOverride
|
|
|
|
|
|
|
|
|
|
updateConsoleOrigin()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetchWindowScene()
|
|
|
|
|
|
|
|
|
|
/// Ensures the window is configured (i.e. scene has been found). If not, delay and wait for a scene to prepare itself, then try again.
|
|
|
|
|
for i in 1...10 {
|
|
|
|
|
|
|
|
|
@@ -303,7 +440,61 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var _hasRelayedOffsetChange = false
|
|
|
|
|
var grabberMode: Bool = false {
|
|
|
|
|
|
|
|
|
|
didSet {
|
|
|
|
|
guard oldValue != grabberMode else { return }
|
|
|
|
|
|
|
|
|
|
if grabberMode {
|
|
|
|
|
|
|
|
|
|
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
|
|
|
|
lumaHeightAnchor.constant = consoleView.frame.size.height
|
|
|
|
|
consoleView.layoutIfNeeded()
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleTextView.alpha = 0
|
|
|
|
|
menuButton.alpha = 0
|
|
|
|
|
borderView.alpha = 0
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) { [self] in
|
|
|
|
|
lumaView.foregroundView.alpha = 0
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
lumaWidthAnchor.constant = -34
|
|
|
|
|
lumaHeightAnchor.constant = 96
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
|
|
|
|
lumaView.layer.cornerRadius = 8
|
|
|
|
|
consoleView.layoutIfNeeded()
|
|
|
|
|
}.startAnimation(afterDelay: 0.06)
|
|
|
|
|
|
|
|
|
|
consoleTextView.isUserInteractionEnabled = false
|
|
|
|
|
hideButton.isHidden = false
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
lumaHeightAnchor.constant = consoleView.frame.size.height
|
|
|
|
|
lumaWidthAnchor.constant = 0
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleView.layoutIfNeeded()
|
|
|
|
|
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleTextView.alpha = 1
|
|
|
|
|
menuButton.alpha = 1
|
|
|
|
|
borderView.alpha = 1
|
|
|
|
|
}.startAnimation(afterDelay: 0.2)
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.65, dampingRatio: 1) { [self] in
|
|
|
|
|
lumaView.foregroundView.alpha = 1
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
consoleTextView.isUserInteractionEnabled = true
|
|
|
|
|
hideButton.isHidden = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Print items to the console view.
|
|
|
|
|
public func print(_ items: Any) {
|
|
|
|
@@ -326,18 +517,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
|
|
var temporaryKeyboardHeightValueTracker: CGFloat?
|
|
|
|
|
|
|
|
|
|
// MARK: Handle keyboard show/hide.
|
|
|
|
|
private var keyboardHeight: CGFloat? = nil {
|
|
|
|
|
didSet {
|
|
|
|
|
|
|
|
|
|
temporaryKeyboardHeightValueTracker = oldValue
|
|
|
|
|
|
|
|
|
|
if consoleView.center != possibleEndpoints[0] && consoleView.center != possibleEndpoints[1] {
|
|
|
|
|
let nearestTargetPosition = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.suffix(2))
|
|
|
|
|
|
|
|
|
|
Swift.print(possibleEndpoints.suffix(2))
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) {
|
|
|
|
|
self.consoleView.center = nearestTargetPosition
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
temporaryKeyboardHeightValueTracker = keyboardHeight
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -394,6 +592,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
dynamicReportTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
|
|
|
|
|
var _currentText = currentText
|
|
|
|
|
|
|
|
|
|
// To optimize performance, only scan the last 2500 characters of text for system report changes.
|
|
|
|
|
let range: NSRange = {
|
|
|
|
|
if _currentText.count <= 2500 {
|
|
|
|
|
return NSMakeRange(0, _currentText.count)
|
|
|
|
@@ -413,6 +612,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
if currentText != _currentText {
|
|
|
|
|
currentText = _currentText
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// Invalidate the timer if there is no longer anything to update.
|
|
|
|
|
timer.invalidate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -453,21 +654,17 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func toggleLock() {
|
|
|
|
|
scrollLocked.toggle()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func commitTextChanges(requestMenuUpdate menuUpdateRequested: Bool) {
|
|
|
|
|
|
|
|
|
|
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - consoleTextView.bounds.size.height - 20
|
|
|
|
|
|| _hasRelayedOffsetChange == false {
|
|
|
|
|
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - consoleTextView.bounds.size.height - 20 {
|
|
|
|
|
|
|
|
|
|
// Weird, weird fix that makes the scroll view bottom pinning system work.
|
|
|
|
|
consoleTextView.isScrollEnabled.toggle()
|
|
|
|
|
consoleTextView.isScrollEnabled.toggle()
|
|
|
|
|
|
|
|
|
|
consoleTextView.pendingOffsetChange = true
|
|
|
|
|
_hasRelayedOffsetChange = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
consoleTextView.text = currentText
|
|
|
|
|
|
|
|
|
|
setAttributedText(currentText)
|
|
|
|
|
|
|
|
|
|
if menuUpdateRequested {
|
|
|
|
@@ -492,22 +689,22 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
func makeMenu() -> UIMenu {
|
|
|
|
|
|
|
|
|
|
let copy = UIAction(title: "Copy",
|
|
|
|
|
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
|
|
|
|
|
self.copy()
|
|
|
|
|
})
|
|
|
|
|
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
|
|
|
|
|
self.copy()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let resize = UIAction(title: "Resize Console",
|
|
|
|
|
image: UIImage(systemName: "arrow.left.and.right.square"), handler: { _ in
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
|
|
|
ResizeController.shared.isActive.toggle()
|
|
|
|
|
ResizeController.shared.platterView.reveal()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
|
|
|
ResizeController.shared.isActive.toggle()
|
|
|
|
|
ResizeController.shared.platterView.reveal()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let clear = UIAction(title: "Clear Console",
|
|
|
|
|
image: UIImage(systemName: "xmark.square"), handler: { _ in
|
|
|
|
|
self.clear()
|
|
|
|
|
})
|
|
|
|
|
self.clear()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let consoleActions = UIMenu(title: "", options: .displayInline, children: [clear, resize])
|
|
|
|
|
|
|
|
|
@@ -518,14 +715,14 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
|
|
|
|
|
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
|
|
|
|
|
image: UIImage(systemName: frameSymbol), handler: { _ in
|
|
|
|
|
self.debugBordersEnabled.toggle()
|
|
|
|
|
self.menuButton.menu = self.makeMenu()
|
|
|
|
|
})
|
|
|
|
|
self.debugBordersEnabled.toggle()
|
|
|
|
|
self.menuButton.menu = self.makeMenu()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let systemReport = UIAction(title: "System Report",
|
|
|
|
|
image: UIImage(systemName: "cpu"), handler: { _ in
|
|
|
|
|
self.systemReport()
|
|
|
|
|
})
|
|
|
|
|
image: UIImage(systemName: "cpu"), handler: { _ in
|
|
|
|
|
self.systemReport()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Show the right glyph for the current device being used.
|
|
|
|
|
let deviceSymbol: String = {
|
|
|
|
@@ -550,28 +747,35 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
let displayReport = UIAction(title: "Display Report",
|
|
|
|
|
image: UIImage(systemName: deviceSymbol), handler: { _ in
|
|
|
|
|
self.displayReport()
|
|
|
|
|
})
|
|
|
|
|
image: UIImage(systemName: deviceSymbol), handler: { _ in
|
|
|
|
|
self.displayReport()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let respring = UIAction(title: "Restart Spring" + "Board",
|
|
|
|
|
image: UIImage(systemName: "apps.iphone"), handler: { _ in
|
|
|
|
|
guard let window = UIApplication.shared.windows.first else { return }
|
|
|
|
|
|
|
|
|
|
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat
|
|
|
|
|
window.layer.masksToBounds = true
|
|
|
|
|
|
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
|
|
|
|
window.transform = .init(scaleX: 0.96, y: 0.96)
|
|
|
|
|
window.alpha = 0
|
|
|
|
|
}
|
|
|
|
|
animator.addCompletion { _ in
|
|
|
|
|
while true {
|
|
|
|
|
window.snapshotView(afterScreenUpdates: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
animator.startAnimation()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
guard let window = UIApplication.shared.windows.first else { return }
|
|
|
|
|
|
|
|
|
|
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat
|
|
|
|
|
window.layer.masksToBounds = true
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
|
|
|
|
window.transform = .init(scaleX: 0.96, y: 0.96)
|
|
|
|
|
window.alpha = 0
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
// Concurrently run these snapshots to decrease the time to crash.
|
|
|
|
|
for _ in 0...1000 {
|
|
|
|
|
DispatchQueue.global(qos: .default).async {
|
|
|
|
|
|
|
|
|
|
// This will cause jetsam to terminate SpringBoard.
|
|
|
|
|
while true {
|
|
|
|
|
window.snapshotView(afterScreenUpdates: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let debugActions = UIMenu(title: "", options: .displayInline,
|
|
|
|
|
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
|
|
|
|
|
children: [viewFrames, systemReport, displayReport, respring])])
|
|
|
|
@@ -591,6 +795,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
|
|
|
|
|
switch recognizer.state {
|
|
|
|
|
case .began:
|
|
|
|
|
|
|
|
|
|
guard !grabberMode else { return }
|
|
|
|
|
|
|
|
|
|
feedbackGenerator.selectionChanged()
|
|
|
|
|
|
|
|
|
|
scrollLocked = false
|
|
|
|
@@ -598,17 +805,21 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleView.transform = .init(scaleX: 1.04, y: 1.04)
|
|
|
|
|
consoleTextView.alpha = 0.5
|
|
|
|
|
menuButton.alpha = 0.5
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
case .cancelled, .ended:
|
|
|
|
|
|
|
|
|
|
scrollLocked = true
|
|
|
|
|
if !grabberMode { scrollLocked = true }
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.5) { [self] in
|
|
|
|
|
consoleView.transform = .init(scaleX: 1, y: 1)
|
|
|
|
|
consoleView.transform = .identity
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleTextView.alpha = 1
|
|
|
|
|
if !grabberMode {
|
|
|
|
|
consoleTextView.alpha = 1
|
|
|
|
|
menuButton.alpha = 1
|
|
|
|
|
}
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
@@ -628,8 +839,16 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
switch recognizer.state {
|
|
|
|
|
case .changed:
|
|
|
|
|
|
|
|
|
|
consoleView.center.x = initialViewLocation.x + translation.x
|
|
|
|
|
consoleView.center.y = initialViewLocation.y + translation.y
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.175, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleView.center = CGPoint(x: initialViewLocation.x + translation.x,
|
|
|
|
|
y: initialViewLocation.y + translation.y)
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
if consoleView.frame.maxX > 30 && consoleView.frame.minX < UIScreen.portraitSize.width - 30 {
|
|
|
|
|
grabberMode = false
|
|
|
|
|
} else {
|
|
|
|
|
grabberMode = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case .ended, .cancelled:
|
|
|
|
|
|
|
|
|
@@ -648,27 +867,31 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
dy: relativeVelocity(forVelocity: velocity.y, from: consoleView.center.y, to: nearestTargetPosition.y)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let timingParameters = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)
|
|
|
|
|
let timingParameters = UISpringTimingParameters(damping: 0.85, response: 0.45, initialVelocity: relativeInitialVelocity)
|
|
|
|
|
let positionAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
|
|
|
|
|
positionAnimator.addAnimations { [self] in
|
|
|
|
|
consoleView.center = nearestTargetPosition
|
|
|
|
|
}
|
|
|
|
|
positionAnimator.startAnimation()
|
|
|
|
|
|
|
|
|
|
UserDefaults.standard.set(nearestTargetPosition.x, forKey: "LocalConsole_X")
|
|
|
|
|
UserDefaults.standard.set(nearestTargetPosition.y, forKey: "LocalConsole_Y")
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
|
|
|
self.grabberMode = nearestTargetPosition.x < 0 || nearestTargetPosition.x > UIScreen.portraitSize.width
|
|
|
|
|
self.scrollLocked = !self.grabberMode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Animate touch down.
|
|
|
|
|
func consolePiPTouchDown() {
|
|
|
|
|
UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) { [self] in
|
|
|
|
|
consoleView.transform = .init(scaleX: 0.96, y: 0.96)
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
guard !grabberMode else { return }
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
|
|
|
|
if !scrollLocked {
|
|
|
|
|
consoleView.backgroundColor = #colorLiteral(red: 0.1331297589, green: 0.1331297589, blue: 0.1331297589, alpha: 1)
|
|
|
|
|
}
|
|
|
|
|
UIViewPropertyAnimator(duration: 1.25, dampingRatio: 0.5) { [self] in
|
|
|
|
|
consoleView.transform = .init(scaleX: 0.95, y: 0.95)
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -679,11 +902,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleTextView.alpha = 1
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
|
|
|
|
|
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) { [self] in
|
|
|
|
|
consoleView.backgroundColor = .black
|
|
|
|
|
if !grabberMode {
|
|
|
|
|
consoleTextView.alpha = 1
|
|
|
|
|
menuButton.alpha = 1
|
|
|
|
|
}
|
|
|
|
|
}.startAnimation()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -696,15 +918,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
switch recognizer.state {
|
|
|
|
|
case .began:
|
|
|
|
|
consolePiPTouchDown()
|
|
|
|
|
case .cancelled:
|
|
|
|
|
consolePiPTouchUp()
|
|
|
|
|
case .changed:
|
|
|
|
|
break
|
|
|
|
|
case .ended:
|
|
|
|
|
consolePiPTouchUp()
|
|
|
|
|
case .failed:
|
|
|
|
|
consolePiPTouchUp()
|
|
|
|
|
case .possible:
|
|
|
|
|
case .ended, .cancelled, .possible, .failed:
|
|
|
|
|
consolePiPTouchUp()
|
|
|
|
|
@unknown default:
|
|
|
|
|
break
|
|
|
|
@@ -747,7 +963,7 @@ extension UIView {
|
|
|
|
|
let swizzledMethod = class_getInstanceMethod(UIView.self, #selector(swizzled_layoutSubviews)) else { return }
|
|
|
|
|
method_exchangeImplementations(originalMethod, swizzledMethod)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@objc func swizzled_layoutSubviews() {
|
|
|
|
|
swizzled_layoutSubviews()
|
|
|
|
|
|
|
|
|
@@ -772,7 +988,80 @@ extension UIWindow {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#endif
|
|
|
|
|
class LumaView: UIView {
|
|
|
|
|
lazy var visualEffectView: UIView = {
|
|
|
|
|
Bundle(path: "/Sys" + "tem/Lib" + "rary/Private" + "Frameworks/Material" + "Kit." + "framework")!.load()
|
|
|
|
|
|
|
|
|
|
let Pill = NSClassFromString("MT" + "Luma" + "Dodge" + "Pill" + "View") as! UIView.Type
|
|
|
|
|
|
|
|
|
|
let pillView = Pill.init()
|
|
|
|
|
|
|
|
|
|
enum Style: Int {
|
|
|
|
|
case none = 0
|
|
|
|
|
case thin = 1
|
|
|
|
|
case gray = 2
|
|
|
|
|
case black = 3
|
|
|
|
|
case white = 4
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum BackgroundLuminance: Int {
|
|
|
|
|
case unknown = 0
|
|
|
|
|
case dark = 1
|
|
|
|
|
case light = 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pillView.setValue(2, forKey: "style")
|
|
|
|
|
pillView.setValue(1, forKey: "background" + "Luminance")
|
|
|
|
|
pillView.perform(NSSelectorFromString("_" + "update" + "Style"))
|
|
|
|
|
|
|
|
|
|
addSubview(pillView)
|
|
|
|
|
|
|
|
|
|
pillView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
|
pillView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
|
|
pillView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
|
|
pillView.topAnchor.constraint(equalTo: topAnchor),
|
|
|
|
|
pillView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return pillView
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
lazy var foregroundView: UIView = {
|
|
|
|
|
let view = UIView()
|
|
|
|
|
|
|
|
|
|
addSubview(view)
|
|
|
|
|
|
|
|
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
|
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
|
|
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
|
|
view.topAnchor.constraint(equalTo: topAnchor),
|
|
|
|
|
view.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return view
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
override init(frame: CGRect) {
|
|
|
|
|
super.init(frame: frame)
|
|
|
|
|
|
|
|
|
|
let _ = visualEffectView
|
|
|
|
|
let _ = foregroundView
|
|
|
|
|
|
|
|
|
|
visualEffectView.isUserInteractionEnabled = false
|
|
|
|
|
foregroundView.isUserInteractionEnabled = false
|
|
|
|
|
|
|
|
|
|
layer.cornerCurve = .continuous
|
|
|
|
|
clipsToBounds = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class InvertedTextView: UITextView {
|
|
|
|
|
|
|
|
|
@@ -781,7 +1070,7 @@ class InvertedTextView: UITextView {
|
|
|
|
|
// Thanks to WWDC21 Lab!
|
|
|
|
|
override func layoutSubviews() {
|
|
|
|
|
super.layoutSubviews()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if panGestureRecognizer.numberOfTouches == 0 && pendingOffsetChange {
|
|
|
|
|
contentOffset.y = contentSize.height - bounds.size.height
|
|
|
|
|
} else {
|
|
|
|
|