import ActivityKit import WidgetKit import SwiftUI import AppIntents struct SkipTrackIntent: AppIntent { static let title: LocalizedStringResource = "Skip Track" @MainActor func perform() async throws -> some IntentResult & OpensIntent { let url = URL(string: "tabatago://skipTrack")! return .result(opensIntent: OpenURLIntent(url)) } } struct MusicLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: MusicActivityAttributes.self) { context in // ── Lock Screen / Banner ─────────────────── VStack(spacing: 0) { HStack(spacing: 10) { Image(systemName: "music.note") .font(.title3.weight(.semibold)) .foregroundStyle(.green) VStack(alignment: .leading, spacing: 2) { Text(context.state.title) .font(.headline) .foregroundStyle(.primary) .lineLimit(1) Text(context.state.artist) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } Spacer() if context.state.isPlaying { LiveActivityMusicBars() } } .padding(.horizontal, 16) .padding(.vertical, 12) } .activityBackgroundTint(.black.opacity(0.9)) .activitySystemActionForegroundColor(.white) } dynamicIsland: { context in DynamicIsland { // ── Expanded ──────────────────────────── DynamicIslandExpandedRegion(.leading) { HStack(spacing: 6) { Image(systemName: "music.note") .foregroundStyle(.green) Text(context.state.title) .font(.caption.weight(.semibold)) .lineLimit(1) if context.state.isPlaying { LiveActivityMusicBars() } } .padding(.leading, 8) } DynamicIslandExpandedRegion(.center) { Text(context.state.artist) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) } DynamicIslandExpandedRegion(.trailing) { Button(intent: SkipTrackIntent()) { HStack(spacing: 3) { Image(systemName: "forward.fill") .font(.system(size: 10, weight: .semibold)) Text("Skip") .font(.caption2.weight(.medium)) } .foregroundStyle(.white.opacity(0.6)) .padding(.horizontal, 8) .padding(.vertical, 3) .background(.white.opacity(0.15)) .clipShape(Capsule()) } .buttonStyle(.plain) .padding(.trailing, 8) } } compactLeading: { // ── Compact Leading ───────────────────── Image(systemName: "music.note") .foregroundStyle(.green) .font(.system(size: 14)) } compactTrailing: { // ── Compact Trailing ──────────────────── HStack(spacing: 3) { Image(systemName: "music.note") .font(.system(size: 10)) .foregroundStyle(.green) Text(context.state.title) .font(.system(size: 10, weight: .medium)) .lineLimit(1) } } minimal: { // ── Minimal ───────────────────────────── Image(systemName: "music.note") .foregroundStyle(.green) .font(.system(size: 13)) } .widgetURL(URL(string: "tabatago://music")!) } } } // MARK: - LiveActivityMusicBars struct LiveActivityMusicBars: View { var body: some View { TimelineView(.periodic(from: .now, by: 0.45 / Double(barSpeeds.count))) { context in HStack(spacing: 2) { ForEach(0..<4, id: \.self) { i in let t = context.date.timeIntervalSinceReferenceDate let phase = t * barSpeeds[i] * 2 * .pi let h = barMin + (barMaxHeights[i] - barMin) * abs(sin(phase)) Capsule() .fill(.green) .frame(width: 2, height: h) } } } } } private let barMaxHeights: [CGFloat] = [8, 14, 6, 12] private let barMin: CGFloat = 3 private let barSpeeds: [Double] = [2.2, 1.8, 2.8, 1.5]