Fixed Widget stats

This commit is contained in:
2025-09-15 23:27:21 -06:00
parent 363fbd676a
commit b53dfb1aa5
13 changed files with 94 additions and 157 deletions

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>1.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>NSExtension</key> <key>NSExtension</key>

View File

@@ -21,7 +21,7 @@ final class LiveActivityManager {
do { do {
currentActivity = try Activity<SessionActivityAttributes>.request( currentActivity = try Activity<SessionActivityAttributes>.request(
attributes: attributes, attributes: attributes,
contentState: initialContentState, content: .init(state: initialContentState, staleDate: nil),
pushType: nil pushType: nil
) )
} catch { } catch {
@@ -38,13 +38,13 @@ final class LiveActivityManager {
totalAttempts: totalAttempts, totalAttempts: totalAttempts,
completedProblems: completedProblems completedProblems: completedProblems
) )
await currentActivity.update(using: updatedContentState, alertConfiguration: nil) await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
} }
/// Call this when a ClimbSession ends to end the Live Activity /// Call this when a ClimbSession ends to end the Live Activity
func endLiveActivity() async { func endLiveActivity() async {
guard let currentActivity else { return } guard let currentActivity else { return }
await currentActivity.end(using: nil, dismissalPolicy: .immediate) await currentActivity.end(nil, dismissalPolicy: .immediate)
self.currentActivity = nil self.currentActivity = nil
} }
} }

View File

@@ -477,7 +477,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -506,7 +506,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@@ -12,7 +12,7 @@
<key>SessionStatusLiveExtension.xcscheme_^#shared#^_</key> <key>SessionStatusLiveExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>1</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>

View File

@@ -43,8 +43,8 @@ struct ContentView: View {
.tag(4) .tag(4)
} }
.environmentObject(dataManager) .environmentObject(dataManager)
.onChange(of: scenePhase) { newPhase in .onChange(of: scenePhase) {
if newPhase == .active { if scenePhase == .active {
dataManager.onAppBecomeActive() dataManager.onAppBecomeActive()
} }
} }

View File

@@ -1,4 +1,3 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
@@ -493,13 +492,14 @@ struct Attempt: Identifiable, Codable, Hashable {
} }
func updated( func updated(
result: AttemptResult? = nil, highestHold: String? = nil, notes: String? = nil, problemId: UUID? = nil, result: AttemptResult? = nil, highestHold: String? = nil,
notes: String? = nil,
duration: Int? = nil, restTime: Int? = nil duration: Int? = nil, restTime: Int? = nil
) -> Attempt { ) -> Attempt {
return Attempt( return Attempt(
id: self.id, id: self.id,
sessionId: self.sessionId, sessionId: self.sessionId,
problemId: self.problemId, problemId: problemId ?? self.problemId,
result: result ?? self.result, result: result ?? self.result,
highestHold: highestHold ?? self.highestHold, highestHold: highestHold ?? self.highestHold,
notes: notes ?? self.notes, notes: notes ?? self.notes,

View File

@@ -1,4 +1,3 @@
import Combine import Combine
import SwiftUI import SwiftUI
@@ -82,9 +81,9 @@ struct IconAppearanceModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.onChange(of: colorScheme) { _, newColorScheme in .onChange(of: colorScheme) {
iconHelper.updateDarkModeStatus(for: newColorScheme) iconHelper.updateDarkModeStatus(for: colorScheme)
onChange(iconHelper.getRecommendedIconVariant(for: newColorScheme)) onChange(iconHelper.getRecommendedIconVariant(for: colorScheme))
} }
.onAppear { .onAppear {
iconHelper.updateDarkModeStatus(for: colorScheme) iconHelper.updateDarkModeStatus(for: colorScheme)

View File

@@ -1020,7 +1020,7 @@ extension ClimbingDataManager {
private func updateLiveActivityForActiveSession() { private func updateLiveActivityForActiveSession() {
guard let activeSession = activeSession, guard let activeSession = activeSession,
activeSession.status == .active, activeSession.status == .active,
let gym = gym(withId: activeSession.gymId) let _ = gym(withId: activeSession.gymId)
else { else {
return return
} }

View File

@@ -45,7 +45,7 @@ final class LiveActivityManager {
do { do {
let activity = try Activity<SessionActivityAttributes>.request( let activity = try Activity<SessionActivityAttributes>.request(
attributes: attributes, attributes: attributes,
contentState: initialContentState, content: .init(state: initialContentState, staleDate: nil),
pushType: nil pushType: nil
) )
self.currentActivity = activity self.currentActivity = activity
@@ -81,12 +81,8 @@ final class LiveActivityManager {
completedProblems: completedProblems completedProblems: completedProblems
) )
do { await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
await currentActivity.update(using: updatedContentState, alertConfiguration: nil) print("✅ Live Activity updated successfully")
print("✅ Live Activity updated successfully")
} catch {
print("❌ Failed to update live activity: \(error)")
}
} }
/// Call this when a ClimbSession ends to end the Live Activity /// Call this when a ClimbSession ends to end the Live Activity
@@ -98,14 +94,9 @@ final class LiveActivityManager {
print("🔴 Ending Live Activity: \(currentActivity.id)") print("🔴 Ending Live Activity: \(currentActivity.id)")
do { await currentActivity.end(nil, dismissalPolicy: .immediate)
await currentActivity.end(using: nil, dismissalPolicy: .immediate) self.currentActivity = nil
self.currentActivity = nil print("✅ Live Activity ended successfully")
print("✅ Live Activity ended successfully")
} catch {
print("❌ Failed to end live activity: \(error)")
self.currentActivity = nil
}
} }
/// Check if Live Activities are available and authorized /// Check if Live Activities are available and authorized

View File

@@ -1,4 +1,3 @@
import SwiftUI import SwiftUI
struct AddAttemptView: View { struct AddAttemptView: View {
@@ -610,36 +609,25 @@ struct EditAttemptView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
Form { Form {
Section("Problem") { Section("Select Problem") {
if availableProblems.isEmpty { if availableProblems.isEmpty {
Text("No problems available") Text("No problems available")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} else { } else {
ForEach(availableProblems, id: \.id) { problem in LazyVGrid(
HStack { columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
VStack(alignment: .leading, spacing: 4) { spacing: 8
Text(problem.name ?? "Unnamed Problem") ) {
.font(.headline) ForEach(availableProblems, id: \.id) { problem in
ProblemSelectionCard(
Text( problem: problem,
"\(problem.difficulty.system.displayName): \(problem.difficulty.grade)" isSelected: selectedProblem?.id == problem.id
) ) {
.font(.subheadline) selectedProblem = problem
.foregroundColor(.blue)
} }
Spacer()
if selectedProblem?.id == problem.id {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
}
}
.contentShape(Rectangle())
.onTapGesture {
selectedProblem = problem
} }
} }
.padding(.vertical, 8)
} }
} }
@@ -724,6 +712,7 @@ struct EditAttemptView: View {
guard selectedProblem != nil else { return } guard selectedProblem != nil else { return }
let updatedAttempt = attempt.updated( let updatedAttempt = attempt.updated(
problemId: selectedProblem?.id,
result: selectedResult, result: selectedResult,
highestHold: highestHold.isEmpty ? nil : highestHold, highestHold: highestHold.isEmpty ? nil : highestHold,
notes: notes.isEmpty ? nil : notes, notes: notes.isEmpty ? nil : notes,

View File

@@ -128,7 +128,7 @@ struct LiveActivityDebugView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(8) .padding(8)
.id("bottom") .id("bottom")
.onChange(of: debugOutput) { _ in .onChange(of: debugOutput) {
withAnimation { withAnimation {
proxy.scrollTo("bottom", anchor: .bottom) proxy.scrollTo("bottom", anchor: .bottom)
} }

View File

@@ -15,7 +15,7 @@ struct ClimbingStatsProvider: TimelineProvider {
ClimbingStatsEntry( ClimbingStatsEntry(
date: Date(), date: Date(),
weeklyAttempts: 42, weeklyAttempts: 42,
todayAttempts: 8, weeklySessions: 5,
currentStreak: 3, currentStreak: 3,
favoriteGym: "Summit Climbing" favoriteGym: "Summit Climbing"
) )
@@ -25,7 +25,7 @@ struct ClimbingStatsProvider: TimelineProvider {
let entry = ClimbingStatsEntry( let entry = ClimbingStatsEntry(
date: Date(), date: Date(),
weeklyAttempts: 42, weeklyAttempts: 42,
todayAttempts: 8, weeklySessions: 5,
currentStreak: 3, currentStreak: 3,
favoriteGym: "Summit Climbing" favoriteGym: "Summit Climbing"
) )
@@ -41,7 +41,7 @@ struct ClimbingStatsProvider: TimelineProvider {
let entry = ClimbingStatsEntry( let entry = ClimbingStatsEntry(
date: currentDate, date: currentDate,
weeklyAttempts: stats.weeklyAttempts, weeklyAttempts: stats.weeklyAttempts,
todayAttempts: stats.todayAttempts, weeklySessions: stats.weeklySessions,
currentStreak: stats.currentStreak, currentStreak: stats.currentStreak,
favoriteGym: stats.favoriteGym favoriteGym: stats.favoriteGym
) )
@@ -60,7 +60,7 @@ struct ClimbingStatsProvider: TimelineProvider {
let attempts = try? JSONDecoder().decode([WidgetAttempt].self, from: attemptsData) let attempts = try? JSONDecoder().decode([WidgetAttempt].self, from: attemptsData)
else { else {
return ClimbingStats( return ClimbingStats(
weeklyAttempts: 0, todayAttempts: 0, currentStreak: 0, favoriteGym: "No Data") weeklyAttempts: 0, weeklySessions: 0, currentStreak: 0, favoriteGym: "No Data")
} }
// Load sessions for streak calculation // Load sessions for streak calculation
@@ -74,16 +74,16 @@ struct ClimbingStatsProvider: TimelineProvider {
let calendar = Calendar.current let calendar = Calendar.current
let now = Date() let now = Date()
let weekAgo = calendar.date(byAdding: .day, value: -7, to: now)! let weekAgo = calendar.date(byAdding: .day, value: -7, to: now)!
let startOfToday = calendar.startOfDay(for: now) _ = calendar.startOfDay(for: now)
// Calculate weekly attempts // Calculate weekly attempts
let weeklyAttempts = attempts.filter { attempt in let weeklyAttempts = attempts.filter { attempt in
attempt.timestamp >= weekAgo attempt.timestamp >= weekAgo
}.count }.count
// Calculate today's attempts // Calculate weekly sessions
let todayAttempts = attempts.filter { attempt in let weeklySessions = sessions.filter { session in
attempt.timestamp >= startOfToday session.date >= weekAgo && session.status == "COMPLETED"
}.count }.count
// Calculate current streak (consecutive days with sessions) // Calculate current streak (consecutive days with sessions)
@@ -94,7 +94,7 @@ struct ClimbingStatsProvider: TimelineProvider {
return ClimbingStats( return ClimbingStats(
weeklyAttempts: weeklyAttempts, weeklyAttempts: weeklyAttempts,
todayAttempts: todayAttempts, weeklySessions: weeklySessions,
currentStreak: currentStreak, currentStreak: currentStreak,
favoriteGym: favoriteGym favoriteGym: favoriteGym
) )
@@ -144,14 +144,14 @@ struct ClimbingStatsProvider: TimelineProvider {
struct ClimbingStatsEntry: TimelineEntry { struct ClimbingStatsEntry: TimelineEntry {
let date: Date let date: Date
let weeklyAttempts: Int let weeklyAttempts: Int
let todayAttempts: Int let weeklySessions: Int
let currentStreak: Int let currentStreak: Int
let favoriteGym: String let favoriteGym: String
} }
struct ClimbingStats { struct ClimbingStats {
let weeklyAttempts: Int let weeklyAttempts: Int
let todayAttempts: Int let weeklySessions: Int
let currentStreak: Int let currentStreak: Int
let favoriteGym: String let favoriteGym: String
} }
@@ -176,7 +176,7 @@ struct SmallWidgetView: View {
let entry: ClimbingStatsEntry let entry: ClimbingStatsEntry
var body: some View { var body: some View {
VStack(spacing: 8) { VStack(spacing: 12) {
// Header // Header
HStack { HStack {
if let uiImage = UIImage(named: "AppIcon") { if let uiImage = UIImage(named: "AppIcon") {
@@ -190,51 +190,37 @@ struct SmallWidgetView: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
Spacer() Spacer()
Text("This Week") Text("Weekly")
.font(.caption)
.foregroundColor(.secondary)
}
// Main stat - weekly attempts
VStack(spacing: 2) {
Text("\(entry.weeklyAttempts)")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.primary)
Text("Attempts")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Spacer() Spacer()
// Bottom stats // Main stats - weekly attempts and sessions
HStack { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 2) { HStack(spacing: 8) {
Text("\(entry.todayAttempts)") Image(systemName: "flame.fill")
.font(.headline) .foregroundColor(.orange)
.fontWeight(.semibold) .font(.title2)
Text("Today") Text("\(entry.weeklyAttempts)")
.font(.caption2) .font(.title)
.foregroundColor(.secondary) .fontWeight(.bold)
.foregroundColor(.primary)
} }
Spacer() HStack(spacing: 8) {
Image(systemName: "play.fill")
VStack(alignment: .trailing, spacing: 2) { .foregroundColor(.blue)
HStack(spacing: 2) { .font(.title2)
Text("\(entry.currentStreak)") Text("\(entry.weeklySessions)")
.font(.headline) .font(.title)
.fontWeight(.semibold) .fontWeight(.bold)
Image(systemName: "flame.fill") .foregroundColor(.primary)
.foregroundColor(.orange)
.font(.caption)
}
Text("Day Streak")
.font(.caption2)
.foregroundColor(.secondary)
} }
} }
Spacer()
} }
.padding() .padding()
} }
@@ -244,7 +230,7 @@ struct MediumWidgetView: View {
let entry: ClimbingStatsEntry let entry: ClimbingStatsEntry
var body: some View { var body: some View {
VStack(spacing: 12) { VStack(spacing: 16) {
// Header // Header
HStack { HStack {
HStack(spacing: 6) { HStack(spacing: 6) {
@@ -258,69 +244,41 @@ struct MediumWidgetView: View {
.font(.title2) .font(.title2)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
Text("Climbing Stats") Text("Weekly")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
} }
Spacer() Spacer()
Text("This Week")
.font(.caption)
.foregroundColor(.secondary)
} }
// Main stats row // Main stats row - weekly attempts and sessions
HStack(spacing: 20) { HStack(spacing: 40) {
VStack(spacing: 4) { VStack(spacing: 8) {
Text("\(entry.weeklyAttempts)") HStack(spacing: 8) {
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
Text("Total Attempts")
.font(.caption)
.foregroundColor(.secondary)
}
VStack(spacing: 4) {
Text("\(entry.todayAttempts)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
Text("Today")
.font(.caption)
.foregroundColor(.secondary)
}
VStack(spacing: 4) {
HStack(spacing: 4) {
Text("\(entry.currentStreak)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.orange)
Image(systemName: "flame.fill") Image(systemName: "flame.fill")
.foregroundColor(.orange) .foregroundColor(.orange)
.font(.title3) .font(.title2)
Text("\(entry.weeklyAttempts)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
}
}
VStack(spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "play.fill")
.foregroundColor(.blue)
.font(.title2)
Text("\(entry.weeklySessions)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
} }
Text("Day Streak")
.font(.caption)
.foregroundColor(.secondary)
} }
} }
Spacer() Spacer()
// Bottom info
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Favorite Gym")
.font(.caption2)
.foregroundColor(.secondary)
Text(entry.favoriteGym)
.font(.caption)
.fontWeight(.medium)
.lineLimit(1)
}
Spacer()
}
} }
.padding() .padding()
} }
@@ -367,14 +325,14 @@ struct WidgetGym: Codable {
ClimbingStatsEntry( ClimbingStatsEntry(
date: .now, date: .now,
weeklyAttempts: 42, weeklyAttempts: 42,
todayAttempts: 8, weeklySessions: 5,
currentStreak: 3, currentStreak: 3,
favoriteGym: "Summit Climbing" favoriteGym: "Summit Climbing"
) )
ClimbingStatsEntry( ClimbingStatsEntry(
date: .now, date: .now,
weeklyAttempts: 58, weeklyAttempts: 58,
todayAttempts: 12, weeklySessions: 8,
currentStreak: 5, currentStreak: 5,
favoriteGym: "Boulder Zone" favoriteGym: "Boulder Zone"
) )