Files
mail-hub/lib/ui/app_state.dart
hz4th_coder 6c1225f588 MailHub v1.0.0: 跨平台邮箱客户端(纯 Dart IMAP/SMTP/MIME 协议栈)
- 登录: QQ/163/126/189/搜狐/Gmail/Outlook/Yahoo/iCloud/企业邮/自定义, 自动识别+测试连接
- 收信: IMAP 多文件夹同步/未读/星标/搜索(本地+服务器)
- 读信: HTML渲染/CID内嵌图/原文查看/附件下载
- 写信: 收件人/抄送/密送/附件/富文本/草稿/回复转发
- 存储: SQLite + 系统安全存储(DPAPI/Keychain/Keystore)
- 测试: 单元测试 + Mock IMAP/SMTP 服务器集成测试(23项全过)
- 可迁移: Windows/Android/iOS/macOS/Linux 一套代码
2026-08-18 01:10:11 +08:00

388 lines
12 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import '../core/models/account.dart';
import '../core/models/address.dart';
import '../core/models/email.dart';
import '../core/service/email_service.dart';
import '../core/storage/stores.dart';
/// 全局应用状态(ChangeNotifier, 单例)
class AppState extends ChangeNotifier {
AppState._();
static final AppState instance = AppState._();
/// 深色模式开关(供 MaterialApp 监听)
final ValueNotifier<bool> darkMode = ValueNotifier(false);
final AccountStore _accountStore = AccountStore();
final FolderStore _folderStore = FolderStore();
final EmailStore _emailStore = EmailStore();
final EmailService _service = EmailService();
// ---- 数据 ----
List<EmailAccount> accounts = [];
Map<int, List<MailFolder>> foldersByAccount = {};
Map<int, Map<String, int>> unreadByFolder = {}; // accountId -> folder -> unread
// ---- 界面状态 ----
int? currentAccountId;
String? currentFolder; // 完整文件夹名
List<Email> currentEmails = [];
bool loadingEmails = false;
String? syncError;
EmailAccount? get currentAccount {
for (final a in accounts) {
if (a.id == currentAccountId) return a;
}
return null;
}
List<MailFolder> get currentFolders =>
foldersByAccount[currentAccountId] ?? const [];
bool get isBusy => loadingEmails;
// ---- 初始化 ----
Future<void> init() async {
await loadAccounts();
}
Future<void> loadAccounts() async {
accounts = await _accountStore.getAll();
if (accounts.isNotEmpty) {
currentAccountId ??= accounts.first.id;
}
for (final a in accounts) {
final folders = await _folderStore.getByAccount(a.id!);
foldersByAccount[a.id!] = folders;
final unread = <String, int>{};
for (final f in folders) {
unread[f.name] = await _emailStore.countUnread(a.id!, f.name);
}
unreadByFolder[a.id!] = unread;
if (currentFolder == null && folders.isNotEmpty) {
currentFolder = folders.first.name;
}
}
notifyListeners();
}
Future<void> addAccount(EmailAccount account, String password) async {
final saved = await _accountStore.insert(account);
await SecureStore.savePassword(account.email, password);
// 同步文件夹
try {
final folders = await _service.syncFolders(saved, password);
foldersByAccount[saved.id!] = folders;
unreadByFolder[saved.id!] = {
for (final f in folders) f.name: 0
};
} catch (_) {}
await loadAccounts();
currentAccountId ??= saved.id;
notifyListeners();
}
Future<void> removeAccount(EmailAccount account) async {
await _accountStore.delete(account.id!);
await SecureStore.deletePassword(account.email);
foldersByAccount.remove(account.id!);
unreadByFolder.remove(account.id!);
if (currentAccountId == account.id) {
currentAccountId = accounts.isNotEmpty ? accounts.first.id : null;
currentFolder = null;
currentEmails = [];
}
await loadAccounts();
notifyListeners();
}
Future<void> updateAccount(EmailAccount account, {String? newPassword}) async {
await _accountStore.update(account);
if (newPassword != null) {
await SecureStore.savePassword(account.email, newPassword);
}
await loadAccounts();
notifyListeners();
}
Future<String?> getPassword(EmailAccount account) =>
SecureStore.readPassword(account.email);
// ---- 文件夹与邮件 ----
void selectAccount(int accountId) {
currentAccountId = accountId;
final folders = foldersByAccount[accountId] ?? [];
if (folders.isNotEmpty) {
final inbox = folders.where((f) => f.kind == 'inbox').toList();
currentFolder = inbox.isNotEmpty ? inbox.first.name : folders.first.name;
} else {
currentFolder = null;
}
currentEmails = [];
notifyListeners();
loadEmails();
}
void selectFolder(String folder) {
currentFolder = folder;
currentEmails = [];
notifyListeners();
loadEmails();
}
/// 加载当前文件夹邮件(本地)
Future<void> loadEmails() async {
final account = currentAccount;
final folder = currentFolder;
if (account == null || folder == null) return;
loadingEmails = true;
notifyListeners();
currentEmails = await _emailStore.queryByFolder(account.id!, folder);
loadingEmails = false;
notifyListeners();
}
/// 刷新:同步当前文件夹 + 更新未读数
Future<void> refresh({bool full = false}) async {
final account = currentAccount;
if (account == null) return;
syncError = null;
notifyListeners();
try {
final password = await SecureStore.readPassword(account.email);
if (password == null) {
syncError = '未找到密码,请在账户设置中重新输入';
notifyListeners();
return;
}
final result = await _service.syncAll(account, password, fullResync: full);
// 刷新文件夹缓存
foldersByAccount[account.id!] = await _folderStore.getByAccount(account.id!);
// 更新未读
final unread = <String, int>{};
for (final f in foldersByAccount[account.id!] ?? []) {
unread[f.name] = await _emailStore.countUnread(account.id!, f.name);
}
unreadByFolder[account.id!] = unread;
if (result.hasError) {
syncError = result.errors.first;
}
} catch (e) {
syncError = '$e';
}
await loadEmails();
notifyListeners();
}
/// 重新拉取当前文件夹
Future<void> resyncCurrentFolder() async {
final account = currentAccount;
final folder = currentFolder;
if (account == null || folder == null) return;
final password = await SecureStore.readPassword(account.email);
if (password == null) return;
await _service.syncFolder(account, password, folder, fullResync: true);
await loadEmails();
}
// ---- 邮件操作 ----
Future<void> markSeen(List<Email> emails, {bool seen = true}) async {
final account = currentAccount;
if (account == null) return;
final password = await SecureStore.readPassword(account.email);
if (password == null) return;
try {
await _service.markSeen(account, password, emails, seen: seen);
} catch (e) {
syncError = '$e';
}
await loadEmails();
notifyListeners();
}
Future<void> toggleFlag(Email email) async {
final account = currentAccount;
if (account == null) return;
final password = await SecureStore.readPassword(account.email);
if (password == null) return;
try {
await _service.toggleFlag(account, password, email);
} catch (e) {
syncError = '$e';
}
await loadEmails();
notifyListeners();
}
Future<void> deleteEmails(List<Email> emails) async {
final account = currentAccount;
if (account == null) return;
final password = await SecureStore.readPassword(account.email);
if (password == null) return;
try {
await _service.deleteEmails(account, password, emails);
} catch (e) {
syncError = '$e';
}
await loadEmails();
notifyListeners();
}
Future<void> moveEmails(List<Email> emails, String destFolder) async {
final account = currentAccount;
if (account == null) return;
final password = await SecureStore.readPassword(account.email);
if (password == null) return;
try {
await _service.moveEmails(account, password, emails, destFolder);
} catch (e) {
syncError = '$e';
}
await loadEmails();
notifyListeners();
}
Future<Email> openEmail(Email meta, {bool forceRefresh = false}) async {
final account = currentAccount;
if (account == null) return meta;
final password = await SecureStore.readPassword(account.email);
if (password == null) return meta;
final full = await _service.getFullEmail(account, password, meta,
forceRefresh: forceRefresh);
if (!meta.seen) {
// 更新未读计数
final unread = unreadByFolder[account.id!] ?? {};
unread[meta.folder] = (unread[meta.folder] ?? 0) - 1 < 0 ? 0 : unread[meta.folder]! - 1;
unreadByFolder[account.id!] = unread;
}
await loadEmails();
notifyListeners();
return full;
}
Future<MailAttachment> downloadAttachment(
Email meta, MailAttachment attachment) async {
final account = currentAccount;
if (account == null) return attachment;
final password = await SecureStore.readPassword(account.email);
if (password == null) return attachment;
return _service.downloadAttachment(account, password, meta, attachment);
}
Future<void> sendEmail({
required List<MailAddress> to,
List<MailAddress> cc = const [],
List<MailAddress> bcc = const [],
required String subject,
String? textBody,
String? htmlBody,
List<({String name, List<int> bytes, String contentType})> attachments = const [],
String? inReplyTo,
String? references,
}) async {
final account = currentAccount;
if (account == null) throw Exception('请先添加账户');
final password = await SecureStore.readPassword(account.email);
if (password == null) throw Exception('未找到密码,请在账户设置中重新输入');
await _service.sendEmail(
account,
password,
to: to,
cc: cc,
bcc: bcc,
subject: subject,
textBody: textBody,
htmlBody: htmlBody,
attachments: attachments,
inReplyTo: inReplyTo,
references: references,
);
// 刷新已发送
final folders = foldersByAccount[account.id!] ?? [];
for (final f in folders) {
if (f.kind == 'sent') {
final pwd = await SecureStore.readPassword(account.email);
if (pwd != null) {
try {
await _service.syncFolder(account, pwd, f.name);
} catch (_) {}
}
}
}
await loadEmails();
notifyListeners();
}
Future<void> saveDraft({
required List<MailAddress> to,
List<MailAddress> cc = const [],
required String subject,
String? textBody,
}) async {
final account = currentAccount;
if (account == null) return;
final password = await SecureStore.readPassword(account.email);
if (password == null) return;
await _service.saveDraft(
account,
password,
to: to,
cc: cc,
subject: subject,
textBody: textBody,
);
await loadEmails();
notifyListeners();
}
/// 搜索
Future<List<Email>> search(String query, {bool remote = true}) async {
final account = currentAccount;
if (account == null) return [];
final local = await _emailStore.searchLocal(account.id!, query);
if (!remote) return local;
final password = await SecureStore.readPassword(account.email);
if (password == null) return local;
try {
final folder = currentFolder ?? 'INBOX';
final remote = await _service.searchRemote(account, password, query,
folder: folder);
// 合并去重
final seen = <String>{};
final merged = <Email>[...remote, ...local];
final result = <Email>[];
for (final e in merged) {
final key = '${e.folder}:${e.uid}';
if (seen.add(key)) result.add(e);
}
return result;
} catch (_) {
return local;
}
}
// ---- 偏好设置 ----
Future<bool> getDarkMode() async =>
await PrefStore.getBool('dark_mode', def: false);
Future<void> setDarkMode(bool v) {
darkMode.value = v;
return PrefStore.setBool('dark_mode', v);
}
Future<int> getSyncInterval() async =>
await PrefStore.getInt('sync_interval', def: 5);
Future<void> setSyncInterval(int minutes) =>
PrefStore.setInt('sync_interval', minutes);
}