658 lines
18 KiB
Dart
658 lines
18 KiB
Dart
/// 时间: 2025-03-23
|
||
/// 功能: 历史记录控制器
|
||
/// 介绍: 管理诗词历史记录的读取、写入、删除等操作
|
||
/// 最新变化: 添加笔记管理功能
|
||
|
||
import 'dart:convert';
|
||
import 'shared_preferences_storage_controller.dart';
|
||
|
||
/// 历史记录控制器类
|
||
/// 负责管理诗词浏览历史记录和点赞记录的本地存储和读取
|
||
class HistoryController {
|
||
static const String _historyKey = 'poetry_history';
|
||
static const String _likedKey = 'liked_poetry';
|
||
static const String _notesKey = 'user_notes';
|
||
static const int _maxHistoryCount = 100;
|
||
static bool _isAdding = false; // 防止并发添加
|
||
|
||
/// 获取历史记录列表
|
||
/// 返回按时间倒序排列的诗词历史记录
|
||
static Future<List<Map<String, dynamic>>> getHistory() async {
|
||
try {
|
||
final historyJson = await SharedPreferencesStorageController.getString(
|
||
_historyKey,
|
||
defaultValue: '[]',
|
||
);
|
||
if (historyJson.isEmpty) {
|
||
return [];
|
||
}
|
||
|
||
final List<dynamic> historyList = json.decode(historyJson);
|
||
return historyList
|
||
.map((item) => Map<String, dynamic>.from(item))
|
||
.toList();
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/// 添加诗词到历史记录
|
||
/// [poetryData] 要保存的诗词数据
|
||
/// 如果诗词已存在,则不会重复添加
|
||
/// 返回是否添加成功
|
||
static Future<bool> addToHistory(Map<String, dynamic> poetryData) async {
|
||
if (_isAdding) {
|
||
return false;
|
||
}
|
||
|
||
_isAdding = true;
|
||
|
||
try {
|
||
final historyJson = await SharedPreferencesStorageController.getString(
|
||
_historyKey,
|
||
defaultValue: '[]',
|
||
);
|
||
final List<dynamic> historyList = json.decode(historyJson);
|
||
|
||
final existingIndex = historyList.indexWhere(
|
||
(item) => item['id'] == poetryData['id'],
|
||
);
|
||
|
||
if (existingIndex >= 0) {
|
||
return false;
|
||
}
|
||
|
||
final enrichedPoetryData = Map<String, dynamic>.from(poetryData);
|
||
enrichedPoetryData['timestamp'] = DateTime.now().millisecondsSinceEpoch;
|
||
enrichedPoetryData['date'] = DateTime.now().toString().split(' ')[0];
|
||
|
||
historyList.insert(0, enrichedPoetryData);
|
||
|
||
if (historyList.length > _maxHistoryCount) {
|
||
historyList.removeRange(_maxHistoryCount, historyList.length);
|
||
}
|
||
|
||
final updatedHistoryJson = json.encode(historyList);
|
||
await SharedPreferencesStorageController.setString(
|
||
_historyKey,
|
||
updatedHistoryJson,
|
||
);
|
||
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
} finally {
|
||
_isAdding = false;
|
||
}
|
||
}
|
||
|
||
/// 从历史记录中移除指定诗词
|
||
/// [poetryId] 要移除的诗词ID
|
||
/// 返回是否移除成功
|
||
static Future<bool> removeFromHistory(int poetryId) async {
|
||
try {
|
||
final historyJson = await SharedPreferencesStorageController.getString(
|
||
_historyKey,
|
||
defaultValue: '[]',
|
||
);
|
||
final List<dynamic> historyList = json.decode(historyJson);
|
||
|
||
final originalLength = historyList.length;
|
||
|
||
historyList.removeWhere((item) => item['id'] == poetryId);
|
||
|
||
if (historyList.length < originalLength) {
|
||
final updatedHistoryJson = json.encode(historyList);
|
||
await SharedPreferencesStorageController.setString(
|
||
_historyKey,
|
||
updatedHistoryJson,
|
||
);
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 清空所有历史记录
|
||
/// 返回是否清空成功
|
||
static Future<bool> clearHistory() async {
|
||
try {
|
||
await SharedPreferencesStorageController.remove(_historyKey);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<int> getHistoryCount() async {
|
||
try {
|
||
final history = await getHistory();
|
||
return history.length;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
static Future<bool> isInHistory(int poetryId) async {
|
||
try {
|
||
final history = await getHistory();
|
||
return history.any((item) => item['id'] == poetryId);
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<List<Map<String, dynamic>>> searchHistory(
|
||
String keyword,
|
||
) async {
|
||
try {
|
||
final history = await getHistory();
|
||
|
||
if (keyword.isEmpty) {
|
||
return history;
|
||
}
|
||
|
||
final lowerKeyword = keyword.toLowerCase();
|
||
return history.where((item) {
|
||
final name = (item['name'] ?? '').toString().toLowerCase();
|
||
final alias = (item['alias'] ?? '').toString().toLowerCase();
|
||
final introduce = (item['introduce'] ?? '').toString().toLowerCase();
|
||
|
||
return name.contains(lowerKeyword) ||
|
||
alias.contains(lowerKeyword) ||
|
||
introduce.contains(lowerKeyword);
|
||
}).toList();
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/// 获取历史记录统计信息
|
||
/// 返回历史记录的统计数据
|
||
static Future<Map<String, dynamic>> getHistoryStats() async {
|
||
try {
|
||
final history = await getHistory();
|
||
|
||
if (history.isEmpty) {
|
||
return {
|
||
'totalCount': 0,
|
||
'todayCount': 0,
|
||
'thisWeekCount': 0,
|
||
'thisMonthCount': 0,
|
||
'topDynasties': <String, int>{},
|
||
};
|
||
}
|
||
|
||
final now = DateTime.now();
|
||
final today = DateTime(now.year, now.month, now.day);
|
||
final thisWeekStart = now.subtract(Duration(days: now.weekday - 1));
|
||
final thisMonthStart = DateTime(now.year, now.month, 1);
|
||
|
||
int todayCount = 0;
|
||
int thisWeekCount = 0;
|
||
int thisMonthCount = 0;
|
||
final Map<String, int> dynasties = {};
|
||
|
||
for (final item in history) {
|
||
final timestamp = item['timestamp'] as int?;
|
||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0);
|
||
|
||
// 统计今日
|
||
if (date.year == today.year &&
|
||
date.month == today.month &&
|
||
date.day == today.day) {
|
||
todayCount++;
|
||
}
|
||
|
||
// 统计本周
|
||
if (date.isAfter(thisWeekStart)) {
|
||
thisWeekCount++;
|
||
}
|
||
|
||
// 统计本月
|
||
if (date.isAfter(thisMonthStart)) {
|
||
thisMonthCount++;
|
||
}
|
||
|
||
// 统计朝代
|
||
final dynasty = item['alias']?.toString() ?? '未知';
|
||
dynasties[dynasty] = (dynasties[dynasty] ?? 0) + 1;
|
||
}
|
||
|
||
// 获取前5个最多朝代
|
||
final sortedDynasties = dynasties.entries.toList()
|
||
..sort((a, b) => b.value.compareTo(a.value))
|
||
..take(5);
|
||
|
||
return {
|
||
'totalCount': history.length,
|
||
'todayCount': todayCount,
|
||
'thisWeekCount': thisWeekCount,
|
||
'thisMonthCount': thisMonthCount,
|
||
'topDynasties': Map.fromEntries(sortedDynasties),
|
||
};
|
||
} catch (e) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
static Future<String> exportHistory({String format = 'json'}) async {
|
||
try {
|
||
final history = await getHistory();
|
||
|
||
if (format.toLowerCase() == 'csv') {
|
||
final buffer = StringBuffer();
|
||
buffer.writeln('ID,诗词名称,朝代,译文,原文,日期,时间戳');
|
||
|
||
for (final item in history) {
|
||
buffer.writeln(
|
||
'${item['id']},${item['name']},${item['alias']},${item['introduce']},${item['drtime']},${item['date']},${item['timestamp']}',
|
||
);
|
||
}
|
||
|
||
return buffer.toString();
|
||
} else {
|
||
return json.encode(history);
|
||
}
|
||
} catch (e) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
// ========== 点赞记录管理 ==========
|
||
|
||
/// 获取点赞诗词列表
|
||
/// 返回点赞的诗词列表
|
||
static Future<List<Map<String, dynamic>>> getLikedHistory() async {
|
||
try {
|
||
final likedJson = await SharedPreferencesStorageController.getString(
|
||
_likedKey,
|
||
defaultValue: '[]',
|
||
);
|
||
if (likedJson.isEmpty) {
|
||
return [];
|
||
}
|
||
|
||
final List<dynamic> likedList = json.decode(likedJson);
|
||
return likedList.map((item) => Map<String, dynamic>.from(item)).toList();
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
static Future<bool> addToLiked(Map<String, dynamic> poetryData) async {
|
||
try {
|
||
final likedJson = await SharedPreferencesStorageController.getString(
|
||
_likedKey,
|
||
defaultValue: '[]',
|
||
);
|
||
final List<dynamic> likedList = json.decode(likedJson);
|
||
|
||
final existingIndex = likedList.indexWhere(
|
||
(item) => item['id'] == poetryData['id'],
|
||
);
|
||
|
||
if (existingIndex >= 0) {
|
||
return false;
|
||
}
|
||
|
||
final enrichedPoetryData = Map<String, dynamic>.from(poetryData);
|
||
final now = DateTime.now();
|
||
enrichedPoetryData['liked_timestamp'] = now.millisecondsSinceEpoch;
|
||
enrichedPoetryData['liked_date'] = now.toString().split(' ')[0];
|
||
enrichedPoetryData['liked_time'] =
|
||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
||
|
||
likedList.insert(0, enrichedPoetryData);
|
||
|
||
final updatedLikedJson = json.encode(likedList);
|
||
await SharedPreferencesStorageController.setString(
|
||
_likedKey,
|
||
updatedLikedJson,
|
||
);
|
||
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> removeLikedPoetry(String poetryId) async {
|
||
try {
|
||
final likedJson = await SharedPreferencesStorageController.getString(
|
||
_likedKey,
|
||
defaultValue: '[]',
|
||
);
|
||
final List<dynamic> likedList = json.decode(likedJson);
|
||
|
||
final originalLength = likedList.length;
|
||
|
||
likedList.removeWhere((item) => item['id'].toString() == poetryId);
|
||
|
||
if (likedList.length < originalLength) {
|
||
final updatedLikedJson = json.encode(likedList);
|
||
await SharedPreferencesStorageController.setString(
|
||
_likedKey,
|
||
updatedLikedJson,
|
||
);
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> isInLiked(String poetryId) async {
|
||
try {
|
||
final likedList = await getLikedHistory();
|
||
return likedList.any((item) => item['id'].toString() == poetryId);
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> clearLikedHistory() async {
|
||
try {
|
||
await SharedPreferencesStorageController.remove(_likedKey);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<int> getLikedCount() async {
|
||
try {
|
||
final likedList = await getLikedHistory();
|
||
return likedList.length;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// ========== 笔记管理 ==========
|
||
|
||
/// 笔记数据结构说明:
|
||
/// - id: 笔记唯一标识(时间戳)
|
||
/// - title: 标题(可选)
|
||
/// - content: 内容
|
||
/// - time: 保存时间(ISO8601格式)
|
||
/// - createTime: 创建时间(ISO8601格式)
|
||
/// - charCount: 字数统计
|
||
/// - isPinned: 是否置顶(true/false)
|
||
/// - isLocked: 是否锁定(true/false)
|
||
/// - password: 访问密码(加密存储)
|
||
/// - category: 分类(可选)
|
||
|
||
/// 获取所有笔记列表
|
||
/// 返回笔记列表(置顶的排在前面)
|
||
static Future<List<Map<String, dynamic>>> getNotes() async {
|
||
try {
|
||
final notesJson = await SharedPreferencesStorageController.getString(
|
||
_notesKey,
|
||
);
|
||
if (notesJson.isEmpty) {
|
||
// 返回默认笔记示例
|
||
return _getDefaultNotes();
|
||
}
|
||
|
||
final List<dynamic> notesList = json.decode(notesJson);
|
||
final notes = notesList
|
||
.map((item) => Map<String, dynamic>.from(item))
|
||
.toList();
|
||
|
||
// 置顶的笔记排在前面
|
||
notes.sort((a, b) {
|
||
final aPinned = a['isPinned'] == true;
|
||
final bPinned = b['isPinned'] == true;
|
||
if (aPinned && !bPinned) return -1;
|
||
if (!aPinned && bPinned) return 1;
|
||
return 0;
|
||
});
|
||
|
||
return notes;
|
||
} catch (e) {
|
||
return _getDefaultNotes();
|
||
}
|
||
}
|
||
|
||
/// 获取默认笔记示例
|
||
static List<Map<String, dynamic>> _getDefaultNotes() {
|
||
final now = DateTime.now();
|
||
return [
|
||
{
|
||
'id': 'default_note_1',
|
||
'title': '欢迎使用笔记功能',
|
||
'content':
|
||
'这是一个示例笔记。\n\n你可以:\n• 创建新笔记\n• 编辑和删除笔记\n• 置顶重要笔记\n• 🔒 锁定笔记保护隐私\n• 选择分类整理笔记\n\n笔记会自动保存,无需手动操作。\n\n🔒 锁定功能说明:\n点击右上角锁图标可设置密码,设置后笔记将被锁定保护。\n\n🔐 体验锁定笔记:\n下方有一个默认锁定的笔记,密码为 qjsc\n点击即可体验锁定功能。',
|
||
'time': now.toIso8601String(),
|
||
'createTime': now.toIso8601String(),
|
||
'charCount': 100,
|
||
'isPinned': true,
|
||
'isLocked': false,
|
||
'password': null,
|
||
'category': '使用说明',
|
||
},
|
||
{
|
||
'id': 'default_note_2',
|
||
'title': '🔒 锁定笔记使用说明',
|
||
'content':
|
||
'这是一个锁定的笔记示例。\n\n使用方法:\n1. 点击右上角锁图标设置密码\n2. 设置密码后笔记会被锁定\n3. 在笔记列表中点击可进入编辑\n4. 再次点击锁图标可修改密码\n\n⚠️ 安全提示:\n• 笔记仅保存在本地设备\n• 不会上传到任何服务器\n• 与应用数据共存亡\n• 卸载应用后笔记将丢失\n• 请妥善保管您的密码\n\n🔐 当前密码:qjsc',
|
||
'time': now.toIso8601String(),
|
||
'createTime': now.toIso8601String(),
|
||
'charCount': 120,
|
||
'isPinned': false,
|
||
'isLocked': true,
|
||
'password': 'qjsc',
|
||
'category': '安全说明',
|
||
},
|
||
];
|
||
}
|
||
|
||
/// 保存笔记(新建或更新)
|
||
/// [noteId] 笔记ID,为null时新建
|
||
/// [title] 标题
|
||
/// [content] 内容
|
||
/// [isPinned] 是否置顶
|
||
/// [isLocked] 是否锁定
|
||
/// [password] 访问密码
|
||
/// [category] 分类
|
||
/// [createTime] 创建时间(可选,用于保留原创建时间)
|
||
/// 返回笔记ID
|
||
static Future<String?> saveNote({
|
||
String? noteId,
|
||
required String title,
|
||
required String content,
|
||
bool? isPinned,
|
||
bool? isLocked,
|
||
String? password,
|
||
String? category,
|
||
String? createTime,
|
||
}) async {
|
||
try {
|
||
final notes = await getNotes();
|
||
final now = DateTime.now();
|
||
final id = noteId ?? now.millisecondsSinceEpoch.toString();
|
||
|
||
// 如果是更新,保留原有的状态
|
||
bool pinned = isPinned ?? false;
|
||
bool locked = isLocked ?? false;
|
||
String? pwd = password;
|
||
String? cat = category;
|
||
String? ct = createTime;
|
||
|
||
if (noteId != null) {
|
||
final existingNote = notes.firstWhere(
|
||
(n) => n['id'] == noteId,
|
||
orElse: () => <String, dynamic>{},
|
||
);
|
||
if (isPinned == null) {
|
||
pinned = existingNote['isPinned'] ?? false;
|
||
}
|
||
if (isLocked == null) {
|
||
locked = existingNote['isLocked'] ?? false;
|
||
}
|
||
if (password == null) {
|
||
pwd = existingNote['password'];
|
||
}
|
||
if (category == null) {
|
||
cat = existingNote['category'];
|
||
}
|
||
// 如果没有传入创建时间,使用原有的创建时间
|
||
if (ct == null) {
|
||
ct = existingNote['createTime'];
|
||
}
|
||
}
|
||
|
||
// 新建笔记时设置创建时间
|
||
if (ct == null) {
|
||
ct = now.toIso8601String();
|
||
}
|
||
|
||
final noteData = {
|
||
'id': id,
|
||
'title': title,
|
||
'content': content,
|
||
'time': now.toIso8601String(),
|
||
'createTime': ct,
|
||
'charCount': title.length + content.length,
|
||
'isPinned': pinned,
|
||
'isLocked': locked,
|
||
'password': pwd,
|
||
'category': cat,
|
||
};
|
||
|
||
if (noteId != null) {
|
||
final index = notes.indexWhere((n) => n['id'] == noteId);
|
||
if (index != -1) {
|
||
notes[index] = noteData;
|
||
} else {
|
||
notes.insert(0, noteData);
|
||
}
|
||
} else {
|
||
notes.insert(0, noteData);
|
||
}
|
||
|
||
final notesJson = json.encode(notes);
|
||
await SharedPreferencesStorageController.setString(_notesKey, notesJson);
|
||
|
||
return id;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
static Future<Map<String, dynamic>?> getNote(String noteId) async {
|
||
try {
|
||
final notes = await getNotes();
|
||
return notes.firstWhere(
|
||
(n) => n['id'] == noteId,
|
||
orElse: () => <String, dynamic>{},
|
||
);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// 删除笔记
|
||
/// [noteId] 笔记ID
|
||
static Future<bool> deleteNote(String noteId) async {
|
||
try {
|
||
final notes = await getNotes();
|
||
notes.removeWhere((n) => n['id'] == noteId);
|
||
|
||
final notesJson = json.encode(notes);
|
||
await SharedPreferencesStorageController.setString(_notesKey, notesJson);
|
||
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> togglePinNote(String noteId) async {
|
||
try {
|
||
final notes = await getNotes();
|
||
final index = notes.indexWhere((n) => n['id'] == noteId);
|
||
|
||
if (index == -1) {
|
||
return false;
|
||
}
|
||
|
||
final currentPinned = notes[index]['isPinned'] ?? false;
|
||
notes[index]['isPinned'] = !currentPinned;
|
||
|
||
final notesJson = json.encode(notes);
|
||
await SharedPreferencesStorageController.setString(_notesKey, notesJson);
|
||
|
||
return !currentPinned;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> setNotePassword(String noteId, String? password) async {
|
||
try {
|
||
final notes = await getNotes();
|
||
final index = notes.indexWhere((n) => n['id'] == noteId);
|
||
|
||
if (index == -1) {
|
||
return false;
|
||
}
|
||
|
||
if (password == null || password.isEmpty) {
|
||
notes[index]['isLocked'] = false;
|
||
notes[index]['password'] = null;
|
||
} else {
|
||
notes[index]['isLocked'] = true;
|
||
notes[index]['password'] = password;
|
||
}
|
||
|
||
final notesJson = json.encode(notes);
|
||
await SharedPreferencesStorageController.setString(_notesKey, notesJson);
|
||
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> verifyNotePassword(String noteId, String password) async {
|
||
try {
|
||
final notes = await getNotes();
|
||
final note = notes.firstWhere(
|
||
(n) => n['id'] == noteId,
|
||
orElse: () => <String, dynamic>{},
|
||
);
|
||
|
||
if (note.isEmpty) {
|
||
return false;
|
||
}
|
||
|
||
final storedPassword = note['password'] as String?;
|
||
if (storedPassword == null || storedPassword.isEmpty) {
|
||
return true;
|
||
}
|
||
|
||
return storedPassword == password;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<int> getNotesCount() async {
|
||
try {
|
||
final notes = await getNotes();
|
||
return notes.length;
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
}
|