import SwiftUI // MARK: - Rotary Knob /// A hardware-inspired rotary knob control. Drag vertically to adjust value. struct RotaryKnobView: View { @Binding var value: Float // 0...1 var label: String = "" var size: CGFloat = 48 var accentColor: Color = Color(red: 0, green: 0.83, blue: 1.0) @State private var isDragging = false @State private var dragStartValue: Float = 0 // Knob rotates from -135deg to +135deg (270deg total range) private var rotationAngle: Double { Double(value) * 270 - 135 } var body: some View { VStack(spacing: 4) { ZStack { // Outer ring — brushed metal Circle() .fill( AngularGradient( colors: [ Color(white: 0.25), Color(white: 0.35), Color(white: 0.20), Color(white: 0.30), Color(white: 0.25), ], center: .center ) ) .frame(width: size, height: size) // Value arc Circle() .trim(from: 0, to: CGFloat(value) * 0.75) .rotation(.degrees(135)) .stroke( accentColor, style: StrokeStyle(lineWidth: 3, lineCap: .round) ) .frame(width: size + 6, height: size + 6) .shadow(color: accentColor.opacity(0.6), radius: 4) // Inner circle — darker center Circle() .fill( RadialGradient( colors: [Color(white: 0.18), Color(white: 0.12)], center: .center, startRadius: 0, endRadius: size * 0.35 ) ) .frame(width: size * 0.7, height: size * 0.7) // Position indicator line Rectangle() .fill(accentColor) .frame(width: 2, height: size * 0.25) .offset(y: -size * 0.2) .rotationEffect(.degrees(rotationAngle)) .shadow(color: accentColor.opacity(0.8), radius: 2) } .gesture( DragGesture(minimumDistance: 1) .onChanged { gesture in if !isDragging { isDragging = true dragStartValue = value } // Vertical drag: up = increase, down = decrease let delta = Float(-gesture.translation.height / 150) value = max(0, min(1, dragStartValue + delta)) } .onEnded { _ in isDragging = false } ) if !label.isEmpty { Text(label) .font(.system(size: 9, weight: .bold, design: .monospaced)) .foregroundStyle(Color(white: 0.5)) } } } } // MARK: - VU Meter /// Vertical LED-strip level meter. Green → Yellow → Red. struct VUMeterView: View { var level: Float // 0...1 var segmentCount: Int = 12 var width: CGFloat = 8 var height: CGFloat = 60 var body: some View { VStack(spacing: 1.5) { ForEach((0.. threshold let color = segmentColor(index: index) RoundedRectangle(cornerRadius: 1) .fill(isLit ? color : color.opacity(0.15)) .frame(width: width, height: max(2, height / CGFloat(segmentCount) - 1.5)) .shadow(color: isLit ? color.opacity(0.5) : .clear, radius: 2) } } } private func segmentColor(index: Int) -> Color { let ratio = Float(index) / Float(segmentCount) if ratio >= 0.83 { return Color(red: 1.0, green: 0.1, blue: 0.1) } // Red if ratio >= 0.66 { return Color(red: 1.0, green: 0.8, blue: 0.0) } // Yellow return Color(red: 0.0, green: 0.85, blue: 0.4) // Green } } // MARK: - LED Display /// Seven-segment-style glowing text display for BPM, time, key. struct LEDDisplay: View { let text: String var fontSize: CGFloat = 16 var color: Color = Color(red: 0, green: 0.83, blue: 1.0) var alignment: Alignment = .center var body: some View { Text(text) .font(.system(size: fontSize, weight: .bold, design: .monospaced)) .foregroundStyle(color) .shadow(color: color.opacity(0.7), radius: 4) .shadow(color: color.opacity(0.3), radius: 8) .frame(maxWidth: .infinity, alignment: alignment) .padding(.horizontal, 6) .padding(.vertical, 3) .background( RoundedRectangle(cornerRadius: 4) .fill(Color(white: 0.05)) .overlay( RoundedRectangle(cornerRadius: 4) .stroke(Color(white: 0.15), lineWidth: 0.5) ) ) } } // MARK: - DJ Transport Button /// Hardware-inspired raised button for transport controls. struct DJTransportButton: View { let icon: String var size: ButtonSize = .regular var isActive: Bool = false var accentColor: Color = Color(red: 0, green: 0.83, blue: 1.0) let action: () -> Void enum ButtonSize { case small, regular, large var dimension: CGFloat { switch self { case .small: 28 case .regular: 36 case .large: 52 } } var iconSize: CGFloat { switch self { case .small: 12 case .regular: 16 case .large: 24 } } } @State private var isPressed = false var body: some View { Button(action: action) { Image(systemName: icon) .font(.system(size: size.iconSize, weight: .semibold)) .foregroundStyle(isActive ? accentColor : Color(white: 0.7)) .frame(width: size.dimension, height: size.dimension) .background( ZStack { // Base RoundedRectangle(cornerRadius: size.dimension * 0.2) .fill( LinearGradient( colors: isPressed ? [Color(white: 0.12), Color(white: 0.16)] : [Color(white: 0.22), Color(white: 0.14)], startPoint: .top, endPoint: .bottom ) ) // Border RoundedRectangle(cornerRadius: size.dimension * 0.2) .stroke( LinearGradient( colors: [Color(white: 0.3), Color(white: 0.1)], startPoint: .top, endPoint: .bottom ), lineWidth: 1 ) } ) .shadow( color: isActive ? accentColor.opacity(0.3) : .clear, radius: 4 ) .shadow( color: Color.black.opacity(isPressed ? 0 : 0.5), radius: isPressed ? 0 : 2, y: isPressed ? 0 : 2 ) .scaleEffect(isPressed ? 0.95 : 1.0) .animation(.easeOut(duration: 0.1), value: isPressed) } .buttonStyle(.plain) .onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in isPressed = pressing }, perform: {}) } } // MARK: - Fader /// Vertical fader for EQ bands. Drag to adjust. struct FaderView: View { @Binding var value: Float // -1...1 for EQ, or 0...1 for volume var label: String = "" var range: ClosedRange = -1...1 var height: CGFloat = 80 var accentColor: Color = Color(red: 0, green: 0.83, blue: 1.0) @State private var isDragging = false @State private var dragStartValue: Float = 0 private var normalizedValue: CGFloat { CGFloat((value - range.lowerBound) / (range.upperBound - range.lowerBound)) } var body: some View { VStack(spacing: 4) { ZStack(alignment: .bottom) { // Track groove RoundedRectangle(cornerRadius: 2) .fill(Color(white: 0.08)) .frame(width: 6, height: height) .overlay( RoundedRectangle(cornerRadius: 2) .stroke(Color(white: 0.2), lineWidth: 0.5) ) // Value fill RoundedRectangle(cornerRadius: 2) .fill(accentColor.opacity(0.5)) .frame(width: 6, height: height * normalizedValue) // Fader cap RoundedRectangle(cornerRadius: 3) .fill( LinearGradient( colors: [Color(white: 0.45), Color(white: 0.25)], startPoint: .top, endPoint: .bottom ) ) .frame(width: 20, height: 12) .shadow(color: .black.opacity(0.4), radius: 2, y: 1) .offset(y: -height * normalizedValue + 6) } .frame(width: 20, height: height) .gesture( DragGesture(minimumDistance: 1) .onChanged { gesture in if !isDragging { isDragging = true dragStartValue = value } let delta = Float(-gesture.translation.height / height) * (range.upperBound - range.lowerBound) value = max(range.lowerBound, min(range.upperBound, dragStartValue + delta)) } .onEnded { _ in isDragging = false } ) if !label.isEmpty { Text(label) .font(.system(size: 8, weight: .bold, design: .monospaced)) .foregroundStyle(Color(white: 0.5)) } } } } // MARK: - DJ Section Background /// Textured surface for grouping DJ controls. struct DJSectionBackground: View { var cornerRadius: CGFloat = 8 var body: some View { RoundedRectangle(cornerRadius: cornerRadius) .fill( LinearGradient( colors: [ Color(red: 0.06, green: 0.06, blue: 0.08), Color(red: 0.04, green: 0.04, blue: 0.06), ], startPoint: .top, endPoint: .bottom ) ) .overlay( RoundedRectangle(cornerRadius: cornerRadius) .stroke( LinearGradient( colors: [Color(white: 0.15), Color(white: 0.05)], startPoint: .top, endPoint: .bottom ), lineWidth: 1 ) ) .shadow(color: .black.opacity(0.5), radius: 4, y: 2) } } // MARK: - Vinyl Spin Animation /// Album art with vinyl grooves that spins during playback. struct VinylSpinView: View { let trackTitle: String let artworkView: AnyView? var isPlaying: Bool var size: CGFloat = 120 @State private var rotation: Double = 0 var body: some View { ZStack { // Vinyl disc Circle() .fill( RadialGradient( colors: [ Color(white: 0.08), Color(white: 0.04), Color(white: 0.06), Color(white: 0.03), Color(white: 0.05), ], center: .center, startRadius: size * 0.2, endRadius: size * 0.5 ) ) .frame(width: size, height: size) // Grooves (concentric rings) ForEach(0..<6, id: \.self) { ring in let ringRadius = size * 0.22 + CGFloat(ring) * (size * 0.045) Circle() .stroke(Color(white: 0.1), lineWidth: 0.5) .frame(width: ringRadius * 2, height: ringRadius * 2) } // Center label (album art or placeholder) if let artwork = artworkView { artwork .frame(width: size * 0.35, height: size * 0.35) .clipShape(Circle()) } else { Circle() .fill(Color(white: 0.12)) .frame(width: size * 0.35, height: size * 0.35) .overlay( Text(String(trackTitle.prefix(2)).uppercased()) .font(.system(size: size * 0.08, weight: .bold, design: .monospaced)) .foregroundStyle(Color(white: 0.4)) ) } // Spindle hole Circle() .fill(Color(white: 0.02)) .frame(width: size * 0.05, height: size * 0.05) } .rotationEffect(.degrees(rotation)) .onChange(of: isPlaying) { _, playing in if playing { withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) { rotation += 360 } } else { // Stop smoothly — remove repeating animation withAnimation(.easeOut(duration: 0.5)) { // Keep current rotation (no reset) } } } .onAppear { if isPlaying { withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) { rotation = 360 } } } } }