✨ watchOS able to set status
This commit is contained in:
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Text("No bio available")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@@ -145,7 +145,6 @@ struct PostDetailView: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: .infinity)
|
||||
}
|
||||
.navigationTitle("Post")
|
||||
}
|
||||
|
||||
132
ios/WatchRunner Watch App/Views/StatusCreationView.swift
Normal file
132
ios/WatchRunner Watch App/Views/StatusCreationView.swift
Normal 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())
|
||||
}
|
||||
Reference in New Issue
Block a user