Settings, no password yet
This commit is contained in:
@@ -37,6 +37,10 @@ struct KordophoneApp: App
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Settings {
|
||||
PreferencesView()
|
||||
}
|
||||
}
|
||||
|
||||
private func reportError(_ e: Error) {
|
||||
|
||||
@@ -197,6 +197,28 @@ enum Serialized
|
||||
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
|
||||
|
||||
105
kordophone2/PreferencesView.swift
Normal file
105
kordophone2/PreferencesView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ struct TranscriptView: View
|
||||
model.setNeedsReload(animated: true)
|
||||
}
|
||||
case .updateStreamReconnected:
|
||||
model.setNeedsReload(animated: false)
|
||||
await model.triggerSync()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -79,8 +79,9 @@ struct TranscriptView: View
|
||||
var displayItems: [DisplayItem] = []
|
||||
var displayedConversation: Display.Conversation.ID? = nil
|
||||
|
||||
internal var needsReload: NeedsReload = .yes(false)
|
||||
internal var needsReload: NeedsReload = .no
|
||||
internal var messages: [Display.Message]
|
||||
internal let client = XPCClient()
|
||||
|
||||
init(messages: [Display.Message] = []) {
|
||||
self.messages = messages
|
||||
@@ -89,6 +90,10 @@ struct TranscriptView: View
|
||||
}
|
||||
|
||||
func setNeedsReload(animated: Bool) {
|
||||
guard case .no = needsReload else {
|
||||
return
|
||||
}
|
||||
|
||||
needsReload = .yes(animated)
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -108,12 +113,24 @@ struct TranscriptView: View
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
await triggerSync()
|
||||
|
||||
setNeedsReload(animated: false)
|
||||
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 {
|
||||
guard case .yes(let animated) = needsReload else { return }
|
||||
needsReload = .no
|
||||
@@ -121,7 +138,6 @@ struct TranscriptView: View
|
||||
guard let displayedConversation else { return }
|
||||
|
||||
do {
|
||||
let client = XPCClient()
|
||||
let clientMessages = try await client.getMessages(conversationId: displayedConversation)
|
||||
.map { Display.Message(from: $0) }
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@ final class XPCClient
|
||||
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] {
|
||||
var args: [String: xpc_object_t] = [:]
|
||||
args["conversation_id"] = xpcString(conversationId)
|
||||
@@ -138,6 +143,24 @@ final class XPCClient
|
||||
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
|
||||
|
||||
enum Error: Swift.Error
|
||||
|
||||
Reference in New Issue
Block a user