mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Reposition and animations
This commit is contained in:
parent
2d2ac570cd
commit
ea2706ad96
2 changed files with 90 additions and 22 deletions
|
|
@ -420,18 +420,20 @@ struct LockScreenMusicPanel: View {
|
|||
private func controlsRow(alignment: Alignment, spacing: CGFloat) -> some View {
|
||||
let controls = resolvedAuxControls
|
||||
|
||||
let skipNudge: CGFloat = isExpanded ? 14 : 9
|
||||
|
||||
return HStack(spacing: spacing) {
|
||||
if let leftControl = controls.left {
|
||||
auxButton(for: leftControl)
|
||||
}
|
||||
|
||||
controlButton(icon: "backward.fill", size: 18) {
|
||||
controlButton(icon: "backward.fill", size: 18, pressNudge: -skipNudge) {
|
||||
musicManager.previousTrack()
|
||||
}
|
||||
|
||||
playPauseButton
|
||||
|
||||
controlButton(icon: "forward.fill", size: 18) {
|
||||
controlButton(icon: "forward.fill", size: 18, pressNudge: skipNudge) {
|
||||
musicManager.nextTrack()
|
||||
}
|
||||
|
||||
|
|
@ -453,6 +455,7 @@ struct LockScreenMusicPanel: View {
|
|||
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
||||
.font(.system(size: symbolSize, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
|
@ -464,37 +467,41 @@ struct LockScreenMusicPanel: View {
|
|||
size: CGFloat = 18,
|
||||
isActive: Bool = false,
|
||||
activeColor: Color = .red,
|
||||
pressNudge: CGFloat? = nil,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||
let iconSize: CGFloat = isExpanded ? max(size, 24) : size
|
||||
let iconColor = isActive ? activeColor : .white.opacity(0.8)
|
||||
let backgroundOpacity: Double = isActive ? 0.22 : 0.0
|
||||
|
||||
return Button(action: {
|
||||
return PanelControlButton(
|
||||
icon: icon,
|
||||
frameSize: frameSize,
|
||||
iconSize: iconSize,
|
||||
iconColor: iconColor,
|
||||
backgroundOpacity: backgroundOpacity,
|
||||
pressNudge: pressNudge
|
||||
) {
|
||||
registerInteraction()
|
||||
action()
|
||||
}) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: iconSize, weight: .medium))
|
||||
.foregroundColor(isActive ? activeColor : .white.opacity(0.8))
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
private var mediaOutputControlButton: some View {
|
||||
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||
let iconSize: CGFloat = isExpanded ? 26 : 18
|
||||
|
||||
return Button(action: toggleVolumeSlider) {
|
||||
Image(systemName: mediaOutputIcon)
|
||||
.font(.system(size: iconSize, weight: .medium))
|
||||
.foregroundColor(shouldShowVolumeSlider ? .accentColor : .white.opacity(0.8))
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
return PanelControlButton(
|
||||
icon: mediaOutputIcon,
|
||||
frameSize: frameSize,
|
||||
iconSize: iconSize,
|
||||
iconColor: shouldShowVolumeSlider ? .accentColor : .white.opacity(0.8),
|
||||
backgroundOpacity: shouldShowVolumeSlider ? 0.22 : 0.0,
|
||||
pressNudge: nil,
|
||||
action: toggleVolumeSlider
|
||||
)
|
||||
.accessibilityLabel("Media output")
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
private func lyricsSection(alignment: Alignment) -> some View {
|
||||
|
|
@ -791,3 +798,60 @@ struct LockScreenMusicPanel: View {
|
|||
print("[\(formatter.string(from: Date()))] LockScreenMusicPanel: \(event) – \(styleDescriptor)")
|
||||
}
|
||||
}
|
||||
|
||||
private struct PanelControlButton: View {
|
||||
let icon: String
|
||||
let frameSize: CGFloat
|
||||
let iconSize: CGFloat
|
||||
let iconColor: Color
|
||||
let backgroundOpacity: Double
|
||||
let pressNudge: CGFloat?
|
||||
let action: () -> Void
|
||||
|
||||
@State private var isHovering = false
|
||||
@State private var pressOffset: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
triggerPressEffect()
|
||||
action()
|
||||
}) {
|
||||
RoundedRectangle(cornerRadius: frameSize / 2, style: .continuous)
|
||||
.fill(backgroundColor)
|
||||
.overlay(
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: iconSize, weight: .medium))
|
||||
.foregroundStyle(iconColor)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
)
|
||||
}
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.offset(x: pressOffset)
|
||||
.onHover { hovering in
|
||||
withAnimation(.smooth(duration: 0.24)) {
|
||||
isHovering = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
let hoveredOpacity = max(backgroundOpacity + 0.08, 0.18)
|
||||
let appliedOpacity = isHovering ? hoveredOpacity : backgroundOpacity
|
||||
return Color.white.opacity(min(appliedOpacity, 0.32))
|
||||
}
|
||||
|
||||
private func triggerPressEffect() {
|
||||
guard let pressNudge else { return }
|
||||
|
||||
withAnimation(.spring(response: 0.2, dampingFraction: 0.55)) {
|
||||
pressOffset = pressNudge
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.16) {
|
||||
withAnimation(.spring(response: 0.28, dampingFraction: 0.72)) {
|
||||
pressOffset = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ final class LockScreenWeatherPanelManager {
|
|||
let fittingSize = hostingView.fittingSize
|
||||
hostingView.frame = NSRect(origin: .zero, size: fittingSize)
|
||||
|
||||
let targetFrame = frame(for: fittingSize, on: screen)
|
||||
let targetFrame = frame(for: fittingSize, snapshot: snapshot, on: screen)
|
||||
let window = ensureWindow()
|
||||
window.setFrame(targetFrame, display: true)
|
||||
latestFrame = targetFrame
|
||||
|
|
@ -78,14 +78,18 @@ final class LockScreenWeatherPanelManager {
|
|||
return newWindow
|
||||
}
|
||||
|
||||
private func frame(for size: CGSize, on screen: NSScreen) -> NSRect {
|
||||
private func frame(for size: CGSize, snapshot: LockScreenWeatherSnapshot, on screen: NSScreen) -> NSRect {
|
||||
let screenFrame = screen.frame
|
||||
let originX = screenFrame.midX - (size.width / 2)
|
||||
let verticalOffset = screenFrame.height * 0.15
|
||||
let maxY = screenFrame.maxY - size.height - 48
|
||||
let baseY = min(maxY, screenFrame.midY + verticalOffset)
|
||||
let loweredY = baseY - 36
|
||||
let clampedY = max(screenFrame.minY + 80, loweredY)
|
||||
|
||||
let inlineLift: CGFloat = snapshot.widgetStyle == .inline ? 28 : 0
|
||||
let adjustedY = loweredY + inlineLift
|
||||
let upperClampedY = min(maxY, adjustedY)
|
||||
let clampedY = max(screenFrame.minY + 80, upperClampedY)
|
||||
return NSRect(x: originX, y: clampedY, width: size.width, height: size.height)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue