diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3cf9600a..44828bb4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -182,8 +182,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = "WatchRunner Watch App"; sourceTree = ""; }; @@ -671,10 +669,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -732,10 +734,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -764,10 +770,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks.sh\"\n"; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index cb3ed702..85c1dee9 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1 +1,334 @@ -{"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-38x38@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "filename" : "Icon-App-38x38@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-64x64@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "filename" : "Icon-App-64x64@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "filename" : "Icon-App-68x68@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "Icon-App-1024x1024@1x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-20x20@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "20x20" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-20x20@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "20x20" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-29x29@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "29x29" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-29x29@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "29x29" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-38x38@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-38x38@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-40x40@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "40x40" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-40x40@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "40x40" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-60x60@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "60x60" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-60x60@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "60x60" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-64x64@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-64x64@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-68x68@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-76x76@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-83.5x83.5@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-App-Dark-1024x1024@1x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "Icon-App-1024x1024@1x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b009..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32ae..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json index 110a3e31..44472cbc 100644 --- a/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon.png", + "filename" : "Icon-App-1024x1024@1x.png", "idiom" : "universal", "platform" : "watchos", "size" : "1024x1024" diff --git a/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..964fbf00 Binary files /dev/null and b/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/icon.png b/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/icon.png deleted file mode 100644 index 0eeb8c11..00000000 Binary files a/ios/WatchRunner Watch App/Assets.xcassets/AppIcon.appiconset/icon.png and /dev/null differ diff --git a/ios/WatchRunner Watch App/ContentView.swift b/ios/WatchRunner Watch App/ContentView.swift index 00c99583..057a9e2c 100644 --- a/ios/WatchRunner Watch App/ContentView.swift +++ b/ios/WatchRunner Watch App/ContentView.swift @@ -15,6 +15,7 @@ struct ContentView: View { enum Panel: Hashable { case explore case notifications + case account } var body: some View { @@ -22,6 +23,7 @@ struct ContentView: View { List(selection: $selection) { Label("Explore", systemImage: "globe").tag(Panel.explore) Label("Notifications", systemImage: "bell").tag(Panel.notifications) + Label("Account", systemImage: "person.circle").tag(Panel.account) } .listStyle(.automatic) } detail: { @@ -32,6 +34,9 @@ struct ContentView: View { case .notifications: NotificationView() .environmentObject(appState) + case .account: + AccountView() + .environmentObject(appState) case .none: Text("Select a panel") } diff --git a/ios/WatchRunner Watch App/Models/Models.swift b/ios/WatchRunner Watch App/Models/Models.swift index 5d89c5b3..2ca62509 100644 --- a/ios/WatchRunner Watch App/Models/Models.swift +++ b/ios/WatchRunner Watch App/Models/Models.swift @@ -213,3 +213,20 @@ struct ActivityResponse { let hasMore: Bool let nextCursor: String? } + +struct SnAccount: Codable { + let id: String + let name: String + let nick: String + let profile: SnUserProfile + let createdAt: Date +} + +struct SnUserProfile: Codable { + let bio: String? + let picture: SnCloudFile? + let background: SnCloudFile? + let level: Int + let experience: Int + let levelingProgress: Double +} diff --git a/ios/WatchRunner Watch App/Services/NetworkService.swift b/ios/WatchRunner Watch App/Services/NetworkService.swift index 5c6a171a..7a87ea01 100644 --- a/ios/WatchRunner Watch App/Services/NetworkService.swift +++ b/ios/WatchRunner Watch App/Services/NetworkService.swift @@ -100,4 +100,25 @@ class NetworkService { return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore) } + + func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount { + guard let baseURL = URL(string: serverUrl) else { + throw URLError(.badURL) + } + let url = baseURL.appendingPathComponent("/pass/accounts/me") + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") + request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") + + let (data, _) = try await session.data(for: request) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + decoder.keyDecodingStrategy = .convertFromSnakeCase + + return try decoder.decode(SnAccount.self, from: data) + } } diff --git a/ios/WatchRunner Watch App/Views/AccountView.swift b/ios/WatchRunner Watch App/Views/AccountView.swift new file mode 100644 index 00000000..f078b688 --- /dev/null +++ b/ios/WatchRunner Watch App/Views/AccountView.swift @@ -0,0 +1,182 @@ +// +// AccountView.swift +// WatchRunner Watch App +// +// Created by LittleSheep on 2025/10/30. +// + +import SwiftUI + +struct AccountView: View { + @EnvironmentObject var appState: AppState + @State private var user: SnAccount? + @State private var isLoading = false + @State private var error: Error? + + @StateObject private var profileImageLoader = ImageLoader() + @StateObject private var bannerImageLoader = ImageLoader() + + private let networkService = NetworkService() + + var body: some View { + ScrollView { + if isLoading { + ProgressView() + .padding() + } else if let error = error { + VStack { + Text("Failed to load account") + .foregroundColor(.red) + Text(error.localizedDescription) + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + } else if let user = user { + VStack(spacing: 16) { + // Banner + if user.profile.background != nil { + if bannerImageLoader.isLoading { + ProgressView() + .frame(height: 80) + } else if let bannerImage = bannerImageLoader.image { + bannerImage + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 80) + .clipped() + .cornerRadius(8) + } else if bannerImageLoader.errorMessage != nil { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(height: 80) + .cornerRadius(8) + } else { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(height: 80) + .cornerRadius(8) + } + } + + // Profile Picture + HStack(spacing: 16) + { + if profileImageLoader.isLoading { + ProgressView() + .frame(width: 60, height: 60) + } else if let profileImage = profileImageLoader.image { + profileImage + .resizable() + .frame(width: 60, height: 60) + .clipShape(Circle()) + } else if profileImageLoader.errorMessage != nil { + Circle() + .fill(Color.red.opacity(0.3)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "exclamationmark.triangle") + .resizable() + .scaledToFit() + .foregroundColor(.red) + ) + } else { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .foregroundColor(.gray) + ) + } + + // Username and Handle + VStack(alignment: .leading, content: <#T##() -> View#>, spacing: 4) { + Text(user.nick) + .font(.headline) + Text("@\(user.name)") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + // Bio + if let bio = user.profile.bio, !bio.isEmpty { + Text(bio) + .font(.body) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + } else { + Text("No bio available") + .font(.body) + .foregroundColor(.secondary) + } + + // Level and Progress + VStack(spacing: 8) { + Text("Level \(user.profile.level)") + .font(.title3) + .bold() + ProgressView(value: user.profile.levelingProgress) + .progressViewStyle(LinearProgressViewStyle()) + .frame(height: 8) + Text("Experience: \(user.profile.experience)") + .font(.caption) + .foregroundColor(.secondary) + } + + // Member since + Text("Member since: \(user.createdAt.formatted(.dateTime.month(.abbreviated).year()))") + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + // Load images when user data is available + .task(id: user.profile.picture?.id) { + if let serverUrl = appState.serverUrl, let pictureId = user.profile.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { + await profileImageLoader.loadImage(from: imageUrl, token: token) + } + } + .task(id: user.profile.background?.id) { + if let serverUrl = appState.serverUrl, let backgroundId = user.profile.background?.id, let imageUrl = getAttachmentUrl(for: backgroundId, serverUrl: serverUrl), let token = appState.token { + await bannerImageLoader.loadImage(from: imageUrl, token: token) + } + } + } else { + Text("No account data") + .padding() + } + } + .navigationTitle("Account") + .onAppear { + Task.detached { + await loadUserProfile() + } + } + } + + private func loadUserProfile() async { + guard let token = appState.token, let serverUrl = appState.serverUrl else { + error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) + return + } + + isLoading = true + error = nil + + do { + user = try await networkService.fetchUserProfile(token: token, serverUrl: serverUrl) + } catch { + self.error = error + } + + isLoading = false + } +} + +#Preview { + AccountView() + .environmentObject(AppState()) +}