Add Spotify integration with toggleable music source
- Add OAuth 2.0 PKCE authentication for Spotify Web API - Create SpotifyNowPlayingMonitor for polling current track - Add Settings tab with music source toggle (Apple Music/Spotify) - Store tokens securely in Keychain - Display current track on Glass as NOW_PLAYING card 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
151
IrisCompanion/iris/Views/SettingsView.swift
Normal file
151
IrisCompanion/iris/Views/SettingsView.swift
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// iris
|
||||
//
|
||||
// Settings UI for music source selection and Spotify connection.
|
||||
//
|
||||
|
||||
import MusicKit
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject private var orchestrator: ContextOrchestrator
|
||||
@EnvironmentObject private var spotifyAuth: SpotifyAuthManager
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("Music Source") {
|
||||
Picker("Source", selection: Binding(
|
||||
get: { orchestrator.musicSource },
|
||||
set: { newValue in
|
||||
if newValue == .spotify && !spotifyAuth.isConnected {
|
||||
return
|
||||
}
|
||||
orchestrator.musicSource = newValue
|
||||
}
|
||||
)) {
|
||||
ForEach(MusicSource.allCases, id: \.self) { source in
|
||||
Text(source.displayName).tag(source)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
if !spotifyAuth.isConnected {
|
||||
Text("Connect Spotify below to enable it as a source")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
if spotifyAuth.isConnected {
|
||||
HStack {
|
||||
Label("Connected", systemImage: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Spacer()
|
||||
}
|
||||
Button("Disconnect", role: .destructive) {
|
||||
spotifyAuth.disconnect()
|
||||
if orchestrator.musicSource == .spotify {
|
||||
orchestrator.musicSource = .appleMusic
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
spotifyAuth.startAuth()
|
||||
} label: {
|
||||
HStack {
|
||||
Label("Connect to Spotify", systemImage: "link")
|
||||
Spacer()
|
||||
if spotifyAuth.isAuthenticating {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(spotifyAuth.isAuthenticating)
|
||||
}
|
||||
|
||||
if let error = spotifyAuth.error {
|
||||
Text(error)
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
} header: {
|
||||
Text("Spotify")
|
||||
} footer: {
|
||||
if !spotifyAuth.isConnected {
|
||||
Text("Connect your Spotify account to display current track on Glass.")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("Authorization")
|
||||
Spacer()
|
||||
Text(authStatusText(orchestrator.musicAuthorization))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Apple Music")
|
||||
}
|
||||
|
||||
Section {
|
||||
if let nowPlayingInfo = currentNowPlaying {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(nowPlayingInfo.title)
|
||||
.font(.headline)
|
||||
if let artist = nowPlayingInfo.artist {
|
||||
Text(artist)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Nothing playing")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Now Playing")
|
||||
} footer: {
|
||||
Text("Source: \(orchestrator.musicSource.displayName)")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
private var currentNowPlaying: (title: String, artist: String?)? {
|
||||
switch orchestrator.musicSource {
|
||||
case .appleMusic:
|
||||
guard let np = orchestrator.nowPlaying else { return nil }
|
||||
return (np.title, np.artist)
|
||||
case .spotify:
|
||||
guard let np = orchestrator.spotifyNowPlaying else { return nil }
|
||||
return (np.title, np.artist)
|
||||
}
|
||||
}
|
||||
|
||||
private func authStatusText(_ status: MusicAuthorization.Status) -> String {
|
||||
switch status {
|
||||
case .notDetermined: return "Not Determined"
|
||||
case .denied: return "Denied"
|
||||
case .restricted: return "Restricted"
|
||||
case .authorized: return "Authorized"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ble = BlePeripheralManager()
|
||||
let spotifyAuth = SpotifyAuthManager()
|
||||
let orchestrator = ContextOrchestrator(ble: ble, spotifyAuth: spotifyAuth)
|
||||
SettingsView()
|
||||
.environmentObject(orchestrator)
|
||||
.environmentObject(spotifyAuth)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user