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" : {
@ -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." : {

View file

@ -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)

View file

@ -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<CustomIdleAnimation?>) {
init(sourceURL: URL, isRemoteURL: Bool, animation: Binding<CustomIdleAnimation?>, 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,

View file

@ -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()
}
}
}
}
}

View file

@ -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()
}