mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
132 lines
3.8 KiB
Swift
132 lines
3.8 KiB
Swift
/*
|
||
* 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 <https://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
import SwiftUI
|
||
import Lottie
|
||
|
||
@MainActor
|
||
final class LockIconAnimator: ObservableObject {
|
||
@Published private(set) var progress: CGFloat
|
||
|
||
private var animationTask: Task<Void, Never>?
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|