From f8466c2027f49ad4ad20eba1ce98a3a4a9736033 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sat, 11 Oct 2025 13:34:18 +0530 Subject: [PATCH] BugFix: LiveActivity expands too much --- DynamicIsland/Localizable.xcstrings | 18 ++++ .../components/IdleAnimationView.swift | 18 +++- .../Settings/AnimationEditorView.swift | 89 +++++++++++++++++-- .../IdleAnimationsSettingsSection.swift | 51 ++++++++++- DynamicIsland/models/Constants.swift | 2 + 5 files changed, 162 insertions(+), 16 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index c86753a..e6cedd7 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -3569,6 +3569,9 @@ } } } + }, + "Bottom Pad:" : { + }, "Brightness HUD" : { @@ -5777,6 +5780,9 @@ }, "Display Mode" : { + }, + "Display Options" : { + }, "Displays a HUD notification when Bluetooth audio devices (headphones, AirPods, speakers) connect, showing device name and battery level." : { "comment" : "A footer for the \"Bluetooth Audio Devices\" section in the HUD settings.", @@ -6213,6 +6219,9 @@ } } } + }, + "Edit Animation" : { + }, "Edit layout" : { "localizations" : { @@ -7175,6 +7184,9 @@ } } } + }, + "Expand Notch with Animation" : { + }, "Extensions" : { "localizations" : { @@ -13845,6 +13857,9 @@ }, "Save" : { + }, + "Save Changes" : { + }, "Save Configuration" : { @@ -17496,6 +17511,9 @@ }, "When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { + }, + "When enabled, the Dynamic Island will expand horizontally to match the animation size" : { + }, "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { diff --git a/DynamicIsland/components/IdleAnimationView.swift b/DynamicIsland/components/IdleAnimationView.swift index 863d2c7..d075ae7 100644 --- a/DynamicIsland/components/IdleAnimationView.swift +++ b/DynamicIsland/components/IdleAnimationView.swift @@ -40,11 +40,16 @@ private struct AnimationContentView: View { speed: animation.speed, loopMode: .loop )) - .frame(width: config.cropWidth * config.scale, - height: config.cropHeight * config.scale) + .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() case .lottieURL(let url): LottieView(state: LUStateData( @@ -52,11 +57,16 @@ private struct AnimationContentView: View { speed: animation.speed, loopMode: .loop )) - .frame(width: config.cropWidth * config.scale, - height: config.cropHeight * config.scale) + .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() case .builtInFace: MinimalFaceFeatures(height: 20, width: 30) diff --git a/DynamicIsland/components/Settings/AnimationEditorView.swift b/DynamicIsland/components/Settings/AnimationEditorView.swift index ad6b682..86063f9 100644 --- a/DynamicIsland/components/Settings/AnimationEditorView.swift +++ b/DynamicIsland/components/Settings/AnimationEditorView.swift @@ -16,6 +16,7 @@ struct AnimationEditorView: View { let sourceURL: URL let isRemoteURL: Bool @Binding var animation: CustomIdleAnimation? + let existingAnimation: CustomIdleAnimation? @State private var name: String @State private var speed: CGFloat = 1.0 @@ -26,6 +27,8 @@ struct AnimationEditorView: View { @State private var cropHeight: CGFloat = 20 @State private var rotation: CGFloat = 0 @State private var opacity: CGFloat = 1.0 + @State private var paddingBottom: CGFloat = 0 + @State private var expandWithAnimation: Bool = false @State private var showingError = false @State private var errorMessage = "" @@ -34,21 +37,37 @@ struct AnimationEditorView: View { // Preview state @State private var previewScale: CGFloat = 1.0 - init(sourceURL: URL, isRemoteURL: Bool, animation: Binding) { + init(sourceURL: URL, isRemoteURL: Bool, animation: Binding, existingAnimation: CustomIdleAnimation? = nil) { self.sourceURL = sourceURL self.isRemoteURL = isRemoteURL self._animation = animation + self.existingAnimation = existingAnimation - // Initialize name from URL - let fileName = sourceURL.deletingPathExtension().lastPathComponent - _name = State(initialValue: fileName) + // Initialize from existing animation if editing, otherwise from URL + if let existing = existingAnimation { + _name = State(initialValue: existing.name) + _speed = State(initialValue: existing.speed) + let config = existing.transformConfig + _scale = State(initialValue: config.scale) + _offsetX = State(initialValue: config.offsetX) + _offsetY = State(initialValue: config.offsetY) + _cropWidth = State(initialValue: config.cropWidth) + _cropHeight = State(initialValue: config.cropHeight) + _rotation = State(initialValue: config.rotation) + _opacity = State(initialValue: config.opacity) + _paddingBottom = State(initialValue: config.paddingBottom) + _expandWithAnimation = State(initialValue: config.expandWithAnimation) + } else { + let fileName = sourceURL.deletingPathExtension().lastPathComponent + _name = State(initialValue: fileName) + } } var body: some View { VStack(spacing: 0) { // Header HStack { - Text("Customize Animation") + Text(existingAnimation != nil ? "Edit Animation" : "Customize Animation") .font(.title2) .fontWeight(.semibold) @@ -94,6 +113,7 @@ struct AnimationEditorView: View { .offset(x: offsetX * previewScale, y: offsetY * previewScale) .rotationEffect(.degrees(rotation)) .opacity(opacity) + .padding(.bottom, paddingBottom * previewScale) .clipped() } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -218,6 +238,21 @@ struct AnimationEditorView: View { .buttonStyle(.bordered) .controlSize(.small) } + + // Padding Bottom + HStack { + Text("Bottom Pad:") + .frame(width: 80, alignment: .leading) + Slider(value: $paddingBottom, in: -20...20) + Text(String(format: "%.1f", paddingBottom)) + .frame(width: 50, alignment: .trailing) + .foregroundStyle(.secondary) + Button("Reset") { + withAnimation { paddingBottom = 0 } + } + .buttonStyle(.bordered) + .controlSize(.small) + } } Divider() @@ -282,6 +317,24 @@ struct AnimationEditorView: View { Divider() + // Display Options + VStack(alignment: .leading, spacing: 12) { + Text("Display Options") + .font(.headline) + + Toggle(isOn: $expandWithAnimation) { + VStack(alignment: .leading, spacing: 4) { + Text("Expand Notch with Animation") + .font(.body) + Text("When enabled, the Dynamic Island will expand horizontally to match the animation size") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + + Divider() + // Presets VStack(alignment: .leading, spacing: 12) { Text("Quick Presets") @@ -296,6 +349,8 @@ struct AnimationEditorView: View { offsetX = 0 offsetY = 0 rotation = 0 + paddingBottom = 0 + expandWithAnimation = false } } .buttonStyle(.bordered) @@ -307,6 +362,8 @@ struct AnimationEditorView: View { cropHeight = 20 offsetX = 0 offsetY = 0 + paddingBottom = 0 + expandWithAnimation = false } } .buttonStyle(.bordered) @@ -314,6 +371,7 @@ struct AnimationEditorView: View { Button("Scale Up 2x") { withAnimation { scale = 2.0 + expandWithAnimation = true } } .buttonStyle(.bordered) @@ -335,6 +393,8 @@ struct AnimationEditorView: View { rotation = 0 opacity = 1.0 speed = 1.0 + paddingBottom = 0 + expandWithAnimation = false } } .buttonStyle(.bordered) @@ -361,7 +421,7 @@ struct AnimationEditorView: View { Spacer() - Button("Import") { + Button(existingAnimation != nil ? "Save Changes" : "Import") { importAnimation() } .buttonStyle(.borderedProminent) @@ -393,10 +453,23 @@ struct AnimationEditorView: View { cropWidth: cropWidth, cropHeight: cropHeight, rotation: rotation, - opacity: opacity + opacity: opacity, + paddingBottom: paddingBottom, + expandWithAnimation: expandWithAnimation ) - // Import with config + // If editing existing animation, update it directly + if let existing = existingAnimation { + var updatedAnimation = existing + updatedAnimation.name = name + updatedAnimation.speed = speed + updatedAnimation.transformConfig = config + animation = updatedAnimation + dismiss() + return + } + + // Import with config (new animation) let result = IdleAnimationManager.shared.importLottieFile( from: sourceURL, name: name, diff --git a/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift b/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift index c575e35..9819f6b 100644 --- a/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift +++ b/DynamicIsland/components/Settings/IdleAnimationsSettingsSection.swift @@ -30,6 +30,7 @@ struct IdleAnimationsSettingsSection: View { @State private var editorSourceURL: URL? @State private var editorIsRemote = false @State private var editedAnimation: CustomIdleAnimation? + @State private var editingExistingAnimation: CustomIdleAnimation? var body: some View { Section { @@ -49,6 +50,18 @@ struct IdleAnimationsSettingsSection: View { onDelete: animation.isBuiltIn ? nil : { selectedForDeletion = animation showingDeleteAlert = true + }, + onEdit: animation.isBuiltIn ? nil : { + editingExistingAnimation = animation + if case .lottieFile(let url) = animation.source { + editorSourceURL = url + editorIsRemote = false + showingEditor = true + } else if case .lottieURL(let url) = animation.source { + editorSourceURL = url + editorIsRemote = true + showingEditor = true + } } ) } @@ -107,19 +120,35 @@ struct IdleAnimationsSettingsSection: View { AnimationEditorView( sourceURL: url, isRemoteURL: editorIsRemote, - animation: $editedAnimation + animation: $editedAnimation, + existingAnimation: editingExistingAnimation ) .frame(minWidth: 800, minHeight: 600) .onChange(of: editedAnimation) { oldValue, newValue in if let animation = newValue { - // Animation was successfully imported via editor - withAnimation { - selectedIdleAnimation = animation + // Animation was successfully imported/edited via editor + if let existingAnim = editingExistingAnimation { + // Editing existing animation - update it + if var animations = customIdleAnimations as? [CustomIdleAnimation], + let index = animations.firstIndex(where: { $0.id == existingAnim.id }) { + animations[index] = animation + customIdleAnimations = animations + // Update selection if this was the selected animation + if selectedIdleAnimation?.id == existingAnim.id { + selectedIdleAnimation = animation + } + } + } else { + // New import - just select it + withAnimation { + selectedIdleAnimation = animation + } } showingEditor = false // Reset editor state editorSourceURL = nil editedAnimation = nil + editingExistingAnimation = nil } } } @@ -184,6 +213,7 @@ struct AnimationPreviewCard: View { let isSelected: Bool let onSelect: () -> Void let onDelete: (() -> Void)? + let onEdit: (() -> Void)? @State private var isHovering = false @@ -262,6 +292,19 @@ struct AnimationPreviewCard: View { .onTapGesture { onSelect() } + .contextMenu { + if let onEdit = onEdit { + Button("Edit Animation") { + onEdit() + } + Divider() + } + if let onDelete = onDelete { + Button("Delete", role: .destructive) { + onDelete() + } + } + } } } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index de6eecf..2772a0a 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -52,6 +52,8 @@ struct AnimationTransformConfig: Codable, Hashable, Equatable { var cropHeight: CGFloat = 20 var rotation: CGFloat = 0 var opacity: CGFloat = 1.0 + var paddingBottom: CGFloat = 0 // Allow adjustment to fill notch from bottom + var expandWithAnimation: Bool = false // Whether notch should expand horizontally with animation static let `default` = AnimationTransformConfig() }