Private
Public Access
1
0

Settings, no password yet

This commit is contained in:
2025-08-25 00:13:55 -07:00
parent f0029d02e1
commit f0fd738935
5 changed files with 173 additions and 3 deletions

View File

@@ -37,6 +37,10 @@ struct KordophoneApp: App
} }
} }
} }
Settings {
PreferencesView()
}
} }
private func reportError(_ e: Error) { private func reportError(_ e: Error) {

View File

@@ -197,6 +197,28 @@ enum Serialized
let height: Int? let height: Int?
} }
} }
struct Settings: Decodable
{
let serverUrl: String
let username: String
}
}
extension Serialized.Settings: XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> Serialized.Settings? {
guard let d = XPCDictionary(value) else { return nil }
let su: String = d["server_url"] ?? ""
let un: String = d["username"] ?? ""
return Serialized.Settings(
serverUrl: su,
username: un
)
}
} }
extension Serialized.Attachment: XPCConvertible extension Serialized.Attachment: XPCConvertible

View File

@@ -0,0 +1,105 @@
//
// PreferencesView.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import SwiftUI
struct PreferencesView: View
{
@State var accountSettingsModel = AccountSettings.ViewModel()
var body: some View {
TabView {
AccountSettings(model: $accountSettingsModel)
.tabItem { Label("Account", systemImage: "person.crop.circle") }
}
.frame(width: 480.0, height: 300.0)
.padding(20.0)
}
}
struct AccountSettings: View
{
@Binding var model: ViewModel
var body: some View {
Form {
Section("Server Settings") {
TextField("Server", text: $model.serverURL)
}
Spacer()
.frame(height: 44.0)
Section("Authentication") {
TextField("Username", text: $model.username)
.textContentType(.username)
SecureField("Password", text: $model.password)
.textContentType(.password)
}
}
.task { await model.load() }
}
// MARK: - Types
@Observable
class ViewModel
{
var serverURL: String
var username: String
var password: String
private let xpc = XPCClient()
init(serverURL: String = "", username: String = "", password: String = "") {
self.serverURL = serverURL
self.username = username
self.password = password
autosave()
}
func load() async {
do {
let settings = try await xpc.getSettings()
self.serverURL = settings.serverUrl
self.username = settings.username
} catch {
print("Error getting settings: \(error)")
}
}
private func autosave() {
withObservationTracking {
_ = serverURL
_ = username
_ = password
} onChange: {
Task { @MainActor [weak self] in
guard let self else { return }
do {
let currentSettings = try await xpc.getSettings()
if currentSettings.serverUrl != serverURL || currentSettings.username != username {
try await xpc.setSettings(settings: Serialized.Settings(
serverUrl: serverURL,
username: username
))
}
} catch {
print("Error saving settings: \(error)")
}
autosave()
}
}
}
}
}

View File

@@ -42,7 +42,7 @@ struct TranscriptView: View
model.setNeedsReload(animated: true) model.setNeedsReload(animated: true)
} }
case .updateStreamReconnected: case .updateStreamReconnected:
model.setNeedsReload(animated: false) await model.triggerSync()
default: default:
break break
} }
@@ -79,8 +79,9 @@ struct TranscriptView: View
var displayItems: [DisplayItem] = [] var displayItems: [DisplayItem] = []
var displayedConversation: Display.Conversation.ID? = nil var displayedConversation: Display.Conversation.ID? = nil
internal var needsReload: NeedsReload = .yes(false) internal var needsReload: NeedsReload = .no
internal var messages: [Display.Message] internal var messages: [Display.Message]
internal let client = XPCClient()
init(messages: [Display.Message] = []) { init(messages: [Display.Message] = []) {
self.messages = messages self.messages = messages
@@ -89,6 +90,10 @@ struct TranscriptView: View
} }
func setNeedsReload(animated: Bool) { func setNeedsReload(animated: Bool) {
guard case .no = needsReload else {
return
}
needsReload = .yes(animated) needsReload = .yes(animated)
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
guard let self else { return } guard let self else { return }
@@ -108,12 +113,24 @@ struct TranscriptView: View
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
guard let self else { return } guard let self else { return }
await triggerSync()
setNeedsReload(animated: false) setNeedsReload(animated: false)
observeDisplayedConversation() observeDisplayedConversation()
} }
} }
} }
func triggerSync() async {
guard let displayedConversation else { return }
do {
try await client.syncConversation(conversationId: displayedConversation)
} catch {
print("Error triggering sync: \(error)")
}
}
private func reloadMessages() async { private func reloadMessages() async {
guard case .yes(let animated) = needsReload else { return } guard case .yes(let animated) = needsReload else { return }
needsReload = .no needsReload = .no
@@ -121,7 +138,6 @@ struct TranscriptView: View
guard let displayedConversation else { return } guard let displayedConversation else { return }
do { do {
let client = XPCClient()
let clientMessages = try await client.getMessages(conversationId: displayedConversation) let clientMessages = try await client.getMessages(conversationId: displayedConversation)
.map { Display.Message(from: $0) } .map { Display.Message(from: $0) }

View File

@@ -84,6 +84,11 @@ final class XPCClient
return results return results
} }
public func syncConversation(conversationId: String) async throws {
let req = makeRequest(method: "SyncConversation", arguments: ["conversation_id": xpcString(conversationId)])
_ = try await sendSync(req)
}
public func getMessages(conversationId: String, limit: Int = 100, offset: Int = 0) async throws -> [Serialized.Message] { public func getMessages(conversationId: String, limit: Int = 100, offset: Int = 0) async throws -> [Serialized.Message] {
var args: [String: xpc_object_t] = [:] var args: [String: xpc_object_t] = [:]
args["conversation_id"] = xpcString(conversationId) args["conversation_id"] = xpcString(conversationId)
@@ -137,6 +142,24 @@ final class XPCClient
return FileHandle(fileDescriptor: fd, closeOnDealloc: true) return FileHandle(fileDescriptor: fd, closeOnDealloc: true)
} }
public func getSettings() async throws -> Serialized.Settings {
let req = makeRequest(method: "GetAllSettings")
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
return Serialized.Settings.fromXPC(reply) ?? Serialized.Settings(serverUrl: "", username: "")
}
public func setSettings(settings: Serialized.Settings) async throws {
let req = makeRequest(
method: "UpdateSettings",
arguments: [
"server_url": xpcString(settings.serverUrl),
"username": xpcString(settings.username),
]
)
_ = try await sendSync(req)
}
// MARK: - Types // MARK: - Types