mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working lock screen widget
This commit is contained in:
parent
a887ee220c
commit
44b18513f2
13 changed files with 705 additions and 3 deletions
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// LockScreenLiveActivity.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created for lock screen live activity
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
import SkyLightWindow
|
||||
|
||||
struct LockScreenLiveActivity: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
// Don't observe lockScreenManager - just show static lock icon without state-driven animations
|
||||
// This prevents animation conflicts with panel window teardown
|
||||
@State private var isHovering: Bool = false
|
||||
@State private var gestureProgress: CGFloat = 0
|
||||
@State private var isExpanded: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Left - Lock icon with subtle glow
|
||||
Color.clear
|
||||
.background {
|
||||
if isExpanded {
|
||||
HStack {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.blue.opacity(0.15))
|
||||
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.blue)
|
||||
.modifier(LockPulsingModifier())
|
||||
}
|
||||
.frame(width: vm.effectiveClosedNotchHeight - 12, height: vm.effectiveClosedNotchHeight - 12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||
|
||||
// Center - Black fill
|
||||
Rectangle()
|
||||
.fill(.black)
|
||||
.frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0))
|
||||
|
||||
// Right - Empty for symmetry with animation
|
||||
Color.clear
|
||||
.frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||
}
|
||||
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0))
|
||||
.onAppear {
|
||||
// Expand immediately without animation to avoid conflicts
|
||||
isExpanded = true
|
||||
}
|
||||
.onDisappear {
|
||||
// Collapse immediately when removed from hierarchy
|
||||
isExpanded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pulsing animation modifier for lock indicator
|
||||
struct LockPulsingModifier: ViewModifier {
|
||||
@State private var isPulsing = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.scaleEffect(isPulsing ? 1.1 : 1.0)
|
||||
.opacity(isPulsing ? 0.8 : 1.0)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) {
|
||||
isPulsing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
257
DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift
Normal file
257
DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
//
|
||||
// LockScreenMusicPanel.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created for lock screen music panel with liquid glass effect
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
struct LockScreenMusicPanel: View {
|
||||
@ObservedObject var musicManager = MusicManager.shared
|
||||
@State private var sliderValue: Double = 0
|
||||
@State private var dragging: Bool = false
|
||||
@State private var lastDragged: Date = .distantPast
|
||||
@State private var isActive = true
|
||||
|
||||
var body: some View {
|
||||
if isActive {
|
||||
panelContent
|
||||
.onDisappear {
|
||||
isActive = false
|
||||
}
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: 420, height: 150)
|
||||
}
|
||||
}
|
||||
|
||||
private var panelContent: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header area with album art
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
// Album art
|
||||
Button {
|
||||
musicManager.openMusicApp()
|
||||
} label: {
|
||||
Image(nsImage: musicManager.albumArt)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
||||
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
||||
|
||||
// Song info
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(musicManager.songTitle.isEmpty ? "No Music Playing" : musicManager.songTitle)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(musicManager.artistName.isEmpty ? "Unknown Artist" : musicManager.artistName)
|
||||
.font(.system(size: 10, weight: .regular))
|
||||
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Visualizer
|
||||
if Defaults[.useMusicVisualizer] {
|
||||
Rectangle()
|
||||
.fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient)
|
||||
.mask {
|
||||
AudioSpectrumView(isPlaying: .constant(musicManager.isPlaying))
|
||||
.frame(width: 20, height: 16)
|
||||
}
|
||||
.frame(width: 20, height: 16)
|
||||
}
|
||||
}
|
||||
.frame(height: 50)
|
||||
|
||||
// Progress bar
|
||||
progressBar
|
||||
.padding(.top, 6)
|
||||
|
||||
// Playback controls
|
||||
playbackControls
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.frame(width: 420, height: 150)
|
||||
.background {
|
||||
if #available(macOS 26.0, *) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.glassEffect(in: .rect(cornerRadius: 20))
|
||||
.onAppear {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||
print("[\(formatter.string(from: Date()))] LockScreenMusicPanel: ✨ Using macOS 26.0+ Liquid Glass effect")
|
||||
}
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(.ultraThinMaterial)
|
||||
.onAppear {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||
print("[\(formatter.string(from: Date()))] LockScreenMusicPanel: 🪟 Using ultraThinMaterial fallback (macOS < 26.0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
|
||||
.onAppear {
|
||||
sliderValue = musicManager.elapsedTime
|
||||
isActive = true
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||
print("[\(formatter.string(from: Date()))] LockScreenMusicPanel: ✅ View appeared")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Progress Bar
|
||||
|
||||
private var progressBar: some View {
|
||||
TimelineView(.animation(minimumInterval: musicManager.playbackRate > 0 ? 0.1 : nil)) { timeline in
|
||||
let currentElapsed = currentSliderValue(timeline.date)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
// Elapsed time
|
||||
Text(formatTime(dragging ? sliderValue : currentElapsed))
|
||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.frame(width: 42, alignment: .leading)
|
||||
|
||||
// Progress bar
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
// Background track
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.fill(Color.white.opacity(0.2))
|
||||
.frame(height: 6)
|
||||
|
||||
// Filled portion
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.fill(sliderColor)
|
||||
.frame(width: max(0, geometry.size.width * (currentSliderValue(timeline.date) / max(musicManager.songDuration, 1))), height: 6)
|
||||
}
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
dragging = true
|
||||
let newValue = min(max(0, Double(value.location.x / geometry.size.width) * musicManager.songDuration), musicManager.songDuration)
|
||||
sliderValue = newValue
|
||||
lastDragged = Date()
|
||||
}
|
||||
.onEnded { _ in
|
||||
musicManager.seek(to: sliderValue)
|
||||
dragging = false
|
||||
}
|
||||
)
|
||||
}
|
||||
.frame(height: 6)
|
||||
|
||||
// Time remaining
|
||||
Text("-\(formatTime(musicManager.songDuration - (dragging ? sliderValue : currentElapsed)))")
|
||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.frame(width: 48, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
sliderValue = musicManager.elapsedTime
|
||||
}
|
||||
}
|
||||
|
||||
private func currentSliderValue(_ date: Date) -> Double {
|
||||
if dragging {
|
||||
return sliderValue
|
||||
}
|
||||
|
||||
if musicManager.isPlaying {
|
||||
let timeSinceLastUpdate = date.timeIntervalSince(musicManager.timestampDate)
|
||||
let estimatedElapsed = musicManager.elapsedTime + (timeSinceLastUpdate * musicManager.playbackRate)
|
||||
return min(estimatedElapsed, musicManager.songDuration)
|
||||
}
|
||||
|
||||
return musicManager.elapsedTime
|
||||
}
|
||||
|
||||
private var sliderColor: Color {
|
||||
switch Defaults[.sliderColor] {
|
||||
case .white:
|
||||
return .white
|
||||
case .albumArt:
|
||||
return Color(nsColor: musicManager.avgColor)
|
||||
case .accent:
|
||||
return .accentColor
|
||||
}
|
||||
}
|
||||
|
||||
private func formatTime(_ time: TimeInterval) -> String {
|
||||
let minutes = Int(time) / 60
|
||||
let seconds = Int(time) % 60
|
||||
return String(format: "%d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
// MARK: - Playback Controls
|
||||
|
||||
private var playbackControls: some View {
|
||||
HStack(spacing: 20) {
|
||||
// Always show shuffle on lock screen
|
||||
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
||||
Task { await musicManager.toggleShuffle() }
|
||||
}
|
||||
|
||||
controlButton(icon: "backward.fill", size: 18) {
|
||||
Task { await musicManager.previousTrack() }
|
||||
}
|
||||
|
||||
playPauseButton
|
||||
|
||||
controlButton(icon: "forward.fill", size: 18) {
|
||||
Task { await musicManager.nextTrack() }
|
||||
}
|
||||
|
||||
// Always show repeat on lock screen
|
||||
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
||||
Task { await musicManager.toggleRepeat() }
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
private var playPauseButton: some View {
|
||||
Button(action: {
|
||||
Task { await musicManager.togglePlay() }
|
||||
}) {
|
||||
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
||||
.font(.system(size: 24, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: size, weight: .medium))
|
||||
.foregroundColor(isActive ? .red : .white.opacity(0.8))
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
private var repeatIcon: String {
|
||||
switch musicManager.repeatMode {
|
||||
case .off: return "repeat"
|
||||
case .all: return "repeat"
|
||||
case .one: return "repeat.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue