/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import SwiftUI import Lottie @MainActor final class LockIconAnimator: ObservableObject { @Published private(set) var progress: CGFloat private var animationTask: Task? private let animationDuration: TimeInterval = 0.35 private let animationSteps: Int = 48 init(initiallyLocked: Bool) { progress = initiallyLocked ? 1.0 : 0.0 } deinit { animationTask?.cancel() } func update(isLocked: Bool, animated: Bool = true) { let target = isLocked ? 1.0 : 0.0 let clampedTarget = max(0.0, min(1.0, target)) if !animated { animationTask?.cancel() progress = clampedTarget return } guard abs(progress - clampedTarget) > 0.0005 else { progress = clampedTarget return } animationTask?.cancel() let startProgress = progress let delta = clampedTarget - startProgress let stepDuration = animationDuration / Double(animationSteps) let stepNanoseconds = UInt64(stepDuration * 1_000_000_000) animationTask = Task { [weak self] in guard let self else { return } for step in 0...animationSteps { if Task.isCancelled { return } if step > 0 { try? await Task.sleep(nanoseconds: stepNanoseconds) } let fraction = Double(step) / Double(animationSteps) let eased = easeOutCubic(fraction) progress = startProgress + CGFloat(eased) * CGFloat(delta) } progress = clampedTarget } } private func easeOutCubic(_ t: Double) -> Double { let clamped = max(0.0, min(1.0, t)) return 1.0 - pow(1.0 - clamped, 3) } } struct LockIconProgressView: View { var progress: CGFloat var iconColor: Color = .white var body: some View { if LockIconLottieView.isAvailable { Rectangle() .fill(iconColor) .mask { LockIconLottieView(progress: 1 - progress) .scaleEffect(1.12) } } else { Image(systemName: progress >= 0.5 ? "lock.fill" : "lock.open.fill") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(iconColor) } } } struct LockIconLottieView: View { var progress: CGFloat private static let animation: LottieAnimation? = { if let animation = LottieAnimation.named("Lock") { return animation } else { print("⚠️ [LockIconLottieView] Missing Lock.json animation – falling back to SF Symbols") return nil } }() static var isAvailable: Bool { animation != nil } var body: some View { Group { if let animation = Self.animation { LottieView(animation: animation) .currentProgress(progress) .configuration(.init(renderingEngine: .mainThread)) } else { Color.clear } } } }