watchOS Account profile page

This commit is contained in:
2025-10-30 00:26:32 +08:00
parent 44c5d91620
commit e2369c40db
13 changed files with 572 additions and 4 deletions

View File

@@ -182,8 +182,6 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = "WatchRunner Watch App";
sourceTree = "<group>";
};
@@ -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";

View File

@@ -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"}}
{
"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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "icon.png",
"filename" : "Icon-App-1024x1024@1x.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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())
}