mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
Working LottieAnimations with Scaling and stuff
This commit is contained in:
parent
c00b6db984
commit
e500f5a5b2
13 changed files with 1451 additions and 1 deletions
301
DynamicIsland/managers/IdleAnimationManager.swift
Normal file
301
DynamicIsland/managers/IdleAnimationManager.swift
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
//
|
||||
// IdleAnimationManager.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by AI Assistant on 11/10/2025.
|
||||
// Manages custom idle animations (Lottie files) for the Dynamic Island
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Defaults
|
||||
import AppKit
|
||||
|
||||
class IdleAnimationManager {
|
||||
static let shared = IdleAnimationManager()
|
||||
|
||||
// Storage directory for user-imported animations
|
||||
private let storageDirectory: URL
|
||||
|
||||
private init() {
|
||||
// Create storage directory in Application Support
|
||||
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
storageDirectory = appSupport.appendingPathComponent("DynamicIsland/IdleAnimations", isDirectory: true)
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
try? FileManager.default.createDirectory(at: storageDirectory, withIntermediateDirectories: true)
|
||||
|
||||
print("📁 [IdleAnimationManager] Storage directory: \(storageDirectory.path)")
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Load bundled animations from the LottieAnimations folder and built-in face
|
||||
func initializeDefaultAnimations() {
|
||||
var animations: [CustomIdleAnimation] = []
|
||||
|
||||
// Add built-in face as first option
|
||||
let builtInFace = CustomIdleAnimation(
|
||||
name: "Classic Face",
|
||||
source: .builtInFace,
|
||||
speed: 1.0,
|
||||
isBuiltIn: true
|
||||
)
|
||||
animations.append(builtInFace)
|
||||
|
||||
// Load bundled Lottie files
|
||||
if let bundledAnimations = loadBundledAnimations() {
|
||||
animations.append(contentsOf: bundledAnimations)
|
||||
}
|
||||
|
||||
// Load previously imported user animations from storage
|
||||
if let userAnimations = loadStoredUserAnimations() {
|
||||
animations.append(contentsOf: userAnimations)
|
||||
}
|
||||
|
||||
// Only set if empty (first launch)
|
||||
if Defaults[.customIdleAnimations].isEmpty {
|
||||
Defaults[.customIdleAnimations] = animations
|
||||
Defaults[.selectedIdleAnimation] = builtInFace // Default to built-in face
|
||||
print("✅ [IdleAnimationManager] Initialized with \(animations.count) animations")
|
||||
} else {
|
||||
// Merge: add any new bundled animations that aren't already in the list
|
||||
var existing = Defaults[.customIdleAnimations]
|
||||
let existingNames = Set(existing.map { $0.name })
|
||||
|
||||
for newAnim in animations where newAnim.isBuiltIn {
|
||||
if !existingNames.contains(newAnim.name) {
|
||||
existing.append(newAnim)
|
||||
}
|
||||
}
|
||||
|
||||
Defaults[.customIdleAnimations] = existing
|
||||
print("✅ [IdleAnimationManager] Updated with \(existing.count) animations")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Bundled Animations
|
||||
|
||||
/// Load animations from the LottieAnimations folder in the bundle
|
||||
private func loadBundledAnimations() -> [CustomIdleAnimation]? {
|
||||
guard let bundlePath = Bundle.main.resourcePath else {
|
||||
print("⚠️ [IdleAnimationManager] Could not find bundle resource path")
|
||||
return nil
|
||||
}
|
||||
|
||||
let lottieFolder = URL(fileURLWithPath: bundlePath).appendingPathComponent("LottieAnimations")
|
||||
|
||||
guard FileManager.default.fileExists(atPath: lottieFolder.path) else {
|
||||
print("⚠️ [IdleAnimationManager] LottieAnimations folder not found at: \(lottieFolder.path)")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let files = try FileManager.default.contentsOfDirectory(at: lottieFolder, includingPropertiesForKeys: nil)
|
||||
let jsonFiles = files.filter { $0.pathExtension.lowercased() == "json" }
|
||||
|
||||
let animations = jsonFiles.map { url -> CustomIdleAnimation in
|
||||
let name = url.deletingPathExtension().lastPathComponent
|
||||
return CustomIdleAnimation(
|
||||
name: name,
|
||||
source: .lottieFile(url),
|
||||
speed: 1.0,
|
||||
isBuiltIn: true
|
||||
)
|
||||
}
|
||||
|
||||
print("📦 [IdleAnimationManager] Loaded \(animations.count) bundled animations")
|
||||
return animations
|
||||
|
||||
} catch {
|
||||
print("❌ [IdleAnimationManager] Error loading bundled animations: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Animations
|
||||
|
||||
/// Load user-imported animations from storage directory
|
||||
private func loadStoredUserAnimations() -> [CustomIdleAnimation]? {
|
||||
do {
|
||||
let files = try FileManager.default.contentsOfDirectory(at: storageDirectory, includingPropertiesForKeys: nil)
|
||||
let jsonFiles = files.filter { $0.pathExtension.lowercased() == "json" }
|
||||
|
||||
let animations = jsonFiles.map { url -> CustomIdleAnimation in
|
||||
let name = url.deletingPathExtension().lastPathComponent
|
||||
return CustomIdleAnimation(
|
||||
name: name,
|
||||
source: .lottieFile(url),
|
||||
speed: 1.0,
|
||||
isBuiltIn: false
|
||||
)
|
||||
}
|
||||
|
||||
if !animations.isEmpty {
|
||||
print("💾 [IdleAnimationManager] Loaded \(animations.count) stored user animations")
|
||||
}
|
||||
return animations.isEmpty ? nil : animations
|
||||
|
||||
} catch {
|
||||
print("❌ [IdleAnimationManager] Error loading stored animations: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Import & Export
|
||||
|
||||
/// Import a Lottie JSON file from URL (either local file or download from remote)
|
||||
func importLottieFile(from url: URL, name: String? = nil, speed: CGFloat = 1.0, transformConfig: AnimationTransformConfig = .default) -> Result<CustomIdleAnimation, Error> {
|
||||
let fileName = name ?? url.deletingPathExtension().lastPathComponent
|
||||
|
||||
// If it's a remote URL, download it first
|
||||
if url.scheme == "http" || url.scheme == "https" {
|
||||
return importRemoteAnimation(from: url, name: fileName, speed: speed, transformConfig: transformConfig)
|
||||
}
|
||||
|
||||
// Local file import
|
||||
return importLocalFile(from: url, name: fileName, speed: speed, transformConfig: transformConfig)
|
||||
}
|
||||
|
||||
/// Import a local Lottie JSON file
|
||||
private func importLocalFile(from sourceURL: URL, name: String, speed: CGFloat, transformConfig: AnimationTransformConfig) -> Result<CustomIdleAnimation, Error> {
|
||||
do {
|
||||
// Validate it's a JSON file
|
||||
guard sourceURL.pathExtension.lowercased() == "json" else {
|
||||
return .failure(AnimationImportError.invalidFileType)
|
||||
}
|
||||
|
||||
// Validate JSON content (basic check)
|
||||
let data = try Data(contentsOf: sourceURL)
|
||||
guard let _ = try? JSONSerialization.jsonObject(with: data) else {
|
||||
return .failure(AnimationImportError.invalidJSON)
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
let uniqueFileName = "\(UUID().uuidString).json"
|
||||
let destinationURL = storageDirectory.appendingPathComponent(uniqueFileName)
|
||||
|
||||
// Copy file to storage
|
||||
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
|
||||
|
||||
// Create animation object
|
||||
let animation = CustomIdleAnimation(
|
||||
name: name,
|
||||
source: .lottieFile(destinationURL),
|
||||
speed: speed,
|
||||
isBuiltIn: false,
|
||||
transformConfig: transformConfig
|
||||
)
|
||||
|
||||
// Add to defaults
|
||||
var animations = Defaults[.customIdleAnimations]
|
||||
animations.append(animation)
|
||||
Defaults[.customIdleAnimations] = animations
|
||||
|
||||
print("✅ [IdleAnimationManager] Imported local file: \(name)")
|
||||
return .success(animation)
|
||||
|
||||
} catch {
|
||||
print("❌ [IdleAnimationManager] Import failed: \(error)")
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Import animation from remote URL
|
||||
private func importRemoteAnimation(from url: URL, name: String, speed: CGFloat, transformConfig: AnimationTransformConfig) -> Result<CustomIdleAnimation, Error> {
|
||||
// For remote URLs, we store the URL directly (no download)
|
||||
// The LottieView will handle downloading when needed
|
||||
|
||||
let animation = CustomIdleAnimation(
|
||||
name: name,
|
||||
source: .lottieURL(url),
|
||||
speed: speed,
|
||||
isBuiltIn: false,
|
||||
transformConfig: transformConfig
|
||||
)
|
||||
|
||||
// Add to defaults
|
||||
var animations = Defaults[.customIdleAnimations]
|
||||
animations.append(animation)
|
||||
Defaults[.customIdleAnimations] = animations
|
||||
|
||||
print("✅ [IdleAnimationManager] Added remote animation: \(name)")
|
||||
return .success(animation)
|
||||
}
|
||||
|
||||
// MARK: - Management
|
||||
|
||||
/// Delete an animation (only user-added ones, not built-in)
|
||||
func deleteAnimation(_ animation: CustomIdleAnimation) -> Bool {
|
||||
guard !animation.isBuiltIn else {
|
||||
print("⚠️ [IdleAnimationManager] Cannot delete built-in animation")
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove from defaults
|
||||
var animations = Defaults[.customIdleAnimations]
|
||||
guard let index = animations.firstIndex(of: animation) else {
|
||||
return false
|
||||
}
|
||||
animations.remove(at: index)
|
||||
Defaults[.customIdleAnimations] = animations
|
||||
|
||||
// If it's a local file, delete it from storage
|
||||
if case .lottieFile(let url) = animation.source {
|
||||
// Only delete if it's in our storage directory (not bundled)
|
||||
if url.path.contains(storageDirectory.path) {
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
print("🗑️ [IdleAnimationManager] Deleted file: \(url.lastPathComponent)")
|
||||
}
|
||||
}
|
||||
|
||||
// If deleted animation was selected, select the first one
|
||||
if Defaults[.selectedIdleAnimation] == animation {
|
||||
Defaults[.selectedIdleAnimation] = animations.first
|
||||
}
|
||||
|
||||
print("✅ [IdleAnimationManager] Deleted animation: \(animation.name)")
|
||||
return true
|
||||
}
|
||||
|
||||
/// Update animation properties
|
||||
func updateAnimation(_ animation: CustomIdleAnimation, name: String? = nil, speed: CGFloat? = nil) {
|
||||
var animations = Defaults[.customIdleAnimations]
|
||||
guard let index = animations.firstIndex(where: { $0.id == animation.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let name = name {
|
||||
animations[index].name = name
|
||||
}
|
||||
if let speed = speed {
|
||||
animations[index].speed = speed
|
||||
}
|
||||
|
||||
Defaults[.customIdleAnimations] = animations
|
||||
|
||||
// Update selected animation if it's the same one
|
||||
if Defaults[.selectedIdleAnimation]?.id == animation.id {
|
||||
Defaults[.selectedIdleAnimation] = animations[index]
|
||||
}
|
||||
|
||||
print("✅ [IdleAnimationManager] Updated animation: \(animation.name)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Types
|
||||
enum AnimationImportError: LocalizedError {
|
||||
case invalidFileType
|
||||
case invalidJSON
|
||||
case downloadFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidFileType:
|
||||
return "Only .json files are supported"
|
||||
case .invalidJSON:
|
||||
return "Invalid Lottie JSON format"
|
||||
case .downloadFailed:
|
||||
return "Failed to download animation"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue