From b7959b78fc58e731bf72ad77ee3afeabc82ad67c Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sat, 11 Oct 2025 15:45:58 +0530 Subject: [PATCH] Working Animations --- .gitignore | 3 +- .../components/IdleAnimationView.swift | 20 +++++- .../Settings/AnimationEditorView.swift | 72 ++++++++++++------- .../IdleAnimationsSettingsSection.swift | 21 +++++- .../managers/IdleAnimationManager.swift | 61 +++++++--------- DynamicIsland/models/Constants.swift | 18 +++-- 6 files changed, 126 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 1c41fef..18c8cac 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,5 @@ PrivateWorks *.py *.sh comparison_report.json -*.txt \ No newline at end of file +*.txt +ANIMATION_DEBUG_GUIDE.md \ No newline at end of file diff --git a/DynamicIsland/components/IdleAnimationView.swift b/DynamicIsland/components/IdleAnimationView.swift index 100c910..62c5c9e 100644 --- a/DynamicIsland/components/IdleAnimationView.swift +++ b/DynamicIsland/components/IdleAnimationView.swift @@ -13,12 +13,13 @@ import Defaults struct IdleAnimationView: View { @Default(.selectedIdleAnimation) var selectedAnimation + @Default(.animationTransformOverrides) var overrides var body: some View { Group { if let animation = selectedAnimation { AnimationContentView(animation: animation) - .id("\(animation.id)-\(animation.hashValue)") // Force complete recreation when ANY property changes + .id("\(animation.id)-\(overrides[animation.id.uuidString]?.hashValue ?? 0)") // Force recreation when override changes } else { // Fallback to original face if nothing selected MinimalFaceFeatures(height: 20, width: 30) @@ -32,7 +33,11 @@ private struct AnimationContentView: View { let animation: CustomIdleAnimation var body: some View { - let config = animation.transformConfig + let config = animation.getTransformConfig() + + // Debug logging + let _ = print("🎨 [IdleAnimationView] Rendering animation: \(animation.name)") + let _ = print("🎨 [IdleAnimationView] Config: scale=\(config.scale), offset=(\(config.offsetX), \(config.offsetY)), opacity=\(config.opacity)") switch animation.source { case .lottieFile(let url): @@ -72,7 +77,18 @@ private struct AnimationContentView: View { .clipped() case .builtInFace: + // Apply transforms to built-in face too! MinimalFaceFeatures(height: 20, width: 30) + .frame( + width: config.cropWidth * config.scale, + height: config.cropHeight * config.scale + ) + .offset(x: config.offsetX, y: config.offsetY) + .rotationEffect(.degrees(config.rotation)) + .opacity(config.opacity) + .padding(.bottom, config.paddingBottom) + .frame(width: config.expandWithAnimation ? nil : 30, height: 20) + .clipped() } } } diff --git a/DynamicIsland/components/Settings/AnimationEditorView.swift b/DynamicIsland/components/Settings/AnimationEditorView.swift index 23ff81e..ac95827 100644 --- a/DynamicIsland/components/Settings/AnimationEditorView.swift +++ b/DynamicIsland/components/Settings/AnimationEditorView.swift @@ -48,7 +48,9 @@ struct AnimationEditorView: View { if let existing = existingAnimation { _name = State(initialValue: existing.name) _speed = State(initialValue: existing.speed) - let config = existing.transformConfig + + // Load from overrides if they exist + let config = Defaults[.animationTransformOverrides][existing.id.uuidString] ?? .default _scale = State(initialValue: config.scale) _offsetX = State(initialValue: config.offsetX) _offsetY = State(initialValue: config.offsetY) @@ -106,17 +108,30 @@ struct AnimationEditorView: View { .frame(width: 30 * previewScale, height: 20 * previewScale) // Animation preview with transformations - LottieView(state: LUStateData( - type: .loadedFrom(sourceURL), - speed: speed, - loopMode: loopMode.lottieLoopMode - )) - .frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale) - .offset(x: offsetX * previewScale, y: offsetY * previewScale) - .rotationEffect(.degrees(rotation)) - .opacity(opacity) - .padding(.bottom, paddingBottom * previewScale) - .clipped() + if let existing = existingAnimation, case .builtInFace = existing.source { + // Show built-in face + MinimalFaceFeatures(height: 20, width: 30) + .scaleEffect(previewScale) + .frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale) + .offset(x: offsetX * previewScale, y: offsetY * previewScale) + .rotationEffect(.degrees(rotation)) + .opacity(opacity) + .padding(.bottom, paddingBottom * previewScale) + .clipped() + } else { + // Show Lottie animation + LottieView(state: LUStateData( + type: .loadedFrom(sourceURL), + speed: speed, + loopMode: loopMode.lottieLoopMode + )) + .frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale) + .offset(x: offsetX * previewScale, y: offsetY * previewScale) + .rotationEffect(.degrees(rotation)) + .opacity(opacity) + .padding(.bottom, paddingBottom * previewScale) + .clipped() + } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() @@ -475,18 +490,26 @@ struct AnimationEditorView: View { loopMode: loopMode ) - // If editing existing animation, create a NEW animation object with updated values + // If editing existing animation, save transform config to overrides if let existing = existingAnimation { - // Create completely new animation to force view refresh - let updatedAnimation = CustomIdleAnimation( - id: existing.id, // Keep same ID so it replaces the right one - name: name, - source: existing.source, - speed: speed, - isBuiltIn: existing.isBuiltIn, - transformConfig: config - ) - animation = updatedAnimation + // Store only the transform config override + var overrides = Defaults[.animationTransformOverrides] + overrides[existing.id.uuidString] = config + Defaults[.animationTransformOverrides] = overrides + + print("✅ [AnimationEditor] Saved transform override for: \(existing.name)") + print("✅ [AnimationEditor] Override: \(config)") + + // Force view refresh by updating selectedIdleAnimation if this is the selected one + if Defaults[.selectedIdleAnimation]?.id == existing.id { + // Trigger refresh by re-setting the same animation + let current = Defaults[.selectedIdleAnimation] + Defaults[.selectedIdleAnimation] = nil + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + Defaults[.selectedIdleAnimation] = current + } + } + dismiss() return } @@ -495,8 +518,7 @@ struct AnimationEditorView: View { let result = IdleAnimationManager.shared.importLottieFile( from: sourceURL, name: name, - speed: speed, - transformConfig: config + speed: speed ) switch result { diff --git a/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift b/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift index 20afc84..57b3b9d 100644 --- a/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift +++ b/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift @@ -51,16 +51,31 @@ struct IdleAnimationsSettingsSection: View { selectedForDeletion = animation showingDeleteAlert = true }, - onEdit: animation.isBuiltIn ? nil : { + onEdit: { + print("🔧 [Edit] Attempting to edit animation: \(animation.name)") + print("🔧 [Edit] Animation source: \(animation.source)") editingExistingAnimation = animation - if case .lottieFile(let url) = animation.source { + + switch animation.source { + case .lottieFile(let url): + print("🔧 [Edit] Lottie file URL: \(url)") + print("🔧 [Edit] File exists: \(FileManager.default.fileExists(atPath: url.path))") editorSourceURL = url editorIsRemote = false showingEditor = true - } else if case .lottieURL(let url) = animation.source { + + case .lottieURL(let url): + print("🔧 [Edit] Lottie URL: \(url)") editorSourceURL = url editorIsRemote = true showingEditor = true + + case .builtInFace: + print("🔧 [Edit] Built-in face - using dummy URL") + // Use a dummy URL for built-in face (editor only needs URL for preview) + editorSourceURL = URL(string: "builtin://face")! + editorIsRemote = false + showingEditor = true } } ) diff --git a/DynamicIsland/managers/IdleAnimationManager.swift b/DynamicIsland/managers/IdleAnimationManager.swift index 3db212b..40f489b 100644 --- a/DynamicIsland/managers/IdleAnimationManager.swift +++ b/DynamicIsland/managers/IdleAnimationManager.swift @@ -77,39 +77,34 @@ class IdleAnimationManager { /// 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 - } + print("📦 [IdleAnimationManager] Loading bundled animations...") - let lottieFolder = URL(fileURLWithPath: bundlePath).appendingPathComponent("LottieAnimations") + // The JSON files are added as individual resources, not in a folder + let bundledFiles = ["Dog waiting", "Moody Dog", "Orange Cat Peeping"] + var animations: [CustomIdleAnimation] = [] - 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, + for filename in bundledFiles { + if let url = Bundle.main.url(forResource: filename, withExtension: "json") { + let animation = CustomIdleAnimation( + name: filename, source: .lottieFile(url), speed: 1.0, isBuiltIn: true ) + animations.append(animation) + print("✅ [IdleAnimationManager] Loaded bundled animation: \(filename)") + } else { + print("⚠️ [IdleAnimationManager] Could not find bundled animation: \(filename).json") } - - print("📦 [IdleAnimationManager] Loaded \(animations.count) bundled animations") - return animations - - } catch { - print("❌ [IdleAnimationManager] Error loading bundled animations: \(error)") + } + + guard !animations.isEmpty else { + print("⚠️ [IdleAnimationManager] No bundled animations found") return nil } + + print("📦 [IdleAnimationManager] Loaded \(animations.count) bundled animations") + return animations } // MARK: - User Animations @@ -144,20 +139,20 @@ class IdleAnimationManager { // 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 { + func importLottieFile(from url: URL, name: String? = nil, speed: CGFloat = 1.0) -> Result { 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) + return importRemoteAnimation(from: url, name: fileName, speed: speed) } // Local file import - return importLocalFile(from: url, name: fileName, speed: speed, transformConfig: transformConfig) + return importLocalFile(from: url, name: fileName, speed: speed) } /// Import a local Lottie JSON file - private func importLocalFile(from sourceURL: URL, name: String, speed: CGFloat, transformConfig: AnimationTransformConfig) -> Result { + private func importLocalFile(from sourceURL: URL, name: String, speed: CGFloat) -> Result { do { // Validate it's a JSON file guard sourceURL.pathExtension.lowercased() == "json" else { @@ -177,13 +172,12 @@ class IdleAnimationManager { // Copy file to storage try FileManager.default.copyItem(at: sourceURL, to: destinationURL) - // Create animation object + // Create animation object (transforms will be stored separately) let animation = CustomIdleAnimation( name: name, source: .lottieFile(destinationURL), speed: speed, - isBuiltIn: false, - transformConfig: transformConfig + isBuiltIn: false ) // Add to defaults @@ -201,7 +195,7 @@ class IdleAnimationManager { } /// Import animation from remote URL - private func importRemoteAnimation(from url: URL, name: String, speed: CGFloat, transformConfig: AnimationTransformConfig) -> Result { + private func importRemoteAnimation(from url: URL, name: String, speed: CGFloat) -> Result { // For remote URLs, we store the URL directly (no download) // The LottieView will handle downloading when needed @@ -209,8 +203,7 @@ class IdleAnimationManager { name: name, source: .lottieURL(url), speed: speed, - isBuiltIn: false, - transformConfig: transformConfig + isBuiltIn: false ) // Add to defaults diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 2a27ddd..7539643 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -33,19 +33,28 @@ struct CustomIdleAnimation: Codable, Hashable, Equatable, Defaults.Serializable, var source: AnimationSource var speed: CGFloat = 1.0 var isBuiltIn: Bool = false // Track if it's bundled vs user-added - var transformConfig: AnimationTransformConfig = .default // Transform settings - init(id: UUID = UUID(), name: String, source: AnimationSource, speed: CGFloat = 1.0, isBuiltIn: Bool = false, transformConfig: AnimationTransformConfig = .default) { + init(id: UUID = UUID(), name: String, source: AnimationSource, speed: CGFloat = 1.0, isBuiltIn: Bool = false) { self.id = id self.name = name self.source = source self.speed = speed self.isBuiltIn = isBuiltIn - self.transformConfig = transformConfig + } + + /// Get the effective transform config (override or default) + func getTransformConfig() -> AnimationTransformConfig { + let override = Defaults[.animationTransformOverrides][id.uuidString] + if let override = override { + print("📋 [CustomIdleAnimation] Found override for '\(name)': \(override)") + } else { + print("📋 [CustomIdleAnimation] No override for '\(name)', using default") + } + return override ?? .default } } -struct AnimationTransformConfig: Codable, Hashable, Equatable { +struct AnimationTransformConfig: Codable, Hashable, Equatable, Defaults.Serializable { var scale: CGFloat = 1.0 var offsetX: CGFloat = 0 var offsetY: CGFloat = 0 @@ -299,6 +308,7 @@ extension Defaults.Keys { static let showNotHumanFace = Key("showNotHumanFace", default: false) static let customIdleAnimations = Key<[CustomIdleAnimation]>("customIdleAnimations", default: []) static let selectedIdleAnimation = Key("selectedIdleAnimation", default: nil) + static let animationTransformOverrides = Key<[String: AnimationTransformConfig]>("animationTransformOverrides", default: [:]) static let tileShowLabels = Key("tileShowLabels", default: false) static let showCalendar = Key("showCalendar", default: false) static let hideCompletedReminders = Key("hideCompletedReminders", default: true)