iOS Release - 1.0.1
This commit is contained in:
@@ -390,7 +390,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -410,7 +410,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -450,7 +450,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict/>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BuildLocationStyle</key>
|
||||||
|
<string>UseAppPreferences</string>
|
||||||
|
<key>CompilationCachingSetting</key>
|
||||||
|
<string>Default</string>
|
||||||
|
<key>CustomBuildLocationType</key>
|
||||||
|
<string>RelativeToDerivedData</string>
|
||||||
|
<key>DerivedDataLocationStyle</key>
|
||||||
|
<string>Default</string>
|
||||||
|
<key>ShowSharedSchemesAutomaticallyEnabled</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -87,16 +87,28 @@ final class LiveActivityManager {
|
|||||||
|
|
||||||
/// 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 {
|
// First end the tracked activity if it exists
|
||||||
print("ℹ️ No current activity to end")
|
if let currentActivity {
|
||||||
return
|
print("🔴 Ending tracked Live Activity: \(currentActivity.id)")
|
||||||
|
await currentActivity.end(nil, dismissalPolicy: .immediate)
|
||||||
|
self.currentActivity = nil
|
||||||
|
print("✅ Tracked Live Activity ended successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
print("🔴 Ending Live Activity: \(currentActivity.id)")
|
// Force end ALL active activities of our type to ensure cleanup
|
||||||
|
print("🔍 Checking for any remaining active activities...")
|
||||||
|
let activities = Activity<SessionActivityAttributes>.activities
|
||||||
|
|
||||||
await currentActivity.end(nil, dismissalPolicy: .immediate)
|
if activities.isEmpty {
|
||||||
self.currentActivity = nil
|
print("ℹ️ No additional activities found")
|
||||||
print("✅ Live Activity ended successfully")
|
} else {
|
||||||
|
print("🔴 Found \(activities.count) additional active activities, ending them...")
|
||||||
|
for activity in activities {
|
||||||
|
print("🔴 Force ending activity: \(activity.id)")
|
||||||
|
await activity.end(nil, dismissalPolicy: .immediate)
|
||||||
|
}
|
||||||
|
print("✅ All Live Activities ended successfully")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if Live Activities are available and authorized
|
/// Check if Live Activities are available and authorized
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -8,19 +7,7 @@ struct SessionsView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(spacing: 0) {
|
Group {
|
||||||
// Active session banner
|
|
||||||
if let activeSession = dataManager.activeSession,
|
|
||||||
let gym = dataManager.gym(withId: activeSession.gymId)
|
|
||||||
{
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
ActiveSessionBanner(session: activeSession, gym: gym)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sessions list
|
|
||||||
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
|
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
|
||||||
EmptySessionsView()
|
EmptySessionsView()
|
||||||
} else {
|
} else {
|
||||||
@@ -28,6 +15,7 @@ struct SessionsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Sessions")
|
.navigationTitle("Sessions")
|
||||||
|
.navigationBarTitleDisplayMode(.automatic)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
if dataManager.gyms.isEmpty {
|
if dataManager.gyms.isEmpty {
|
||||||
@@ -47,75 +35,7 @@ struct SessionsView: View {
|
|||||||
AddEditSessionView()
|
AddEditSessionView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.navigationViewStyle(.stack)
|
||||||
}
|
|
||||||
|
|
||||||
struct ActiveSessionBanner: View {
|
|
||||||
let session: ClimbSession
|
|
||||||
let gym: Gym
|
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
||||||
@State private var currentTime = Date()
|
|
||||||
|
|
||||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "play.fill")
|
|
||||||
.foregroundColor(.green)
|
|
||||||
.font(.caption)
|
|
||||||
Text("Active Session")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(gym.name)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
if let startTime = session.startTime {
|
|
||||||
Text(formatDuration(from: startTime, to: currentTime))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button("End") {
|
|
||||||
dataManager.endSession(session.id)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(.green.opacity(0.1))
|
|
||||||
.stroke(.green.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.onReceive(timer) { _ in
|
|
||||||
currentTime = Date()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func formatDuration(from start: Date, to end: Date) -> String {
|
|
||||||
let interval = end.timeIntervalSince(start)
|
|
||||||
let hours = Int(interval) / 3600
|
|
||||||
let minutes = Int(interval) % 3600 / 60
|
|
||||||
let seconds = Int(interval) % 60
|
|
||||||
|
|
||||||
if hours > 0 {
|
|
||||||
return String(format: "%dh %dm %ds", hours, minutes, seconds)
|
|
||||||
} else if minutes > 0 {
|
|
||||||
return String(format: "%dm %ds", minutes, seconds)
|
|
||||||
} else {
|
|
||||||
return String(format: "%ds", seconds)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,18 +50,45 @@ struct SessionsList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(completedSessions) { session in
|
List {
|
||||||
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
// Active session banner section
|
||||||
SessionRow(session: session)
|
if let activeSession = dataManager.activeSession,
|
||||||
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
||||||
|
{
|
||||||
|
Section {
|
||||||
|
ActiveSessionBanner(session: activeSession, gym: gym)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button(role: .destructive) {
|
// Completed sessions section
|
||||||
sessionToDelete = session
|
if !completedSessions.isEmpty {
|
||||||
} label: {
|
Section {
|
||||||
Label("Delete", systemImage: "trash")
|
ForEach(completedSessions) { session in
|
||||||
|
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
||||||
|
SessionRow(session: session)
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
sessionToDelete = session
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
if dataManager.activeSession != nil {
|
||||||
|
Text("Previous Sessions")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
.alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) {
|
.alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) {
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
sessionToDelete = nil
|
sessionToDelete = nil
|
||||||
@@ -160,6 +107,91 @@ struct SessionsList: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ActiveSessionBanner: View {
|
||||||
|
let session: ClimbSession
|
||||||
|
let gym: Gym
|
||||||
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
|
@State private var currentTime = Date()
|
||||||
|
@State private var navigateToDetail = false
|
||||||
|
|
||||||
|
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "play.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.font(.caption)
|
||||||
|
Text("Active Session")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(gym.name)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if let startTime = session.startTime {
|
||||||
|
Text(formatDuration(from: startTime, to: currentTime))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
navigateToDetail = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
dataManager.endSession(session.id)
|
||||||
|
}) {
|
||||||
|
Image(systemName: "stop.fill")
|
||||||
|
.font(.system(size: 16, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.background(Color.red)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(.green.opacity(0.1))
|
||||||
|
.stroke(.green.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onReceive(timer) { _ in
|
||||||
|
currentTime = Date()
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
NavigationLink(
|
||||||
|
destination: SessionDetailView(sessionId: session.id),
|
||||||
|
isActive: $navigateToDetail
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.hidden()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDuration(from start: Date, to end: Date) -> String {
|
||||||
|
let interval = end.timeIntervalSince(start)
|
||||||
|
let hours = Int(interval) / 3600
|
||||||
|
let minutes = Int(interval) % 3600 / 60
|
||||||
|
let seconds = Int(interval) % 60
|
||||||
|
|
||||||
|
if hours > 0 {
|
||||||
|
return String(format: "%dh %dm %ds", hours, minutes, seconds)
|
||||||
|
} else if minutes > 0 {
|
||||||
|
return String(format: "%dm %ds", minutes, seconds)
|
||||||
|
} else {
|
||||||
|
return String(format: "%ds", seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SessionRow: View {
|
struct SessionRow: View {
|
||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
|
|||||||
Reference in New Issue
Block a user