add mail, account setup viewmodels with grdb observation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 21:23:00 +01:00
parent 1b915fc9ab
commit 6f0ba20d86
2 changed files with 168 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import SwiftUI
import Models
@Observable
@MainActor
final class AccountSetupViewModel {
var email: String = ""
var password: String = ""
var imapHost: String = ""
var imapPort: String = "993"
var accountName: String = ""
var isAutoDiscovering = false
var autoDiscoveryFailed = false
var isManualMode = false
var errorMessage: String?
var canSubmit: Bool {
!email.isEmpty && !password.isEmpty && !imapHost.isEmpty && !imapPort.isEmpty
}
func autoDiscover() async {
isAutoDiscovering = true
autoDiscoveryFailed = false
// Auto-discovery implementation in Task 16
isAutoDiscovering = false
isManualMode = true
}
func buildConfig() -> (AccountConfig, Credentials)? {
guard let port = Int(imapPort), canSubmit else { return nil }
let id = email.replacingOccurrences(of: "@", with: "-at-")
.replacingOccurrences(of: ".", with: "-")
let config = AccountConfig(
id: id,
name: accountName.isEmpty ? email : accountName,
email: email,
imapHost: imapHost,
imapPort: port
)
let credentials = Credentials(username: email, password: password)
return (config, credentials)
}
}

View File

@@ -0,0 +1,124 @@
import SwiftUI
import GRDB
import Models
import MailStore
import SyncEngine
import IMAPClient
@Observable
@MainActor
final class MailViewModel {
private var store: MailStore?
private var coordinator: SyncCoordinator?
var threads: [ThreadSummary] = []
var selectedThread: ThreadSummary?
var messages: [MessageSummary] = []
var mailboxes: [MailboxInfo] = []
var selectedMailbox: MailboxInfo?
var syncState: SyncState = .idle
var errorMessage: String?
private var threadObservation: Task<Void, Never>?
private var messageObservation: Task<Void, Never>?
var hasAccount: Bool {
store != nil && coordinator != nil
}
func setup(config: AccountConfig, credentials: Credentials) throws {
let dbPath = Self.databasePath(for: config.id)
let dbPool = try DatabaseSetup.openDatabase(atPath: dbPath)
let mailStore = MailStore(dbWriter: dbPool)
let imapClient = IMAPClient(
host: config.imapHost,
port: config.imapPort,
credentials: credentials
)
store = mailStore
coordinator = SyncCoordinator(
accountConfig: config,
imapClient: imapClient,
store: mailStore
)
}
func loadMailboxes(accountId: String) async {
guard let store else { return }
do {
let records = try store.mailboxes(accountId: accountId)
mailboxes = records.map { record in
MailboxInfo(
id: record.id, accountId: record.accountId,
name: record.name, unreadCount: 0, totalCount: 0
)
}
if selectedMailbox == nil, let inbox = mailboxes.first(where: { $0.name.lowercased() == "inbox" }) {
selectedMailbox = inbox
}
} catch {
errorMessage = error.localizedDescription
}
}
func startObservingThreads(accountId: String) {
guard let store else { return }
threadObservation?.cancel()
threadObservation = Task {
do {
for try await summaries in store.observeThreadSummaries(accountId: accountId) {
self.threads = summaries
}
} catch {
if !Task.isCancelled {
self.errorMessage = error.localizedDescription
}
}
}
}
func selectThread(_ thread: ThreadSummary) {
selectedThread = thread
messageObservation?.cancel()
guard let store else { return }
messageObservation = Task {
do {
for try await msgs in store.observeMessages(threadId: thread.id) {
self.messages = msgs
}
} catch {
if !Task.isCancelled {
self.errorMessage = error.localizedDescription
}
}
}
}
func syncNow() async {
guard let coordinator else { return }
do {
try await coordinator.syncNow()
syncState = coordinator.syncState
} catch {
errorMessage = error.localizedDescription
syncState = .error(error.localizedDescription)
}
}
func startPeriodicSync() {
coordinator?.startPeriodicSync()
}
func stopSync() {
coordinator?.stopSync()
threadObservation?.cancel()
messageObservation?.cancel()
}
static func databasePath(for accountId: String) -> String {
let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("MagnumOpus", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("\(accountId).sqlite").path
}
}