This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -1,4 +1,3 @@
|
|||||||
import ImageIO
|
|
||||||
import Observation
|
import Observation
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -156,8 +155,6 @@ struct SybilWorkspaceView: View {
|
|||||||
workspaceContentStack
|
workspaceContentStack
|
||||||
|
|
||||||
if showsCustomWorkspaceNavigation {
|
if showsCustomWorkspaceNavigation {
|
||||||
SybilWorkspaceCharacterBackdrop(isBusy: viewModel.isActiveSelectionSending)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
customWorkspaceNavigationBar
|
customWorkspaceNavigationBar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1375,173 +1372,6 @@ private struct SybilNavigationFadeBackground: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SybilWorkspaceCharacterBackdrop: View {
|
|
||||||
var isBusy: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
RadialGradient(
|
|
||||||
colors: [
|
|
||||||
SybilTheme.primary.opacity(0.36),
|
|
||||||
SybilTheme.primary.opacity(0.13),
|
|
||||||
Color.clear
|
|
||||||
],
|
|
||||||
center: .center,
|
|
||||||
startRadius: 10,
|
|
||||||
endRadius: 150
|
|
||||||
)
|
|
||||||
.frame(width: 136, height: 118)
|
|
||||||
.blur(radius: 7)
|
|
||||||
.offset(x: 28, y: -24)
|
|
||||||
|
|
||||||
SybilAnimatedGIFView(resourceName: isBusy ? "character-busy" : "character-idle")
|
|
||||||
.frame(width: 172, height: 172)
|
|
||||||
.opacity(0.92)
|
|
||||||
.mask {
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.black,
|
|
||||||
Color.black,
|
|
||||||
Color.black.opacity(0)
|
|
||||||
],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.mask {
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.clear,
|
|
||||||
Color.black.opacity(0.98),
|
|
||||||
Color.black
|
|
||||||
],
|
|
||||||
startPoint: .leading,
|
|
||||||
endPoint: .trailing
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.offset(x: 8, y: 4)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, minHeight: 156, maxHeight: 156, alignment: .topTrailing)
|
|
||||||
.clipped()
|
|
||||||
.ignoresSafeArea(edges: .top)
|
|
||||||
.accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct SybilAnimatedGIFView: UIViewRepresentable {
|
|
||||||
var resourceName: String
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIImageView {
|
|
||||||
let imageView = SybilAnimatedUIImageView()
|
|
||||||
imageView.backgroundColor = .clear
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.clipsToBounds = false
|
|
||||||
imageView.isOpaque = false
|
|
||||||
imageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
||||||
imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
||||||
imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
||||||
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
||||||
return imageView
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ imageView: UIImageView, context: Context) {
|
|
||||||
guard context.coordinator.resourceName != resourceName else {
|
|
||||||
if !imageView.isAnimating {
|
|
||||||
imageView.startAnimating()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
context.coordinator.resourceName = resourceName
|
|
||||||
imageView.image = SybilAnimatedGIFCache.image(named: resourceName)
|
|
||||||
imageView.startAnimating()
|
|
||||||
}
|
|
||||||
|
|
||||||
final class Coordinator {
|
|
||||||
var resourceName: String?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class SybilAnimatedUIImageView: UIImageView {
|
|
||||||
override var intrinsicContentSize: CGSize {
|
|
||||||
CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private enum SybilAnimatedGIFCache {
|
|
||||||
private static var images: [String: UIImage] = [:]
|
|
||||||
|
|
||||||
static func image(named name: String) -> UIImage? {
|
|
||||||
if let cached = images[name] {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let image = loadImage(named: name) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
images[name] = image
|
|
||||||
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func loadImage(named name: String) -> UIImage? {
|
|
||||||
let url = Bundle.main.url(forResource: name, withExtension: "gif", subdirectory: "Character") ??
|
|
||||||
Bundle.main.url(forResource: name, withExtension: "gif")
|
|
||||||
|
|
||||||
guard let url,
|
|
||||||
let data = try? Data(contentsOf: url),
|
|
||||||
let source = CGImageSourceCreateWithData(data as CFData, nil)
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let frameCount = CGImageSourceGetCount(source)
|
|
||||||
guard frameCount > 0 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var frames: [UIImage] = []
|
|
||||||
frames.reserveCapacity(frameCount)
|
|
||||||
var duration: TimeInterval = 0
|
|
||||||
|
|
||||||
for index in 0 ..< frameCount {
|
|
||||||
guard let cgImage = CGImageSourceCreateImageAtIndex(source, index, nil) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
frames.append(UIImage(cgImage: cgImage))
|
|
||||||
duration += frameDuration(at: index, source: source)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !frames.isEmpty else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if frames.count == 1 {
|
|
||||||
return frames[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return UIImage.animatedImage(with: frames, duration: max(duration, 0.1))
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func frameDuration(at index: Int, source: CGImageSource) -> TimeInterval {
|
|
||||||
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any],
|
|
||||||
let gifProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any]
|
|
||||||
else {
|
|
||||||
return 0.08
|
|
||||||
}
|
|
||||||
|
|
||||||
let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime] as? TimeInterval
|
|
||||||
let delay = unclampedDelay ?? gifProperties[kCGImagePropertyGIFDelayTime] as? TimeInterval ?? 0.08
|
|
||||||
return max(delay, 0.03)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct NewChatSwipeBackdrop: View {
|
private struct NewChatSwipeBackdrop: View {
|
||||||
var progress: CGFloat
|
var progress: CGFloat
|
||||||
var hasLatched: Bool
|
var hasLatched: Bool
|
||||||
|
|||||||
Reference in New Issue
Block a user