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 {
|
private func controlsRow(alignment: Alignment, spacing: CGFloat) -> some View {
|
||||||
let controls = resolvedAuxControls
|
let controls = resolvedAuxControls
|
||||||
|
|
||||||
|
let skipNudge: CGFloat = isExpanded ? 14 : 9
|
||||||
|
|
||||||
return HStack(spacing: spacing) {
|
return HStack(spacing: spacing) {
|
||||||
if let leftControl = controls.left {
|
if let leftControl = controls.left {
|
||||||
auxButton(for: leftControl)
|
auxButton(for: leftControl)
|
||||||
}
|
}
|
||||||
|
|
||||||
controlButton(icon: "backward.fill", size: 18) {
|
controlButton(icon: "backward.fill", size: 18, pressNudge: -skipNudge) {
|
||||||
musicManager.previousTrack()
|
musicManager.previousTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
playPauseButton
|
playPauseButton
|
||||||
|
|
||||||
controlButton(icon: "forward.fill", size: 18) {
|
controlButton(icon: "forward.fill", size: 18, pressNudge: skipNudge) {
|
||||||
musicManager.nextTrack()
|
musicManager.nextTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,6 +455,7 @@ struct LockScreenMusicPanel: View {
|
||||||
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
||||||
.font(.system(size: symbolSize, weight: .semibold))
|
.font(.system(size: symbolSize, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
.contentTransition(.symbolEffect(.replace))
|
||||||
.frame(width: frameSize, height: frameSize)
|
.frame(width: frameSize, height: frameSize)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
@ -464,37 +467,41 @@ struct LockScreenMusicPanel: View {
|
||||||
size: CGFloat = 18,
|
size: CGFloat = 18,
|
||||||
isActive: Bool = false,
|
isActive: Bool = false,
|
||||||
activeColor: Color = .red,
|
activeColor: Color = .red,
|
||||||
|
pressNudge: CGFloat? = nil,
|
||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) -> some View {
|
) -> some View {
|
||||||
let frameSize: CGFloat = isExpanded ? 56 : 32
|
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||||
let iconSize: CGFloat = isExpanded ? max(size, 24) : size
|
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()
|
registerInteraction()
|
||||||
action()
|
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 {
|
private var mediaOutputControlButton: some View {
|
||||||
let frameSize: CGFloat = isExpanded ? 56 : 32
|
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||||
let iconSize: CGFloat = isExpanded ? 26 : 18
|
let iconSize: CGFloat = isExpanded ? 26 : 18
|
||||||
|
|
||||||
return Button(action: toggleVolumeSlider) {
|
return PanelControlButton(
|
||||||
Image(systemName: mediaOutputIcon)
|
icon: mediaOutputIcon,
|
||||||
.font(.system(size: iconSize, weight: .medium))
|
frameSize: frameSize,
|
||||||
.foregroundColor(shouldShowVolumeSlider ? .accentColor : .white.opacity(0.8))
|
iconSize: iconSize,
|
||||||
.frame(width: frameSize, height: frameSize)
|
iconColor: shouldShowVolumeSlider ? .accentColor : .white.opacity(0.8),
|
||||||
.contentShape(Rectangle())
|
backgroundOpacity: shouldShowVolumeSlider ? 0.22 : 0.0,
|
||||||
}
|
pressNudge: nil,
|
||||||
|
action: toggleVolumeSlider
|
||||||
|
)
|
||||||
.accessibilityLabel("Media output")
|
.accessibilityLabel("Media output")
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func lyricsSection(alignment: Alignment) -> some View {
|
private func lyricsSection(alignment: Alignment) -> some View {
|
||||||
|
|
@ -791,3 +798,60 @@ struct LockScreenMusicPanel: View {
|
||||||
print("[\(formatter.string(from: Date()))] LockScreenMusicPanel: \(event) – \(styleDescriptor)")
|
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
|
let fittingSize = hostingView.fittingSize
|
||||||
hostingView.frame = NSRect(origin: .zero, size: 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()
|
let window = ensureWindow()
|
||||||
window.setFrame(targetFrame, display: true)
|
window.setFrame(targetFrame, display: true)
|
||||||
latestFrame = targetFrame
|
latestFrame = targetFrame
|
||||||
|
|
@ -78,14 +78,18 @@ final class LockScreenWeatherPanelManager {
|
||||||
return newWindow
|
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 screenFrame = screen.frame
|
||||||
let originX = screenFrame.midX - (size.width / 2)
|
let originX = screenFrame.midX - (size.width / 2)
|
||||||
let verticalOffset = screenFrame.height * 0.15
|
let verticalOffset = screenFrame.height * 0.15
|
||||||
let maxY = screenFrame.maxY - size.height - 48
|
let maxY = screenFrame.maxY - size.height - 48
|
||||||
let baseY = min(maxY, screenFrame.midY + verticalOffset)
|
let baseY = min(maxY, screenFrame.midY + verticalOffset)
|
||||||
let loweredY = baseY - 36
|
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)
|
return NSRect(x: originX, y: clampedY, width: size.width, height: size.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue