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"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.5.1",
|
"@astrojs/node": "^9.5.2",
|
||||||
"@astrojs/starlight": "^0.37.2",
|
"@astrojs/starlight": "^0.37.5",
|
||||||
"astro": "^5.16.8",
|
"astro": "^5.17.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.34.5"
|
"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)
|
.tag(4)
|
||||||
}
|
}
|
||||||
.environmentObject(dataManager)
|
.environmentObject(dataManager)
|
||||||
.environmentObject(MusicService.shared)
|
|
||||||
.onChange(of: scenePhase) { _, newPhase in
|
.onChange(of: scenePhase) { _, newPhase in
|
||||||
if newPhase == .active {
|
if newPhase == .active {
|
||||||
// Add slight delay to ensure app is fully loaded
|
// 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 syncService = SyncService()
|
||||||
let healthKitService = HealthKitService.shared
|
let healthKitService = HealthKitService.shared
|
||||||
let musicService = MusicService.shared
|
|
||||||
|
|
||||||
@Published var isSyncing = false
|
@Published var isSyncing = false
|
||||||
private enum Keys {
|
private enum Keys {
|
||||||
@@ -427,8 +426,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
gymName: gym.name)
|
gymName: gym.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
musicService.resetSessionPlaybackState()
|
|
||||||
musicService.playSelectedPlaylistIfHeadphonesConnected()
|
|
||||||
|
|
||||||
if healthKitService.isEnabled {
|
if healthKitService.isEnabled {
|
||||||
do {
|
do {
|
||||||
@@ -470,10 +468,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
await LiveActivityManager.shared.endLiveActivity()
|
await LiveActivityManager.shared.endLiveActivity()
|
||||||
|
|
||||||
if UserDefaults.standard.bool(forKey: "isAutoStopMusicEnabled") {
|
|
||||||
musicService.stopPlaybackIfEnabled()
|
|
||||||
}
|
|
||||||
musicService.stopPlaybackIfEnabled()
|
|
||||||
|
|
||||||
if healthKitService.isEnabled {
|
if healthKitService.isEnabled {
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ struct CalendarView: View {
|
|||||||
let gym = dataManager.gym(withId: activeSession.gymId)
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
||||||
{
|
{
|
||||||
ActiveSessionBanner(session: activeSession, gym: gym)
|
ActiveSessionBanner(session: activeSession, gym: gym)
|
||||||
.environmentObject(MusicService.shared)
|
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import MusicKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SessionDetailView: View {
|
struct SessionDetailView: View {
|
||||||
let sessionId: UUID
|
let sessionId: UUID
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
@EnvironmentObject var musicService: MusicService
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var showingDeleteAlert = false
|
@State private var showingDeleteAlert = false
|
||||||
@State private var showingAddAttempt = false
|
@State private var showingAddAttempt = false
|
||||||
@@ -47,12 +45,7 @@ struct SessionDetailView: View {
|
|||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.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)
|
SessionStatsCard(stats: sessionStats)
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
|
.listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16))
|
||||||
@@ -449,52 +442,7 @@ struct SessionStats {
|
|||||||
let uniqueProblemsCompleted: Int
|
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 {
|
#Preview {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ struct SessionsList: View {
|
|||||||
{
|
{
|
||||||
Section {
|
Section {
|
||||||
ActiveSessionBanner(session: activeSession, gym: gym)
|
ActiveSessionBanner(session: activeSession, gym: gym)
|
||||||
.environmentObject(MusicService.shared)
|
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
@@ -184,7 +183,7 @@ struct ActiveSessionBanner: View {
|
|||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
let gym: Gym
|
let gym: Gym
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
@EnvironmentObject var musicService: MusicService
|
|
||||||
@State private var navigateToDetail = false
|
@State private var navigateToDetail = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -210,18 +209,7 @@ struct ActiveSessionBanner: View {
|
|||||||
.monospacedDigit()
|
.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)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import HealthKit
|
import HealthKit
|
||||||
import MusicKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
@@ -24,8 +23,7 @@ struct SettingsView: View {
|
|||||||
HealthKitSection()
|
HealthKitSection()
|
||||||
.environmentObject(dataManager.healthKitService)
|
.environmentObject(dataManager.healthKitService)
|
||||||
|
|
||||||
MusicSection()
|
|
||||||
.environmentObject(dataManager.musicService)
|
|
||||||
|
|
||||||
AppearanceSection()
|
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 {
|
#Preview {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
|||||||
Reference in New Issue
Block a user