Settings, no password yet
This commit is contained in:
@@ -37,6 +37,10 @@ struct KordophoneApp: App
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Settings {
|
||||||
|
PreferencesView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reportError(_ e: Error) {
|
private func reportError(_ e: Error) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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)
|
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) }
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -138,6 +143,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
|
||||||
|
|
||||||
enum Error: Swift.Error
|
enum Error: Swift.Error
|
||||||
|
|||||||
Reference in New Issue
Block a user