iOS 2.5.0 - Apple Music Integration

This commit is contained in:
2025-12-08 15:31:09 -07:00
parent 06bfb02ccc
commit 8154cd24bb
12 changed files with 359 additions and 15 deletions

View File

@@ -58,6 +58,7 @@ struct CalendarView: View {
let gym = dataManager.gym(withId: activeSession.gymId)
{
ActiveSessionBanner(session: activeSession, gym: gym)
.environmentObject(MusicService.shared)
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 16)

View File

@@ -1,10 +1,12 @@
import Combine
import MusicKit
import SwiftUI
struct SessionDetailView: View {
let sessionId: UUID
@EnvironmentObject var dataManager: ClimbingDataManager
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var musicService: MusicService
@Environment(\.dismiss) private var dismiss
@State private var showingDeleteAlert = false
@State private var showingAddAttempt = false
@@ -41,13 +43,19 @@ struct SessionDetailView: View {
Section {
SessionHeaderCard(
session: session, gym: gym, stats: sessionStats)
.listRowInsets(EdgeInsets())
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.padding(.bottom, 8)
if session.status == .active && musicService.isMusicEnabled && musicService.isAuthorized {
MusicControlCard()
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
SessionStatsCard(stats: sessionStats)
.listRowInsets(EdgeInsets())
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
@@ -76,6 +84,7 @@ struct SessionDetailView: View {
)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
} else {
ForEach(attemptsWithProblems.indices, id: \.self) { index in
let (attempt, problem) = attemptsWithProblems[index]
@@ -442,6 +451,53 @@ struct SessionStats {
let uniqueProblemsCompleted: Int
}
struct MusicControlCard: View {
@EnvironmentObject var musicService: MusicService
var body: some View {
HStack {
Image(systemName: "music.note")
.font(.title2)
.foregroundColor(.pink)
.frame(width: 40, height: 40)
.background(Color.pink.opacity(0.1))
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text("Music")
.font(.headline)
if let playlistId = musicService.selectedPlaylistId,
let playlist = musicService.playlists.first(where: { $0.id.rawValue == playlistId }) {
Text(playlist.name)
.font(.caption)
.foregroundColor(.secondary)
} else {
Text("No playlist selected")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
Button(action: {
musicService.togglePlayback()
}) {
Image(systemName: musicService.isPlaying ? "pause.circle.fill" : "play.circle.fill")
.font(.system(size: 32))
.foregroundColor(.pink)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.secondarySystemGroupedBackground))
.shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 2)
)
}
}
#Preview {
NavigationView {
SessionDetailView(sessionId: UUID())

View File

@@ -122,6 +122,7 @@ struct SessionsList: View {
{
Section {
ActiveSessionBanner(session: activeSession, gym: gym)
.environmentObject(MusicService.shared)
.padding(.horizontal, 16)
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
.listRowBackground(Color.clear)
@@ -178,6 +179,17 @@ struct ActiveSessionBanner: View {
let gym: Gym
@EnvironmentObject var dataManager: ClimbingDataManager
@State private var navigateToDetail = false
// Access MusicService via DataManager if possible, or EnvironmentObject if injected
// Since DataManager holds MusicService, we can access it through there if we expose it or inject it.
// In SettingsView we saw .environmentObject(dataManager.musicService).
// We should probably inject it here too or access via dataManager if it's public.
// Let's check ClimbingDataManager again. It has `let musicService = MusicService.shared`.
// But it's not @Published so it won't trigger updates unless we observe the service itself.
// The best way is to use @EnvironmentObject var musicService: MusicService
// and ensure it's injected in the parent view.
@EnvironmentObject var musicService: MusicService
var body: some View {
HStack {
@@ -201,6 +213,19 @@ struct ActiveSessionBanner: View {
.foregroundColor(.secondary)
.monospacedDigit()
}
if musicService.isMusicEnabled && musicService.isAuthorized {
Button(action: {
musicService.togglePlayback()
}) {
HStack(spacing: 4) {
Image(systemName: musicService.isPlaying ? "pause.fill" : "play.fill")
.font(.caption)
}
.foregroundColor(.pink)
.padding(.top, 2)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())

View File

@@ -1,4 +1,5 @@
import HealthKit
import MusicKit
import SwiftUI
import UniformTypeIdentifiers
@@ -21,6 +22,9 @@ struct SettingsView: View {
HealthKitSection()
.environmentObject(dataManager.healthKitService)
MusicSection()
.environmentObject(dataManager.musicService)
AppearanceSection()
DataManagementSection(
@@ -1099,6 +1103,53 @@ struct HealthKitSection: View {
}
}
struct MusicSection: View {
@EnvironmentObject var musicService: MusicService
var body: some View {
Section {
Toggle(isOn: Binding(
get: { musicService.isMusicEnabled },
set: { musicService.toggleMusicEnabled($0) }
)) {
HStack {
Image(systemName: "music.note")
.foregroundColor(.pink)
Text("Apple Music Integration")
}
}
if musicService.isMusicEnabled {
if !musicService.isAuthorized {
Button("Connect Apple Music") {
Task {
await musicService.checkAuthorizationStatus()
}
}
} else {
Toggle("Auto-Play on Session Start", isOn: $musicService.isAutoPlayEnabled)
Toggle("Stop Music on Session End", isOn: $musicService.isAutoStopEnabled)
Picker("Playlist", selection: $musicService.selectedPlaylistId) {
Text("None").tag(nil as String?)
ForEach(musicService.playlists, id: \.id) { playlist in
Text(playlist.name).tag(playlist.id.rawValue as String?)
}
}
if musicService.isAutoPlayEnabled {
Text("Music will only auto-play if headphones are connected when you start a session.")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
} header: {
Text("Music")
}
}
}
#Preview {
SettingsView()
.environmentObject(ClimbingDataManager.preview)