add mail, account setup viewmodels with grdb observation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
44
Apps/MagnumOpus/ViewModels/AccountSetupViewModel.swift
Normal file
44
Apps/MagnumOpus/ViewModels/AccountSetupViewModel.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
124
Apps/MagnumOpus/ViewModels/MailViewModel.swift
Normal file
124
Apps/MagnumOpus/ViewModels/MailViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user