Remove music
All checks were successful
Ascently - Docs Deploy / build-and-push (push) Successful in 3m57s
All checks were successful
Ascently - Docs Deploy / build-and-push (push) Successful in 3m57s
This commit is contained in:
@@ -25,9 +25,9 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
"@astrojs/starlight": "^0.37.2",
|
||||
"astro": "^5.16.8",
|
||||
"@astrojs/node": "^9.5.2",
|
||||
"@astrojs/starlight": "^0.37.5",
|
||||
"astro": "^5.17.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
|
||||
644
docs/pnpm-lock.yaml
generated
644
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -44,7 +44,6 @@ struct ContentView: View {
|
||||
.tag(4)
|
||||
}
|
||||
.environmentObject(dataManager)
|
||||
.environmentObject(MusicService.shared)
|
||||
.onChange(of: scenePhase) { _, newPhase in
|
||||
if newPhase == .active {
|
||||
// Add slight delay to ensure app is fully loaded
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import AVFoundation
|
||||
import Combine
|
||||
import MusicKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
class MusicService: ObservableObject {
|
||||
static let shared = MusicService()
|
||||
|
||||
@Published var isAuthorized = false
|
||||
@Published var playlists: MusicItemCollection<Playlist> = []
|
||||
@Published var selectedPlaylistId: String? {
|
||||
didSet {
|
||||
UserDefaults.standard.set(selectedPlaylistId, forKey: "ascently_selected_playlist_id")
|
||||
}
|
||||
}
|
||||
@Published var isMusicEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(isMusicEnabled, forKey: "ascently_music_enabled")
|
||||
if !isMusicEnabled {
|
||||
// Genuinely unsure what I want to do with this but we should account for it at some point
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var isAutoPlayEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(isAutoPlayEnabled, forKey: "ascently_music_autoplay_enabled")
|
||||
}
|
||||
}
|
||||
@Published var isAutoStopEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(isAutoStopEnabled, forKey: "ascently_music_autostop_enabled")
|
||||
}
|
||||
}
|
||||
@Published var isPlaying = false
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var hasStartedSessionPlayback = false
|
||||
private var currentPlaylistTrackIds: Set<MusicItemID> = []
|
||||
|
||||
private init() {
|
||||
self.selectedPlaylistId = UserDefaults.standard.string(forKey: "ascently_selected_playlist_id")
|
||||
self.isMusicEnabled = UserDefaults.standard.bool(forKey: "ascently_music_enabled")
|
||||
self.isAutoPlayEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autoplay_enabled")
|
||||
self.isAutoStopEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autostop_enabled")
|
||||
|
||||
if isMusicEnabled {
|
||||
Task {
|
||||
await checkAuthorizationStatus()
|
||||
}
|
||||
}
|
||||
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
SystemMusicPlayer.shared.state.objectWillChange
|
||||
.sink { [weak self] _ in
|
||||
self?.updatePlaybackStatus()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
SystemMusicPlayer.shared.queue.objectWillChange
|
||||
.sink { [weak self] _ in
|
||||
self?.checkQueueConsistency()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updatePlaybackStatus() {
|
||||
isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
|
||||
}
|
||||
|
||||
private func checkQueueConsistency() {
|
||||
guard hasStartedSessionPlayback else { return }
|
||||
|
||||
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
|
||||
let item = currentEntry.item {
|
||||
if !currentPlaylistTrackIds.isEmpty && !currentPlaylistTrackIds.contains(item.id) {
|
||||
hasStartedSessionPlayback = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toggleMusicEnabled(_ enabled: Bool) {
|
||||
isMusicEnabled = enabled
|
||||
if enabled {
|
||||
Task {
|
||||
await checkAuthorizationStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkAuthorizationStatus() async {
|
||||
let status = await MusicAuthorization.request()
|
||||
self.isAuthorized = status == .authorized
|
||||
if isAuthorized {
|
||||
await fetchPlaylists()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPlaylists() async {
|
||||
guard isAuthorized else { return }
|
||||
do {
|
||||
var request = MusicLibraryRequest<Playlist>()
|
||||
request.sort(by: \.name, ascending: true)
|
||||
let response = try await request.response()
|
||||
self.playlists = response.items
|
||||
} catch {
|
||||
print("Error fetching playlists: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func playSelectedPlaylistIfHeadphonesConnected() {
|
||||
guard isMusicEnabled, isAutoPlayEnabled, let playlistId = selectedPlaylistId else { return }
|
||||
|
||||
if isHeadphonesConnected() {
|
||||
playPlaylist(id: playlistId)
|
||||
}
|
||||
}
|
||||
|
||||
func resetSessionPlaybackState() {
|
||||
hasStartedSessionPlayback = false
|
||||
currentPlaylistTrackIds.removeAll()
|
||||
}
|
||||
|
||||
func playPlaylist(id: String) {
|
||||
print("Attempting to play playlist \(id)")
|
||||
Task {
|
||||
do {
|
||||
if playlists.isEmpty {
|
||||
await fetchPlaylists()
|
||||
}
|
||||
|
||||
var targetPlaylist: Playlist?
|
||||
|
||||
if let playlist = playlists.first(where: { $0.id.rawValue == id }) {
|
||||
targetPlaylist = playlist
|
||||
} else {
|
||||
var request = MusicLibraryRequest<Playlist>()
|
||||
request.filter(matching: \.id, equalTo: MusicItemID(id))
|
||||
let response = try await request.response()
|
||||
targetPlaylist = response.items.first
|
||||
}
|
||||
|
||||
if let playlist = targetPlaylist {
|
||||
let detailedPlaylist = try await playlist.with([.tracks])
|
||||
if let tracks = detailedPlaylist.tracks {
|
||||
self.currentPlaylistTrackIds = Set(tracks.map { $0.id })
|
||||
}
|
||||
|
||||
SystemMusicPlayer.shared.queue = [playlist]
|
||||
try await SystemMusicPlayer.shared.play()
|
||||
hasStartedSessionPlayback = true
|
||||
}
|
||||
} catch {
|
||||
print("Error playing playlist: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopPlaybackIfEnabled() {
|
||||
guard isMusicEnabled, isAutoStopEnabled else { return }
|
||||
SystemMusicPlayer.shared.stop()
|
||||
}
|
||||
|
||||
func togglePlayback() {
|
||||
Task {
|
||||
if isPlaying {
|
||||
SystemMusicPlayer.shared.pause()
|
||||
} else {
|
||||
if let playlistId = selectedPlaylistId, !hasStartedSessionPlayback {
|
||||
playPlaylist(id: playlistId)
|
||||
} else {
|
||||
try? await SystemMusicPlayer.shared.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isHeadphonesConnected() -> Bool {
|
||||
let route = AVAudioSession.sharedInstance().currentRoute
|
||||
return route.outputs.contains { port in
|
||||
port.portType == .headphones ||
|
||||
port.portType == .bluetoothA2DP ||
|
||||
port.portType == .bluetoothLE ||
|
||||
port.portType == .bluetoothHFP ||
|
||||
port.portType == .usbAudio ||
|
||||
port.portType == .airPlay
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
let syncService = SyncService()
|
||||
let healthKitService = HealthKitService.shared
|
||||
let musicService = MusicService.shared
|
||||
|
||||
@Published var isSyncing = false
|
||||
private enum Keys {
|
||||
@@ -427,8 +426,7 @@ class ClimbingDataManager: ObservableObject {
|
||||
gymName: gym.name)
|
||||
}
|
||||
|
||||
musicService.resetSessionPlaybackState()
|
||||
musicService.playSelectedPlaylistIfHeadphonesConnected()
|
||||
|
||||
|
||||
if healthKitService.isEnabled {
|
||||
do {
|
||||
@@ -470,10 +468,7 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
await LiveActivityManager.shared.endLiveActivity()
|
||||
|
||||
if UserDefaults.standard.bool(forKey: "isAutoStopMusicEnabled") {
|
||||
musicService.stopPlaybackIfEnabled()
|
||||
}
|
||||
musicService.stopPlaybackIfEnabled()
|
||||
|
||||
|
||||
if healthKitService.isEnabled {
|
||||
do {
|
||||
|
||||
@@ -58,7 +58,6 @@ 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)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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
|
||||
@@ -47,12 +45,7 @@ struct SessionDetailView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
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(top: 6, leading: 16, bottom: 6, trailing: 16))
|
||||
@@ -449,52 +442,7 @@ 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 {
|
||||
|
||||
@@ -122,7 +122,6 @@ 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)
|
||||
@@ -184,7 +183,7 @@ struct ActiveSessionBanner: View {
|
||||
let session: ClimbSession
|
||||
let gym: Gym
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@EnvironmentObject var musicService: MusicService
|
||||
|
||||
@State private var navigateToDetail = false
|
||||
|
||||
var body: some View {
|
||||
@@ -210,18 +209,7 @@ struct ActiveSessionBanner: View {
|
||||
.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())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import HealthKit
|
||||
import MusicKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
@@ -24,8 +23,7 @@ struct SettingsView: View {
|
||||
HealthKitSection()
|
||||
.environmentObject(dataManager.healthKitService)
|
||||
|
||||
MusicSection()
|
||||
.environmentObject(dataManager.musicService)
|
||||
|
||||
|
||||
AppearanceSection()
|
||||
|
||||
@@ -1282,52 +1280,7 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user