1.0.2 - Widget and Photos fixes
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
D24C19682E75002A0045894C /* OpenClimb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenClimb.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionStatusLiveExtension.entitlements; sourceTree = "<group>"; };
|
||||
D2FE94802E78E958008CDB25 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
|
||||
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
@@ -107,6 +108,7 @@
|
||||
D24C195F2E75002A0045894C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
||||
D24C196A2E75002A0045894C /* OpenClimb */,
|
||||
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
||||
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
||||
@@ -389,8 +391,10 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -410,9 +414,10 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
@@ -429,8 +434,10 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -450,9 +457,10 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
@@ -469,8 +477,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||
@@ -481,7 +490,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -498,8 +507,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||
@@ -510,7 +520,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
||||
Binary file not shown.
@@ -6,5 +6,7 @@
|
||||
<true/>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs access to your photo library to add photos to climbing problems.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
10
ios/OpenClimb/OpenClimb.entitlements
Normal file
10
ios/OpenClimb/OpenClimb.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.atridad.OpenClimb</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -32,6 +32,27 @@ class ClimbingDataManager: ObservableObject {
|
||||
static let activeSession = "openclimb_active_session"
|
||||
}
|
||||
|
||||
// Widget data models
|
||||
private struct WidgetAttempt: Codable {
|
||||
let id: String
|
||||
let sessionId: String
|
||||
let problemId: String
|
||||
let timestamp: Date
|
||||
let result: String
|
||||
}
|
||||
|
||||
private struct WidgetSession: Codable {
|
||||
let id: String
|
||||
let gymId: String
|
||||
let date: Date
|
||||
let status: String
|
||||
}
|
||||
|
||||
private struct WidgetGym: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
}
|
||||
|
||||
init() {
|
||||
_ = ImageManager.shared
|
||||
loadAllData()
|
||||
@@ -97,8 +118,13 @@ class ClimbingDataManager: ObservableObject {
|
||||
private func saveGyms() {
|
||||
if let data = try? encoder.encode(gyms) {
|
||||
userDefaults.set(data, forKey: Keys.gyms)
|
||||
// Share with widget
|
||||
sharedUserDefaults?.set(data, forKey: Keys.gyms)
|
||||
// Share with widget - convert to widget format
|
||||
let widgetGyms = gyms.map { gym in
|
||||
WidgetGym(id: gym.id.uuidString, name: gym.name)
|
||||
}
|
||||
if let widgetData = try? encoder.encode(widgetGyms) {
|
||||
sharedUserDefaults?.set(widgetData, forKey: Keys.gyms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,16 +139,37 @@ class ClimbingDataManager: ObservableObject {
|
||||
private func saveSessions() {
|
||||
if let data = try? encoder.encode(sessions) {
|
||||
userDefaults.set(data, forKey: Keys.sessions)
|
||||
// Share with widget
|
||||
sharedUserDefaults?.set(data, forKey: Keys.sessions)
|
||||
// Share with widget - convert to widget format
|
||||
let widgetSessions = sessions.map { session in
|
||||
WidgetSession(
|
||||
id: session.id.uuidString,
|
||||
gymId: session.gymId.uuidString,
|
||||
date: session.date,
|
||||
status: session.status.rawValue
|
||||
)
|
||||
}
|
||||
if let widgetData = try? encoder.encode(widgetSessions) {
|
||||
sharedUserDefaults?.set(widgetData, forKey: Keys.sessions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAttempts() {
|
||||
if let data = try? encoder.encode(attempts) {
|
||||
userDefaults.set(data, forKey: Keys.attempts)
|
||||
// Share with widget
|
||||
sharedUserDefaults?.set(data, forKey: Keys.attempts)
|
||||
// Share with widget - convert to widget format
|
||||
let widgetAttempts = attempts.map { attempt in
|
||||
WidgetAttempt(
|
||||
id: attempt.id.uuidString,
|
||||
sessionId: attempt.sessionId.uuidString,
|
||||
problemId: attempt.problemId.uuidString,
|
||||
timestamp: attempt.timestamp,
|
||||
result: attempt.result.rawValue
|
||||
)
|
||||
}
|
||||
if let widgetData = try? encoder.encode(widgetAttempts) {
|
||||
sharedUserDefaults?.set(widgetData, forKey: Keys.attempts)
|
||||
}
|
||||
// Update widget timeline
|
||||
updateWidgetTimeline()
|
||||
}
|
||||
@@ -1020,8 +1067,14 @@ extension ClimbingDataManager {
|
||||
private func updateLiveActivityForActiveSession() {
|
||||
guard let activeSession = activeSession,
|
||||
activeSession.status == .active,
|
||||
let _ = gym(withId: activeSession.gymId)
|
||||
let gym = gym(withId: activeSession.gymId)
|
||||
else {
|
||||
print("⚠️ Live Activity update skipped - no active session or gym")
|
||||
if let session = activeSession {
|
||||
print(" Session ID: \(session.id)")
|
||||
print(" Session Status: \(session.status)")
|
||||
print(" Gym ID: \(session.gymId)")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1040,6 +1093,16 @@ extension ClimbingDataManager {
|
||||
elapsedInterval = 0
|
||||
}
|
||||
|
||||
print("🔄 Live Activity Update Debug:")
|
||||
print(" Session ID: \(activeSession.id)")
|
||||
print(" Gym: \(gym.name)")
|
||||
print(" Total attempts in session: \(totalAttempts)")
|
||||
print(" Completed problems: \(completedProblems)")
|
||||
print(" Elapsed time: \(elapsedInterval) seconds")
|
||||
print(
|
||||
" All attempts for session: \(attemptsForSession.map { "\($0.result) - Problem: \($0.problemId)" })"
|
||||
)
|
||||
|
||||
Task {
|
||||
await LiveActivityManager.shared.updateLiveActivity(
|
||||
elapsed: elapsedInterval,
|
||||
@@ -1061,6 +1124,14 @@ extension ClimbingDataManager {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Debug function to manually trigger widget data update
|
||||
func debugUpdateWidgetData() {
|
||||
// Force save all data to widget
|
||||
saveGyms()
|
||||
saveSessions()
|
||||
saveAttempts()
|
||||
}
|
||||
|
||||
private func validateImportData(_ importData: ClimbDataExport) throws {
|
||||
if importData.gyms.isEmpty {
|
||||
throw NSError(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
|
||||
struct AddAttemptView: View {
|
||||
@@ -19,6 +20,8 @@ struct AddAttemptView: View {
|
||||
@State private var newProblemGrade = ""
|
||||
@State private var selectedClimbType: ClimbType = .boulder
|
||||
@State private var selectedDifficultySystem: DifficultySystem = .vScale
|
||||
@State private var selectedPhotos: [PhotosPickerItem] = []
|
||||
@State private var imageData: [Data] = []
|
||||
|
||||
private var activeProblems: [Problem] {
|
||||
dataManager.activeProblems(forGym: gym.id)
|
||||
@@ -126,6 +129,8 @@ struct AddAttemptView: View {
|
||||
|
||||
Button("Back") {
|
||||
showingCreateProblem = false
|
||||
selectedPhotos = []
|
||||
imageData = []
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
@@ -209,6 +214,74 @@ struct AddAttemptView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Photos (Optional)") {
|
||||
PhotosPicker(
|
||||
selection: $selectedPhotos,
|
||||
maxSelectionCount: 5,
|
||||
matching: .images
|
||||
) {
|
||||
HStack {
|
||||
Image(systemName: "camera.fill")
|
||||
.foregroundColor(.blue)
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Add Photos")
|
||||
.font(.headline)
|
||||
.foregroundColor(.blue)
|
||||
Text("\(imageData.count) of 5 photos added")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.onChange(of: selectedPhotos) { _, _ in
|
||||
Task {
|
||||
await loadSelectedPhotos()
|
||||
}
|
||||
}
|
||||
|
||||
if !imageData.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(imageData.indices, id: \.self) { index in
|
||||
if let uiImage = UIImage(data: imageData[index]) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 80, height: 80)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button(action: {
|
||||
imageData.remove(at: index)
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
.background(Circle().fill(.white))
|
||||
}
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.gray.opacity(0.3))
|
||||
.frame(width: 80, height: 80)
|
||||
.overlay {
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -310,11 +383,20 @@ struct AddAttemptView: View {
|
||||
let difficulty = DifficultyGrade(
|
||||
system: selectedDifficultySystem, grade: newProblemGrade)
|
||||
|
||||
// Save images and get paths
|
||||
var imagePaths: [String] = []
|
||||
for data in imageData {
|
||||
if let relativePath = ImageManager.shared.saveImageData(data) {
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
let newProblem = Problem(
|
||||
gymId: gym.id,
|
||||
name: newProblemName.isEmpty ? nil : newProblemName,
|
||||
climbType: selectedClimbType,
|
||||
difficulty: difficulty
|
||||
difficulty: difficulty,
|
||||
imagePaths: imagePaths
|
||||
)
|
||||
|
||||
dataManager.addProblem(newProblem)
|
||||
@@ -347,8 +429,26 @@ struct AddAttemptView: View {
|
||||
dataManager.addAttempt(attempt)
|
||||
}
|
||||
|
||||
// Clear photo states after saving
|
||||
selectedPhotos = []
|
||||
imageData = []
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func loadSelectedPhotos() async {
|
||||
var newImageData: [Data] = []
|
||||
|
||||
for item in selectedPhotos {
|
||||
if let data = try? await item.loadTransferable(type: Data.self) {
|
||||
newImageData.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
imageData = newImageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProblemSelectionRow: View {
|
||||
@@ -599,6 +699,8 @@ struct EditAttemptView: View {
|
||||
@State private var newProblemGrade = ""
|
||||
@State private var selectedClimbType: ClimbType = .boulder
|
||||
@State private var selectedDifficultySystem: DifficultySystem = .vScale
|
||||
@State private var selectedPhotos: [PhotosPickerItem] = []
|
||||
@State private var imageData: [Data] = []
|
||||
|
||||
private var availableProblems: [Problem] {
|
||||
guard let session = dataManager.session(withId: attempt.sessionId) else {
|
||||
@@ -727,6 +829,8 @@ struct EditAttemptView: View {
|
||||
|
||||
Button("Back") {
|
||||
showingCreateProblem = false
|
||||
selectedPhotos = []
|
||||
imageData = []
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
@@ -810,6 +914,74 @@ struct EditAttemptView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Photos (Optional)") {
|
||||
PhotosPicker(
|
||||
selection: $selectedPhotos,
|
||||
maxSelectionCount: 5,
|
||||
matching: .images
|
||||
) {
|
||||
HStack {
|
||||
Image(systemName: "camera.fill")
|
||||
.foregroundColor(.blue)
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Add Photos")
|
||||
.font(.headline)
|
||||
.foregroundColor(.blue)
|
||||
Text("\(imageData.count) of 5 photos added")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.onChange(of: selectedPhotos) { _, _ in
|
||||
Task {
|
||||
await loadSelectedPhotos()
|
||||
}
|
||||
}
|
||||
|
||||
if !imageData.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(imageData.indices, id: \.self) { index in
|
||||
if let uiImage = UIImage(data: imageData[index]) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 80, height: 80)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button(action: {
|
||||
imageData.remove(at: index)
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
.background(Circle().fill(.white))
|
||||
}
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.gray.opacity(0.3))
|
||||
.frame(width: 80, height: 80)
|
||||
.overlay {
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -915,11 +1087,20 @@ struct EditAttemptView: View {
|
||||
let difficulty = DifficultyGrade(
|
||||
system: selectedDifficultySystem, grade: newProblemGrade)
|
||||
|
||||
// Save images and get paths
|
||||
var imagePaths: [String] = []
|
||||
for data in imageData {
|
||||
if let relativePath = ImageManager.shared.saveImageData(data) {
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
let newProblem = Problem(
|
||||
gymId: gym.id,
|
||||
name: newProblemName.isEmpty ? nil : newProblemName,
|
||||
climbType: selectedClimbType,
|
||||
difficulty: difficulty
|
||||
difficulty: difficulty,
|
||||
imagePaths: imagePaths
|
||||
)
|
||||
|
||||
dataManager.addProblem(newProblem)
|
||||
@@ -949,8 +1130,26 @@ struct EditAttemptView: View {
|
||||
dataManager.updateAttempt(updatedAttempt)
|
||||
}
|
||||
|
||||
// Clear photo states after saving
|
||||
selectedPhotos = []
|
||||
imageData = []
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func loadSelectedPhotos() async {
|
||||
var newImageData: [Data] = []
|
||||
|
||||
for item in selectedPhotos {
|
||||
if let data = try? await item.loadTransferable(type: Data.self) {
|
||||
newImageData.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
imageData = newImageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@@ -997,6 +1196,7 @@ struct ProblemSelectionImageView: View {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
|
||||
@@ -61,11 +60,11 @@ struct AddEditProblemView: View {
|
||||
Form {
|
||||
GymSelectionSection()
|
||||
BasicInfoSection()
|
||||
PhotosSection()
|
||||
ClimbTypeSection()
|
||||
DifficultySection()
|
||||
LocationAndSetterSection()
|
||||
TagsSection()
|
||||
PhotosSection()
|
||||
AdditionalInfoSection()
|
||||
}
|
||||
.navigationTitle(isEditing ? "Edit Problem" : "Add Problem")
|
||||
@@ -304,18 +303,30 @@ struct AddEditProblemView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func PhotosSection() -> some View {
|
||||
Section("Photos") {
|
||||
Section("Photos (Optional)") {
|
||||
PhotosPicker(
|
||||
selection: $selectedPhotos,
|
||||
maxSelectionCount: 5,
|
||||
matching: .images
|
||||
) {
|
||||
HStack {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
Image(systemName: "camera.fill")
|
||||
.foregroundColor(.blue)
|
||||
Text("Add Photos (\(imageData.count)/5)")
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Add Photos")
|
||||
.font(.headline)
|
||||
.foregroundColor(.blue)
|
||||
Text("\(imageData.count) of 5 photos added")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if !imageData.isEmpty {
|
||||
|
||||
10
ios/SessionStatusLiveExtension.entitlements
Normal file
10
ios/SessionStatusLiveExtension.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.atridad.OpenClimb</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user