BugFix: LiveActivity expands too much

This commit is contained in:
Hariharan Mudaliar 2025-10-11 13:34:18 +05:30
commit f8466c2027
5 changed files with 162 additions and 16 deletions

View file

@ -3569,6 +3569,9 @@
} }
} }
} }
},
"Bottom Pad:" : {
}, },
"Brightness HUD" : { "Brightness HUD" : {
@ -5777,6 +5780,9 @@
}, },
"Display Mode" : { "Display Mode" : {
},
"Display Options" : {
}, },
"Displays a HUD notification when Bluetooth audio devices (headphones, AirPods, speakers) connect, showing device name and battery level." : { "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.", "comment" : "A footer for the \"Bluetooth Audio Devices\" section in the HUD settings.",
@ -6213,6 +6219,9 @@
} }
} }
} }
},
"Edit Animation" : {
}, },
"Edit layout" : { "Edit layout" : {
"localizations" : { "localizations" : {
@ -7175,6 +7184,9 @@
} }
} }
} }
},
"Expand Notch with Animation" : {
}, },
"Extensions" : { "Extensions" : {
"localizations" : { "localizations" : {
@ -13845,6 +13857,9 @@
}, },
"Save" : { "Save" : {
},
"Save Changes" : {
}, },
"Save Configuration" : { "Save Configuration" : {
@ -17496,6 +17511,9 @@
}, },
"When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { "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." : { "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {

View file

@ -40,11 +40,16 @@ private struct AnimationContentView: View {
speed: animation.speed, speed: animation.speed,
loopMode: .loop loopMode: .loop
)) ))
.frame(width: config.cropWidth * config.scale, .frame(
height: config.cropHeight * config.scale) width: config.cropWidth * config.scale,
height: config.cropHeight * config.scale
)
.offset(x: config.offsetX, y: config.offsetY) .offset(x: config.offsetX, y: config.offsetY)
.rotationEffect(.degrees(config.rotation)) .rotationEffect(.degrees(config.rotation))
.opacity(config.opacity) .opacity(config.opacity)
.padding(.bottom, config.paddingBottom)
.frame(width: config.expandWithAnimation ? nil : 30, height: 20)
.clipped()
case .lottieURL(let url): case .lottieURL(let url):
LottieView(state: LUStateData( LottieView(state: LUStateData(
@ -52,11 +57,16 @@ private struct AnimationContentView: View {
speed: animation.speed, speed: animation.speed,
loopMode: .loop loopMode: .loop
)) ))
.frame(width: config.cropWidth * config.scale, .frame(
height: config.cropHeight * config.scale) width: config.cropWidth * config.scale,
height: config.cropHeight * config.scale
)
.offset(x: config.offsetX, y: config.offsetY) .offset(x: config.offsetX, y: config.offsetY)
.rotationEffect(.degrees(config.rotation)) .rotationEffect(.degrees(config.rotation))
.opacity(config.opacity) .opacity(config.opacity)
.padding(.bottom, config.paddingBottom)
.frame(width: config.expandWithAnimation ? nil : 30, height: 20)
.clipped()
case .builtInFace: case .builtInFace:
MinimalFaceFeatures(height: 20, width: 30) MinimalFaceFeatures(height: 20, width: 30)

View file

@ -16,6 +16,7 @@ struct AnimationEditorView: View {
let sourceURL: URL let sourceURL: URL
let isRemoteURL: Bool let isRemoteURL: Bool
@Binding var animation: CustomIdleAnimation? @Binding var animation: CustomIdleAnimation?
let existingAnimation: CustomIdleAnimation?
@State private var name: String @State private var name: String
@State private var speed: CGFloat = 1.0 @State private var speed: CGFloat = 1.0
@ -26,6 +27,8 @@ struct AnimationEditorView: View {
@State private var cropHeight: CGFloat = 20 @State private var cropHeight: CGFloat = 20
@State private var rotation: CGFloat = 0 @State private var rotation: CGFloat = 0
@State private var opacity: CGFloat = 1.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 showingError = false
@State private var errorMessage = "" @State private var errorMessage = ""
@ -34,21 +37,37 @@ struct AnimationEditorView: View {
// Preview state // Preview state
@State private var previewScale: CGFloat = 1.0 @State private var previewScale: CGFloat = 1.0
init(sourceURL: URL, isRemoteURL: Bool, animation: Binding<CustomIdleAnimation?>) { init(sourceURL: URL, isRemoteURL: Bool, animation: Binding<CustomIdleAnimation?>, existingAnimation: CustomIdleAnimation? = nil) {
self.sourceURL = sourceURL self.sourceURL = sourceURL
self.isRemoteURL = isRemoteURL self.isRemoteURL = isRemoteURL
self._animation = animation self._animation = animation
self.existingAnimation = existingAnimation
// Initialize name from URL // Initialize from existing animation if editing, otherwise from URL
let fileName = sourceURL.deletingPathExtension().lastPathComponent if let existing = existingAnimation {
_name = State(initialValue: fileName) _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 { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Header // Header
HStack { HStack {
Text("Customize Animation") Text(existingAnimation != nil ? "Edit Animation" : "Customize Animation")
.font(.title2) .font(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
@ -94,6 +113,7 @@ struct AnimationEditorView: View {
.offset(x: offsetX * previewScale, y: offsetY * previewScale) .offset(x: offsetX * previewScale, y: offsetY * previewScale)
.rotationEffect(.degrees(rotation)) .rotationEffect(.degrees(rotation))
.opacity(opacity) .opacity(opacity)
.padding(.bottom, paddingBottom * previewScale)
.clipped() .clipped()
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
@ -218,6 +238,21 @@ struct AnimationEditorView: View {
.buttonStyle(.bordered) .buttonStyle(.bordered)
.controlSize(.small) .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() Divider()
@ -282,6 +317,24 @@ struct AnimationEditorView: View {
Divider() 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 // Presets
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text("Quick Presets") Text("Quick Presets")
@ -296,6 +349,8 @@ struct AnimationEditorView: View {
offsetX = 0 offsetX = 0
offsetY = 0 offsetY = 0
rotation = 0 rotation = 0
paddingBottom = 0
expandWithAnimation = false
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@ -307,6 +362,8 @@ struct AnimationEditorView: View {
cropHeight = 20 cropHeight = 20
offsetX = 0 offsetX = 0
offsetY = 0 offsetY = 0
paddingBottom = 0
expandWithAnimation = false
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@ -314,6 +371,7 @@ struct AnimationEditorView: View {
Button("Scale Up 2x") { Button("Scale Up 2x") {
withAnimation { withAnimation {
scale = 2.0 scale = 2.0
expandWithAnimation = true
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@ -335,6 +393,8 @@ struct AnimationEditorView: View {
rotation = 0 rotation = 0
opacity = 1.0 opacity = 1.0
speed = 1.0 speed = 1.0
paddingBottom = 0
expandWithAnimation = false
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@ -361,7 +421,7 @@ struct AnimationEditorView: View {
Spacer() Spacer()
Button("Import") { Button(existingAnimation != nil ? "Save Changes" : "Import") {
importAnimation() importAnimation()
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
@ -393,10 +453,23 @@ struct AnimationEditorView: View {
cropWidth: cropWidth, cropWidth: cropWidth,
cropHeight: cropHeight, cropHeight: cropHeight,
rotation: rotation, 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( let result = IdleAnimationManager.shared.importLottieFile(
from: sourceURL, from: sourceURL,
name: name, name: name,

View file

@ -30,6 +30,7 @@ struct IdleAnimationsSettingsSection: View {
@State private var editorSourceURL: URL? @State private var editorSourceURL: URL?
@State private var editorIsRemote = false @State private var editorIsRemote = false
@State private var editedAnimation: CustomIdleAnimation? @State private var editedAnimation: CustomIdleAnimation?
@State private var editingExistingAnimation: CustomIdleAnimation?
var body: some View { var body: some View {
Section { Section {
@ -49,6 +50,18 @@ struct IdleAnimationsSettingsSection: View {
onDelete: animation.isBuiltIn ? nil : { onDelete: animation.isBuiltIn ? nil : {
selectedForDeletion = animation selectedForDeletion = animation
showingDeleteAlert = true 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( AnimationEditorView(
sourceURL: url, sourceURL: url,
isRemoteURL: editorIsRemote, isRemoteURL: editorIsRemote,
animation: $editedAnimation animation: $editedAnimation,
existingAnimation: editingExistingAnimation
) )
.frame(minWidth: 800, minHeight: 600) .frame(minWidth: 800, minHeight: 600)
.onChange(of: editedAnimation) { oldValue, newValue in .onChange(of: editedAnimation) { oldValue, newValue in
if let animation = newValue { if let animation = newValue {
// Animation was successfully imported via editor // Animation was successfully imported/edited via editor
withAnimation { if let existingAnim = editingExistingAnimation {
selectedIdleAnimation = animation // 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 showingEditor = false
// Reset editor state // Reset editor state
editorSourceURL = nil editorSourceURL = nil
editedAnimation = nil editedAnimation = nil
editingExistingAnimation = nil
} }
} }
} }
@ -184,6 +213,7 @@ struct AnimationPreviewCard: View {
let isSelected: Bool let isSelected: Bool
let onSelect: () -> Void let onSelect: () -> Void
let onDelete: (() -> Void)? let onDelete: (() -> Void)?
let onEdit: (() -> Void)?
@State private var isHovering = false @State private var isHovering = false
@ -262,6 +292,19 @@ struct AnimationPreviewCard: View {
.onTapGesture { .onTapGesture {
onSelect() onSelect()
} }
.contextMenu {
if let onEdit = onEdit {
Button("Edit Animation") {
onEdit()
}
Divider()
}
if let onDelete = onDelete {
Button("Delete", role: .destructive) {
onDelete()
}
}
}
} }
} }

View file

@ -52,6 +52,8 @@ struct AnimationTransformConfig: Codable, Hashable, Equatable {
var cropHeight: CGFloat = 20 var cropHeight: CGFloat = 20
var rotation: CGFloat = 0 var rotation: CGFloat = 0
var opacity: CGFloat = 1.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() static let `default` = AnimationTransformConfig()
} }