diff --git a/ios/Apps/Sybil/Resources/Character/character-busy.gif b/ios/Apps/Sybil/Resources/Character/character-busy.gif deleted file mode 100644 index 75888bb..0000000 Binary files a/ios/Apps/Sybil/Resources/Character/character-busy.gif and /dev/null differ diff --git a/ios/Apps/Sybil/Resources/Character/character-idle.gif b/ios/Apps/Sybil/Resources/Character/character-idle.gif deleted file mode 100644 index 7d4d544..0000000 Binary files a/ios/Apps/Sybil/Resources/Character/character-idle.gif and /dev/null differ diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift index 5836319..1c6439c 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift @@ -1,4 +1,3 @@ -import ImageIO import Observation import PhotosUI import SwiftUI @@ -156,8 +155,6 @@ struct SybilWorkspaceView: View { workspaceContentStack if showsCustomWorkspaceNavigation { - SybilWorkspaceCharacterBackdrop(isBusy: viewModel.isActiveSelectionSending) - .allowsHitTesting(false) 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 { var progress: CGFloat var hasLatched: Bool