Allow deletion of workouts
This commit is contained in:
parent
7e66f81aa6
commit
ee1993e757
@ -40,6 +40,9 @@ struct WorkoutDetailView: View {
|
|||||||
@State
|
@State
|
||||||
private var isProcessingWorkout = false
|
private var isProcessingWorkout = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var healthButtonText = "Checking for workout in Health..."
|
||||||
|
|
||||||
private var metadataFields: [(key: String, value: Any)] {
|
private var metadataFields: [(key: String, value: Any)] {
|
||||||
workout.metadata.sorted { $0.key }
|
workout.metadata.sorted { $0.key }
|
||||||
}
|
}
|
||||||
@ -51,37 +54,28 @@ struct WorkoutDetailView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if healthWorkout != nil {
|
Button(action: addOrDeleteHealthWorkout) {
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
Text("Matching workout found in Health")
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.listRowBackground(Color.accentColor)
|
|
||||||
} else {
|
|
||||||
Button(action: addWorkoutToHealth) {
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
if isProcessingWorkout {
|
if isProcessingWorkout {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
.padding(.trailing, 10)
|
.padding(.trailing, 10)
|
||||||
Text("Adding workout to health...")
|
}
|
||||||
.foregroundStyle(.accent)
|
VStack {
|
||||||
} else {
|
Text(healthButtonText)
|
||||||
Text("Add workout to health")
|
|
||||||
.foregroundStyle(.accent)
|
.foregroundStyle(.accent)
|
||||||
|
if healthWorkout != nil {
|
||||||
|
Text("Tap to delete")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
.disabled(isProcessingWorkout)
|
.disabled(isProcessingWorkout)
|
||||||
}
|
|
||||||
Section("Info") {
|
Section("Info") {
|
||||||
DetailRow("Start", date: workout.startDate)
|
DetailRow("Start", date: workout.startDate)
|
||||||
DetailRow("Duration", duration: workout.duration)
|
DetailRow("Duration", duration: workout.duration)
|
||||||
@ -180,21 +174,65 @@ struct WorkoutDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addWorkoutToHealth() {
|
private func updateButtonText() {
|
||||||
guard let db = database.store else {
|
if isProcessingWorkout {
|
||||||
|
if healthWorkout == nil {
|
||||||
|
self.healthButtonText = "Adding workout to Health..."
|
||||||
|
} else {
|
||||||
|
self.healthButtonText = "Deleting workout from Health..."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if healthWorkout == nil {
|
||||||
|
healthButtonText = "Add workout to Health"
|
||||||
|
} else {
|
||||||
|
healthButtonText = "Found matching workout in Health"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addOrDeleteHealthWorkout() {
|
||||||
|
guard !isProcessingWorkout else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isProcessingWorkout = true
|
self.isProcessingWorkout = true
|
||||||
|
updateButtonText()
|
||||||
}
|
}
|
||||||
Task {
|
Task {
|
||||||
await insert(workout: workout, using: db)
|
if let healthWorkout {
|
||||||
|
await delete(healthWorkout: healthWorkout)
|
||||||
|
} else {
|
||||||
|
await addWorkoutToHealth()
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isProcessingWorkout = false
|
||||||
|
updateButtonText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func delete(healthWorkout: HKWorkout) async {
|
||||||
|
do {
|
||||||
|
try await store.store.delete(healthWorkout)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.healthWorkout = nil
|
||||||
|
self.isProcessingWorkout = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
show("Failed to delete workout: \(error)")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isProcessingWorkout = false
|
self.isProcessingWorkout = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addWorkoutToHealth() async {
|
||||||
|
guard let db = database.store else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await insert(workout: workout, using: db)
|
||||||
|
}
|
||||||
|
|
||||||
private func requestAuthorization() async -> Bool {
|
private func requestAuthorization() async -> Bool {
|
||||||
do {
|
do {
|
||||||
try await store.requestAuthorization(
|
try await store.requestAuthorization(
|
||||||
@ -222,16 +260,28 @@ struct WorkoutDetailView: View {
|
|||||||
show("No sharing permission for workout routes")
|
show("No sharing permission for workout routes")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let samples: [HKSample]
|
var samples: [HKSample]
|
||||||
do {
|
do {
|
||||||
samples = try db.store.samples(associatedWith: workout)
|
samples = try db.store.samples(associatedWith: workout)
|
||||||
} catch {
|
} catch {
|
||||||
show("Failed to access samples associated with workout: \(error)")
|
show("Failed to access samples associated with workout: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add missing samples from statistics
|
||||||
|
let newSamples = makeStatisticSamples(workout, in: db)
|
||||||
|
for sample in newSamples {
|
||||||
|
guard !samples.contains(where: { $0.sampleType == sample.sampleType }) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let typeName = HKQuantityTypeIdentifier(rawValue: sample.quantityType.identifier).description
|
||||||
|
print("Adding missing sample \(typeName)")
|
||||||
|
samples.append(sample)
|
||||||
|
}
|
||||||
|
|
||||||
let route: WorkoutRoute?
|
let route: WorkoutRoute?
|
||||||
do {
|
do {
|
||||||
route = try db.store.route(associatedWith: workout)
|
route = try db.route(associatedWith: workout)
|
||||||
} catch {
|
} catch {
|
||||||
show("Failed to get route associated with workout: \(error)")
|
show("Failed to get route associated with workout: \(error)")
|
||||||
return
|
return
|
||||||
@ -239,7 +289,7 @@ struct WorkoutDetailView: View {
|
|||||||
let locations: [CLLocation]
|
let locations: [CLLocation]
|
||||||
do {
|
do {
|
||||||
locations = try route.map {
|
locations = try route.map {
|
||||||
try db.store.locations(associatedWith: $0)
|
try db.locations(associatedWith: $0)
|
||||||
} ?? []
|
} ?? []
|
||||||
} catch {
|
} catch {
|
||||||
show("Failed to get locations associated with route: \(error)")
|
show("Failed to get locations associated with route: \(error)")
|
||||||
@ -261,6 +311,22 @@ struct WorkoutDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeStatisticSamples(_ workout: Workout, in db: HealthDatabase) -> [HKQuantitySample] {
|
||||||
|
let startDate = workout.startDate
|
||||||
|
let endDate = workout.endDate
|
||||||
|
let activity = workout.workoutActivities[0]
|
||||||
|
|
||||||
|
do {
|
||||||
|
let statistics = try db.statistics(associatedWith: activity)
|
||||||
|
return statistics.map {
|
||||||
|
.init(type: $0, quantity: $1.average, start: startDate, end: endDate)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Failed to get statistics for activity: \(error)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadSamples() {
|
private func loadSamples() {
|
||||||
Task {
|
Task {
|
||||||
await checkPermissionsAndSearchHealth()
|
await checkPermissionsAndSearchHealth()
|
||||||
@ -318,11 +384,19 @@ struct WorkoutDetailView: View {
|
|||||||
guard HKHealthStore.isHealthDataAvailable() else {
|
guard HKHealthStore.isHealthDataAvailable() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.healthButtonText = "Checking for matching workout..."
|
||||||
|
self.isProcessingWorkout = true
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
try await checkPermissionsAndFindWorkout()
|
try await checkPermissionsAndFindWorkout()
|
||||||
} catch {
|
} catch {
|
||||||
show("Failed to search for similar workout in Health: \(error)")
|
show("Failed to search for similar workout in Health: \(error)")
|
||||||
}
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isProcessingWorkout = false
|
||||||
|
updateButtonText()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkPermissionsAndFindWorkout() async throws {
|
private func checkPermissionsAndFindWorkout() async throws {
|
||||||
|
Loading…
Reference in New Issue
Block a user