mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-29 08:04:32 +00:00
Working widget
This commit is contained in:
parent
5a722373ce
commit
b77de94b62
2 changed files with 138 additions and 17 deletions
|
|
@ -13,15 +13,19 @@ struct LockScreenMusicPanel: View {
|
||||||
static let expandedSize = CGSize(width: 720, height: 340)
|
static let expandedSize = CGSize(width: 720, height: 340)
|
||||||
|
|
||||||
@ObservedObject var musicManager = MusicManager.shared
|
@ObservedObject var musicManager = MusicManager.shared
|
||||||
|
@ObservedObject private var routeManager = AudioRouteManager.shared
|
||||||
|
@StateObject private var volumeModel = MediaOutputVolumeViewModel()
|
||||||
@State private var sliderValue: Double = 0
|
@State private var sliderValue: Double = 0
|
||||||
@State private var dragging: Bool = false
|
@State private var dragging: Bool = false
|
||||||
@State private var isActive = true
|
@State private var isActive = true
|
||||||
@State private var isExpanded = false
|
@State private var isExpanded = false
|
||||||
|
@State private var isVolumeSliderVisible = false
|
||||||
@State private var collapseWorkItem: DispatchWorkItem?
|
@State private var collapseWorkItem: DispatchWorkItem?
|
||||||
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||||
@Default(.lockScreenShowAppIcon) var showAppIcon
|
@Default(.lockScreenShowAppIcon) var showAppIcon
|
||||||
@Default(.lockScreenPanelShowsBorder) var showPanelBorder
|
@Default(.lockScreenPanelShowsBorder) var showPanelBorder
|
||||||
@Default(.lockScreenPanelUsesBlur) var enableBlur
|
@Default(.lockScreenPanelUsesBlur) var enableBlur
|
||||||
|
@Default(.showMediaOutputControl) var showMediaOutputControl
|
||||||
|
|
||||||
private let collapsedPanelCornerRadius: CGFloat = 28
|
private let collapsedPanelCornerRadius: CGFloat = 28
|
||||||
private let expandedPanelCornerRadius: CGFloat = 52
|
private let expandedPanelCornerRadius: CGFloat = 52
|
||||||
|
|
@ -29,9 +33,12 @@ struct LockScreenMusicPanel: View {
|
||||||
private let expandedAlbumArtCornerRadius: CGFloat = 60
|
private let expandedAlbumArtCornerRadius: CGFloat = 60
|
||||||
private let expandedContentSpacing: CGFloat = 40
|
private let expandedContentSpacing: CGFloat = 40
|
||||||
private let collapseTimeout: TimeInterval = 5
|
private let collapseTimeout: TimeInterval = 5
|
||||||
|
private let collapsedSliderExtraHeight: CGFloat = 72
|
||||||
|
private let expandedSliderExtraHeight: CGFloat = 88
|
||||||
|
|
||||||
private var currentSize: CGSize {
|
private var currentSize: CGSize {
|
||||||
isExpanded ? Self.expandedSize : Self.collapsedSize
|
let base = isExpanded ? Self.expandedSize : Self.collapsedSize
|
||||||
|
return CGSize(width: base.width, height: base.height + sliderExtraHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var panelCornerRadius: CGFloat {
|
private var panelCornerRadius: CGFloat {
|
||||||
|
|
@ -58,18 +65,32 @@ struct LockScreenMusicPanel: View {
|
||||||
panelCore
|
panelCore
|
||||||
.frame(width: currentSize.width, height: currentSize.height)
|
.frame(width: currentSize.width, height: currentSize.height)
|
||||||
.animation(.spring(response: 0.48, dampingFraction: 0.82, blendDuration: 0.18), value: isExpanded)
|
.animation(.spring(response: 0.48, dampingFraction: 0.82, blendDuration: 0.18), value: isExpanded)
|
||||||
|
.animation(.spring(response: 0.45, dampingFraction: 0.85, blendDuration: 0.18), value: shouldShowVolumeSlider)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
sliderValue = musicManager.elapsedTime
|
sliderValue = musicManager.elapsedTime
|
||||||
isActive = true
|
isActive = true
|
||||||
logPanelAppearance()
|
logPanelAppearance()
|
||||||
LockScreenPanelManager.shared.updatePanelSize(expanded: false, animated: false)
|
LockScreenPanelManager.shared.updatePanelSize(expanded: false, additionalHeight: sliderExtraHeight, animated: false)
|
||||||
|
routeManager.refreshDevices()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
isActive = false
|
isActive = false
|
||||||
cancelCollapseTimer()
|
cancelCollapseTimer()
|
||||||
|
isVolumeSliderVisible = false
|
||||||
}
|
}
|
||||||
.onChange(of: isExpanded) { _, expanded in
|
.onChange(of: isExpanded) { _, expanded in
|
||||||
LockScreenPanelManager.shared.updatePanelSize(expanded: expanded)
|
LockScreenPanelManager.shared.updatePanelSize(expanded: expanded, additionalHeight: sliderExtraHeight)
|
||||||
|
}
|
||||||
|
.onChange(of: isVolumeSliderVisible) { _, _ in
|
||||||
|
LockScreenPanelManager.shared.updatePanelSize(expanded: isExpanded, additionalHeight: sliderExtraHeight)
|
||||||
|
}
|
||||||
|
.onChange(of: showMediaOutputControl) { _, enabled in
|
||||||
|
if !enabled {
|
||||||
|
withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) {
|
||||||
|
isVolumeSliderVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LockScreenPanelManager.shared.updatePanelSize(expanded: isExpanded, additionalHeight: sliderExtraHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,8 +353,22 @@ struct LockScreenMusicPanel: View {
|
||||||
private func playbackControls(alignment: Alignment) -> some View {
|
private func playbackControls(alignment: Alignment) -> some View {
|
||||||
let spacing: CGFloat = isExpanded ? 24 : 20
|
let spacing: CGFloat = isExpanded ? 24 : 20
|
||||||
|
|
||||||
return HStack(spacing: spacing) {
|
return VStack(spacing: shouldShowVolumeSlider ? 14 : 10) {
|
||||||
// Always show shuffle on lock screen
|
controlsRow(alignment: alignment, spacing: spacing)
|
||||||
|
|
||||||
|
if shouldShowVolumeSlider {
|
||||||
|
volumeSlider
|
||||||
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
|
.padding(.top, isExpanded ? 6 : 2)
|
||||||
|
.animation(.spring(response: 0.45, dampingFraction: 0.82), value: shouldShowVolumeSlider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func controlsRow(alignment: Alignment, spacing: CGFloat) -> some View {
|
||||||
|
HStack(spacing: spacing) {
|
||||||
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
||||||
musicManager.toggleShuffle()
|
musicManager.toggleShuffle()
|
||||||
}
|
}
|
||||||
|
|
@ -348,13 +383,15 @@ struct LockScreenMusicPanel: View {
|
||||||
musicManager.nextTrack()
|
musicManager.nextTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show repeat on lock screen
|
if showMediaOutputControl {
|
||||||
|
mediaOutputControlButton
|
||||||
|
} else {
|
||||||
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
||||||
musicManager.toggleRepeat()
|
musicManager.toggleRepeat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: alignment)
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
.padding(.top, isExpanded ? 6 : 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var playPauseButton: some View {
|
private var playPauseButton: some View {
|
||||||
|
|
@ -391,6 +428,29 @@ struct LockScreenMusicPanel: View {
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var mediaOutputControlButton: some View {
|
||||||
|
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||||
|
let iconSize: CGFloat = isExpanded ? 26 : 18
|
||||||
|
|
||||||
|
return Button(action: {
|
||||||
|
registerInteraction()
|
||||||
|
withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) {
|
||||||
|
isVolumeSliderVisible.toggle()
|
||||||
|
}
|
||||||
|
if isVolumeSliderVisible {
|
||||||
|
routeManager.refreshDevices()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: mediaOutputIcon)
|
||||||
|
.font(.system(size: iconSize, weight: .medium))
|
||||||
|
.foregroundColor(isVolumeSliderVisible ? .accentColor : .white.opacity(0.8))
|
||||||
|
.frame(width: frameSize, height: frameSize)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.accessibilityLabel("Media output")
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
private var repeatIcon: String {
|
private var repeatIcon: String {
|
||||||
switch musicManager.repeatMode {
|
switch musicManager.repeatMode {
|
||||||
case .off: return "repeat"
|
case .off: return "repeat"
|
||||||
|
|
@ -399,6 +459,65 @@ struct LockScreenMusicPanel: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var mediaOutputIcon: String {
|
||||||
|
routeManager.activeDevice?.iconName ?? "speaker.wave.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var volumeSlider: some View {
|
||||||
|
HStack(spacing: 14) {
|
||||||
|
Image(systemName: volumeIconName)
|
||||||
|
.font(.system(size: isExpanded ? 16 : 14, weight: .semibold))
|
||||||
|
.foregroundColor(.white.opacity(0.8))
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value: Binding(
|
||||||
|
get: { Double(volumeModel.level) },
|
||||||
|
set: { newValue in
|
||||||
|
registerInteraction()
|
||||||
|
volumeModel.setVolume(Float(newValue))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
in: 0 ... 1
|
||||||
|
)
|
||||||
|
.tint(sliderColor)
|
||||||
|
|
||||||
|
Text(volumePercentage)
|
||||||
|
.font(.system(size: isExpanded ? 12 : 11, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
.frame(width: 48, alignment: .trailing)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: isExpanded ? 16 : 12, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.08))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldShowVolumeSlider: Bool {
|
||||||
|
showMediaOutputControl && isVolumeSliderVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sliderExtraHeight: CGFloat {
|
||||||
|
guard shouldShowVolumeSlider else { return 0 }
|
||||||
|
return isExpanded ? expandedSliderExtraHeight : collapsedSliderExtraHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private var volumeIconName: String {
|
||||||
|
if volumeModel.isMuted || volumeModel.level <= 0.001 {
|
||||||
|
return "speaker.slash.fill"
|
||||||
|
} else if volumeModel.level < 0.33 {
|
||||||
|
return "speaker.wave.1.fill"
|
||||||
|
} else if volumeModel.level < 0.66 {
|
||||||
|
return "speaker.wave.2.fill"
|
||||||
|
}
|
||||||
|
return "speaker.wave.3.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var volumePercentage: String {
|
||||||
|
"\(Int(round(volumeModel.level * 100)))%"
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var panelBackground: some View {
|
private var panelBackground: some View {
|
||||||
if enableBlur {
|
if enableBlur {
|
||||||
|
|
|
||||||
|
|
@ -89,15 +89,17 @@ class LockScreenPanelManager {
|
||||||
print("[\(timestamp())] LockScreenPanelManager: panel visible")
|
print("[\(timestamp())] LockScreenPanelManager: panel visible")
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePanelSize(expanded: Bool, animated: Bool = true) {
|
func updatePanelSize(expanded: Bool, additionalHeight: CGFloat = 0, animated: Bool = true) {
|
||||||
guard let window = panelWindow, let baseFrame = collapsedFrame else {
|
guard let window = panelWindow, let baseFrame = collapsedFrame else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetSize = expanded ? LockScreenMusicPanel.expandedSize : LockScreenMusicPanel.collapsedSize
|
let baseSize = expanded ? LockScreenMusicPanel.expandedSize : LockScreenMusicPanel.collapsedSize
|
||||||
let originX = baseFrame.midX - (targetSize.width / 2)
|
let targetWidth = baseSize.width
|
||||||
|
let targetHeight = baseSize.height + additionalHeight
|
||||||
|
let originX = baseFrame.midX - (targetWidth / 2)
|
||||||
let originY = baseFrame.origin.y
|
let originY = baseFrame.origin.y
|
||||||
let targetFrame = NSRect(x: originX, y: originY, width: targetSize.width, height: targetSize.height)
|
let targetFrame = NSRect(x: originX, y: originY, width: targetWidth, height: targetHeight)
|
||||||
|
|
||||||
if animated {
|
if animated {
|
||||||
window.animator().setFrame(targetFrame, display: true)
|
window.animator().setFrame(targetFrame, display: true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue