Fixed Widget stats
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Binary file not shown.
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AddAttemptView: View {
|
struct AddAttemptView: View {
|
||||||
@@ -610,37 +609,26 @@ 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 {
|
||||||
|
LazyVGrid(
|
||||||
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
|
spacing: 8
|
||||||
|
) {
|
||||||
ForEach(availableProblems, id: \.id) { problem in
|
ForEach(availableProblems, id: \.id) { problem in
|
||||||
HStack {
|
ProblemSelectionCard(
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
problem: problem,
|
||||||
Text(problem.name ?? "Unnamed Problem")
|
isSelected: selectedProblem?.id == problem.id
|
||||||
.font(.headline)
|
) {
|
||||||
|
|
||||||
Text(
|
|
||||||
"\(problem.difficulty.system.displayName): \(problem.difficulty.grade)"
|
|
||||||
)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if selectedProblem?.id == problem.id {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture {
|
|
||||||
selectedProblem = problem
|
selectedProblem = problem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Result") {
|
Section("Result") {
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
Text("Today")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
Text("\(entry.currentStreak)")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
Image(systemName: "flame.fill")
|
Image(systemName: "flame.fill")
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
.font(.caption)
|
.font(.title2)
|
||||||
|
Text("\(entry.weeklyAttempts)")
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
Text("Day Streak")
|
|
||||||
.font(.caption2)
|
HStack(spacing: 8) {
|
||||||
.foregroundColor(.secondary)
|
Image(systemName: "play.fill")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.font(.title2)
|
||||||
|
Text("\(entry.weeklySessions)")
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
.font(.title2)
|
||||||
Text("\(entry.weeklyAttempts)")
|
Text("\(entry.weeklyAttempts)")
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Text("Total Attempts")
|
}
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 8) {
|
||||||
Text("\(entry.todayAttempts)")
|
HStack(spacing: 8) {
|
||||||
.font(.title)
|
Image(systemName: "play.fill")
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
Text("Today")
|
.font(.title2)
|
||||||
.font(.caption)
|
Text("\(entry.weeklySessions)")
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text("\(entry.currentStreak)")
|
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.primary)
|
||||||
Image(systemName: "flame.fill")
|
|
||||||
.foregroundColor(.orange)
|
|
||||||
.font(.title3)
|
|
||||||
}
|
}
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user