watchOS able to set status

This commit is contained in:
2025-10-30 01:03:19 +08:00
parent a8055de910
commit dbcd1b6d36
5 changed files with 311 additions and 13 deletions

View File

@@ -230,3 +230,19 @@ struct SnUserProfile: Codable {
let experience: Int
let levelingProgress: Double
}
struct SnAccountStatus: Codable {
let id: String
let attitude: Int
let isOnline: Bool
let isInvisible: Bool
let isNotDisturb: Bool
let isCustomized: Bool
let label: String
let meta: [String: AnyCodable]?
let clearedAt: Date?
let accountId: String
let createdAt: Date
let updatedAt: Date
let deletedAt: Date?
}

View File

@@ -121,4 +121,94 @@ class NetworkService {
return try decoder.decode(SnAccount.self, from: data)
}
func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
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, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
return nil
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(SnAccountStatus.self, from: data)
}
func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus {
// First check if status exists
let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl)
let method = existingStatus == nil ? "POST" : "PATCH"
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
var body: [String: Any] = [
"attitude": attitude,
"is_invisible": isInvisible,
"is_not_disturb": isNotDisturb
]
if let label = label, !label.isEmpty {
body["label"] = label
}
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 {
let responseBody = String(data: data, encoding: .utf8) ?? ""
print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(SnAccountStatus.self, from: data)
}
func clearStatus(token: String, serverUrl: String) async throws {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
let (data, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
let responseBody = String(data: data, encoding: .utf8) ?? ""
print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
}
}
}

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct AccountView: View {
@EnvironmentObject var appState: AppState
@State private var user: SnAccount?
@State private var status: SnAccountStatus?
@State private var isLoading = false
@State private var error: Error?
@@ -102,20 +103,64 @@ struct AccountView: View {
}
}
// Bio
if let bio = user.profile.bio, !bio.isEmpty {
Text(bio)
.font(.body)
.multilineTextAlignment(.center)
// Status
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Status")
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
NavigationLink(
destination: StatusCreationView(initialStatus: status)
.environmentObject(appState)
) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.1))
.frame(width: 28, height: 28)
Image(systemName: "pencil")
.foregroundColor(.blue)
}
}
.buttonStyle(.plain)
.frame(width: 28, height: 28)
}
if let status = status {
VStack(alignment: .leading, spacing: 4) {
HStack {
Circle()
.fill(status.isOnline ? Color.green : Color.gray)
.frame(width: 8, height: 8)
Text(status.label.isEmpty ? "No status" : status.label)
.font(.body)
}
if status.isInvisible {
Text("Invisible")
.font(.caption)
.foregroundColor(.secondary)
}
if status.isNotDisturb {
Text("Do Not Disturb")
.font(.caption)
.foregroundColor(.secondary)
}
if let clearedAt = status.clearedAt {
Text("Clears: \(clearedAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption)
.foregroundColor(.secondary)
}
}
} else {
Text("No bio available")
Text("No status set")
.font(.body)
.foregroundColor(.secondary)
}
}
// Level and Progress
VStack(spacing: 8) {
VStack(alignment: .leading, spacing: 8) {
Text("Level \(user.profile.level)")
.font(.title3)
.bold()
@@ -127,10 +172,25 @@ struct AccountView: View {
.foregroundColor(.secondary)
}
// Bio
if let bio = user.profile.bio, !bio.isEmpty {
Text(bio)
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.frame(alignment: .leading)
} else {
Text("No bio available")
.font(.body)
.foregroundColor(.secondary)
.frame(alignment: .leading)
}
// Member since
Text("Member since: \(user.createdAt.formatted(.dateTime.month(.abbreviated).year()))")
Text("Joined at \(user.createdAt.formatted(.dateTime.month(.abbreviated).year()))")
.font(.caption)
.foregroundColor(.secondary)
.frame(alignment: .leading)
}
.padding()
// Load images when user data is available
@@ -168,6 +228,7 @@ struct AccountView: View {
do {
user = try await networkService.fetchUserProfile(token: token, serverUrl: serverUrl)
status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl)
} catch {
self.error = error
}

View File

@@ -145,7 +145,6 @@ struct PostDetailView: View {
}
}
.padding()
.frame(width: .infinity)
}
.navigationTitle("Post")
}

View File

@@ -0,0 +1,132 @@
//
// StatusCreationView.swift
// WatchRunner Watch App
//
// Created by LittleSheep on 2025/10/30.
//
import SwiftUI
struct StatusCreationView: View {
@EnvironmentObject var appState: AppState
@Environment(\.dismiss) var dismiss
let initialStatus: SnAccountStatus?
@State private var attitude: Int
@State private var isInvisible: Bool
@State private var isNotDisturb: Bool
@State private var label: String
@State private var isSubmitting: Bool = false
@State private var error: Error? = nil
private let networkService = NetworkService()
init(initialStatus: SnAccountStatus? = nil) {
self.initialStatus = initialStatus
_attitude = State(initialValue: initialStatus?.attitude ?? 1)
_isInvisible = State(initialValue: initialStatus?.isInvisible ?? false)
_isNotDisturb = State(initialValue: initialStatus?.isNotDisturb ?? false)
_label = State(initialValue: initialStatus?.label ?? "")
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Title
Text("Set Status")
.font(.headline)
.padding(.top)
// Label TextField
TextField("Status label", text: $label)
.textFieldStyle(.automatic)
.padding(.horizontal)
// Attitude Picker
VStack(alignment: .leading, spacing: 8) {
Text("Mood")
.font(.subheadline)
.foregroundColor(.secondary)
Picker("Attitude", selection: $attitude) {
Text("😊 Positive").tag(0)
Text("😐 Neutral").tag(1)
Text("😢 Negative").tag(2)
}
.pickerStyle(.wheel)
.frame(height: 80)
}
.padding(.horizontal)
// Toggles
VStack(spacing: 12) {
Toggle("Invisible", isOn: $isInvisible)
.padding(.horizontal)
Toggle("Do Not Disturb", isOn: $isNotDisturb)
.padding(.horizontal)
}
// Error message
if let error = error {
Text("Error: \(error.localizedDescription)")
.foregroundColor(.red)
.font(.caption)
.padding(.horizontal)
}
// Buttons
HStack(spacing: 12) {
Button("Cancel") {
dismiss()
}
.buttonStyle(.glass)
Button(isSubmitting ? "Saving..." : "Save") {
Task {
await submitStatus()
}
}
.buttonStyle(.glassProminent)
.disabled(isSubmitting)
}
.padding(.horizontal)
.padding(.bottom)
}
}
.navigationTitle("Status")
.navigationBarTitleDisplayMode(.inline)
}
private func submitStatus() async {
guard let token = appState.token, let serverUrl = appState.serverUrl else {
error = NSError(domain: "StatusCreationView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"])
return
}
isSubmitting = true
error = nil
do {
_ = try await networkService.createOrUpdateStatus(
attitude: attitude,
isInvisible: isInvisible,
isNotDisturb: isNotDisturb,
label: label.isEmpty ? nil : label,
token: token,
serverUrl: serverUrl
)
dismiss()
} catch {
self.error = error
}
isSubmitting = false
}
}
#Preview {
StatusCreationView()
.environmentObject(AppState())
}