feat: 新增壁纸图库组件和编辑器功能优化
- 新增壁纸图库相关组件(WallpaperGalleryView/WallpaperSearchBar等) - 优化编辑器主题服务和系统UI管理 - 新增虚线边框和拖拽描边风格支持 - 完善今日诗词服务和阅读报告功能 - 修复多个UI问题和空指针异常 - 更新依赖库版本和SVG资源 - 优化交互动画和状态管理 - 补充文档和API测试脚本
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — Hive KV 存储封装
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-04-28
|
||||
/// 更新时间: 2026-05-02
|
||||
/// 作用: 基于 Hive 的高性能 KV 存储,替代部分 SharedPreferences
|
||||
/// 上次更新: 修复 import 路径 + 类型参数 + SP 数据迁移
|
||||
/// 上次更新: 修复init()内_migrateFromSharedPreferences自锁问题
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
@@ -58,11 +58,11 @@ class AppKVStore {
|
||||
|
||||
static Future<void> _migrateFromSharedPreferences() async {
|
||||
try {
|
||||
final migrated = _box(HiveBoxNames.app).get('_sp_migrated') as bool?;
|
||||
final appBox = Hive.box<dynamic>(HiveBoxNames.app);
|
||||
final migrated = appBox.get('_sp_migrated') as bool?;
|
||||
if (migrated == true) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final appBox = _box(HiveBoxNames.app);
|
||||
|
||||
for (final key in prefs.getKeys()) {
|
||||
final value = prefs.get(key);
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
/// ============================================================
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'database_connection/native.dart'
|
||||
if (dart.library.html) 'database_connection/web.dart';
|
||||
|
||||
@@ -29,6 +30,7 @@ class Sentences extends Table {
|
||||
IntColumn get views => integer().withDefault(const Constant(0))();
|
||||
TextColumn get imageUrl => text().withDefault(const Constant(''))();
|
||||
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isLiked => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isRead => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
@@ -167,6 +169,40 @@ class ShareHistories extends Table {
|
||||
DateTimeColumn get sharedAt => dateTime()();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 学习计划表
|
||||
// ============================================================
|
||||
|
||||
class LearningPlans extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text()();
|
||||
TextColumn get description => text().withDefault(const Constant(''))();
|
||||
TextColumn get category => text().withDefault(const Constant('poetry'))();
|
||||
IntColumn get dailyGoal => integer().withDefault(const Constant(5))();
|
||||
IntColumn get streakDays => integer().withDefault(const Constant(0))();
|
||||
IntColumn get totalCompleted => integer().withDefault(const Constant(0))();
|
||||
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
|
||||
DateTimeColumn get startDate => dateTime()();
|
||||
DateTimeColumn get endDate => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 学习记录表
|
||||
// ============================================================
|
||||
|
||||
class LearningRecords extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get planId => integer()();
|
||||
TextColumn get contentId => text().withDefault(const Constant(''))();
|
||||
TextColumn get contentType => text().withDefault(const Constant('poetry'))();
|
||||
TextColumn get title => text().withDefault(const Constant(''))();
|
||||
TextColumn get note => text().withDefault(const Constant(''))();
|
||||
IntColumn get durationSeconds => integer().withDefault(const Constant(0))();
|
||||
DateTimeColumn get completedAt => dateTime()();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 数据库实例
|
||||
// ============================================================
|
||||
@@ -182,6 +218,8 @@ class ShareHistories extends Table {
|
||||
OfflineActionQueue,
|
||||
HanziCaches,
|
||||
ShareHistories,
|
||||
LearningPlans,
|
||||
LearningRecords,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
@@ -191,7 +229,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
static AppDatabase get instance => _instance;
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 8;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -216,6 +254,15 @@ class AppDatabase extends _$AppDatabase {
|
||||
if (from < 6) {
|
||||
await m.createTable(shareHistories);
|
||||
}
|
||||
if (from < 7) {
|
||||
await m.createTable(learningPlans);
|
||||
await m.createTable(learningRecords);
|
||||
}
|
||||
if (from < 8) {
|
||||
await customStatement(
|
||||
'ALTER TABLE sentences ADD COLUMN isLiked INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -246,8 +293,22 @@ class AppDatabase extends _$AppDatabase {
|
||||
return (select(sentences)..where((t) => t.isFavorite.equals(true))).get();
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(String id) {
|
||||
return (update(sentences)..where((t) => t.id.equals(id))).write(
|
||||
Future<List<Sentence>> getLikedSentences() {
|
||||
return (select(sentences)..where((t) => t.isLiked.equals(true))).get();
|
||||
}
|
||||
|
||||
Future<int> getLikedCount() {
|
||||
return (select(
|
||||
sentences,
|
||||
)..where((t) => t.isLiked.equals(true))).get().then((rows) => rows.length);
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(String id) async {
|
||||
final exists = await (select(
|
||||
sentences,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
if (exists == null) return;
|
||||
await (update(sentences)..where((t) => t.id.equals(id))).write(
|
||||
SentencesCompanion.custom(
|
||||
isFavorite: sentences.isFavorite.not(),
|
||||
updatedAt: Constant(DateTime.now()),
|
||||
@@ -255,8 +316,25 @@ class AppDatabase extends _$AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> markAsRead(String id) {
|
||||
return (update(sentences)..where((t) => t.id.equals(id))).write(
|
||||
Future<void> toggleLike(String id) async {
|
||||
final exists = await (select(
|
||||
sentences,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
if (exists == null) return;
|
||||
await (update(sentences)..where((t) => t.id.equals(id))).write(
|
||||
SentencesCompanion(
|
||||
isLiked: Value(!exists.isLiked),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> markAsRead(String id) async {
|
||||
final exists = await (select(
|
||||
sentences,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
if (exists == null) return;
|
||||
await (update(sentences)..where((t) => t.id.equals(id))).write(
|
||||
SentencesCompanion.custom(
|
||||
isRead: const Constant(true),
|
||||
updatedAt: Constant(DateTime.now()),
|
||||
@@ -288,6 +366,209 @@ class AppDatabase extends _$AppDatabase {
|
||||
)..where((t) => t.sentenceId.equals(sentenceId))).go();
|
||||
}
|
||||
|
||||
Future<void> deleteReadHistoryBatch(List<String> sentenceIds) {
|
||||
return (delete(
|
||||
readHistory,
|
||||
)..where((t) => t.sentenceId.isIn(sentenceIds))).go();
|
||||
}
|
||||
|
||||
Future<int> getReadHistoryCount() async {
|
||||
final rows = await (selectOnly(
|
||||
readHistory,
|
||||
)..addColumns([readHistory.id.count()])).getSingle();
|
||||
return rows.read(readHistory.id.count()) ?? 0;
|
||||
}
|
||||
|
||||
Future<int> getReadHistoryCountSince(DateTime since) async {
|
||||
final rows =
|
||||
await (selectOnly(readHistory)
|
||||
..addColumns([readHistory.id.count()])
|
||||
..where(readHistory.readAt.isBiggerOrEqualValue(since)))
|
||||
.getSingle();
|
||||
return rows.read(readHistory.id.count()) ?? 0;
|
||||
}
|
||||
|
||||
Future<List<HistorySentenceWithTime>> searchHistorySentences(
|
||||
String query, {
|
||||
int limit = 50,
|
||||
}) async {
|
||||
final q = '%$query%';
|
||||
final history =
|
||||
await (select(readHistory)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.readAt)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
if (history.isEmpty) return [];
|
||||
final ids = history.map((h) => h.sentenceId).toSet();
|
||||
final rows =
|
||||
await (select(sentences)..where(
|
||||
(t) => t.id.isIn(ids) & (t.content.like(q) | t.author.like(q)),
|
||||
))
|
||||
.get();
|
||||
final sentenceMap = {for (final r in rows) r.id: r};
|
||||
return history
|
||||
.where((h) => sentenceMap.containsKey(h.sentenceId))
|
||||
.map(
|
||||
(h) => HistorySentenceWithTime(
|
||||
sentence: sentenceMap[h.sentenceId]!,
|
||||
readAt: h.readAt,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.readAt.compareTo(a.readAt));
|
||||
}
|
||||
|
||||
Future<List<HistorySentenceWithTime>> getHistorySince(
|
||||
DateTime since, {
|
||||
int limit = 200,
|
||||
}) async {
|
||||
final history =
|
||||
await (select(readHistory)
|
||||
..where((t) => t.readAt.isBiggerOrEqualValue(since))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.readAt)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
if (history.isEmpty) return [];
|
||||
final ids = history.map((h) => h.sentenceId).toSet();
|
||||
final rows = await (select(sentences)..where((t) => t.id.isIn(ids))).get();
|
||||
final sentenceMap = {for (final r in rows) r.id: r};
|
||||
return history
|
||||
.where((h) => sentenceMap.containsKey(h.sentenceId))
|
||||
.map(
|
||||
(h) => HistorySentenceWithTime(
|
||||
sentence: sentenceMap[h.sentenceId]!,
|
||||
readAt: h.readAt,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.readAt.compareTo(a.readAt));
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getHistoryCountByDate({int days = 7}) async {
|
||||
final result = <String, int>{};
|
||||
final now = DateTime.now();
|
||||
for (int i = 0; i < days; i++) {
|
||||
final day = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(Duration(days: i));
|
||||
final nextDay = day.add(const Duration(days: 1));
|
||||
final count = await getReadHistoryCountSince(day);
|
||||
final dayAfterNext = await getReadHistoryCountSince(nextDay);
|
||||
final dayCount = count - dayAfterNext;
|
||||
final label = i == 0
|
||||
? '今天'
|
||||
: i == 1
|
||||
? '昨天'
|
||||
: DateFormat('MM/dd').format(day);
|
||||
result[label] = dayCount;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<int> getFavoriteCount() async {
|
||||
final rows =
|
||||
await (selectOnly(sentences)
|
||||
..addColumns([sentences.id.count()])
|
||||
..where(sentences.isFavorite.equals(true)))
|
||||
.getSingle();
|
||||
return rows.read(sentences.id.count()) ?? 0;
|
||||
}
|
||||
|
||||
Future<List<Sentence>> searchFavoriteSentences(
|
||||
String query, {
|
||||
int limit = 50,
|
||||
}) async {
|
||||
final q = '%$query%';
|
||||
return (select(sentences)
|
||||
..where(
|
||||
(t) =>
|
||||
t.isFavorite.equals(true) &
|
||||
(t.content.like(q) | t.author.like(q) | t.source.like(q)),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.updatedAt)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getFavoriteCountByFeedType() async {
|
||||
final rows =
|
||||
await (selectOnly(sentences)
|
||||
..addColumns([sentences.id.count(), sentences.feedType])
|
||||
..where(sentences.isFavorite.equals(true))
|
||||
..groupBy([sentences.feedType]))
|
||||
.get();
|
||||
final result = <String, int>{};
|
||||
for (final row in rows) {
|
||||
final feedType = row.read(sentences.feedType) ?? '其他';
|
||||
final count = row.read(sentences.id.count()) ?? 0;
|
||||
if (count > 0) result[feedType] = count;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getHistoryCountByFeedType() async {
|
||||
final query = selectOnly(sentences)
|
||||
..addColumns([sentences.id.count(), sentences.feedType])
|
||||
..where(sentences.isRead.equals(true))
|
||||
..groupBy([sentences.feedType]);
|
||||
final rows = await query.get();
|
||||
final result = <String, int>{};
|
||||
for (final row in rows) {
|
||||
final feedType = row.read(sentences.feedType) ?? '其他';
|
||||
final count = row.read(sentences.id.count()) ?? 0;
|
||||
if (count > 0) result[feedType] = count;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<int> getShareHistoryCount() async {
|
||||
final rows = await (selectOnly(
|
||||
shareHistories,
|
||||
)..addColumns([shareHistories.id.count()])).getSingle();
|
||||
return rows.read(shareHistories.id.count()) ?? 0;
|
||||
}
|
||||
|
||||
Future<int> getFeedCacheSize() async {
|
||||
return getFeedCacheCount();
|
||||
}
|
||||
|
||||
Future<int> getHanziCacheCount() async {
|
||||
final rows = await (selectOnly(
|
||||
hanziCaches,
|
||||
)..addColumns([hanziCaches.cacheKey.count()])).getSingle();
|
||||
return rows.read(hanziCaches.cacheKey.count()) ?? 0;
|
||||
}
|
||||
|
||||
Future<int> getOfflineActionCount() async {
|
||||
return getPendingActionCount();
|
||||
}
|
||||
|
||||
Future<void> clearAllFavorites() async {
|
||||
await (update(sentences)..where((t) => t.isFavorite.equals(true))).write(
|
||||
const SentencesCompanion(isFavorite: Value(false)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearAllNotes() async {
|
||||
final box = await Hive.openBox<dynamic>('notes');
|
||||
await box.clear();
|
||||
}
|
||||
|
||||
Future<void> clearAllShareHistory() async {
|
||||
await clearShareHistories();
|
||||
}
|
||||
|
||||
Future<void> clearAllHanziCache() async {
|
||||
await clearHanziCache();
|
||||
}
|
||||
|
||||
Future<int> getNoteCount() async {
|
||||
final box = await Hive.openBox<dynamic>('notes');
|
||||
return box.length;
|
||||
}
|
||||
|
||||
Future<void> clearAllReadHistory() {
|
||||
return delete(readHistory).go();
|
||||
}
|
||||
@@ -309,7 +590,14 @@ class AppDatabase extends _$AppDatabase {
|
||||
author: Value(item.author),
|
||||
source: Value(item.source),
|
||||
tags: Value(item.tags),
|
||||
feedType: Value(item.feedType),
|
||||
feedName: Value(item.feedName),
|
||||
feedIcon: Value(item.feedIcon),
|
||||
views: Value(item.views),
|
||||
imageUrl: Value(item.imageUrl),
|
||||
isFavorite: Value(item.isFavorite),
|
||||
isLiked: Value(item.isLiked),
|
||||
isRead: Value(item.isRead),
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
@@ -342,10 +630,16 @@ class AppDatabase extends _$AppDatabase {
|
||||
final ids = history.map((h) => h.sentenceId).toSet();
|
||||
final rows = await (select(sentences)..where((t) => t.id.isIn(ids))).get();
|
||||
final sentenceMap = {for (final r in rows) r.id: r};
|
||||
return history.map((h) {
|
||||
final s = sentenceMap[h.sentenceId];
|
||||
return HistorySentenceWithTime(sentence: s!, readAt: h.readAt);
|
||||
}).toList()..sort((a, b) => b.readAt.compareTo(a.readAt));
|
||||
return history
|
||||
.where((h) => sentenceMap.containsKey(h.sentenceId))
|
||||
.map(
|
||||
(h) => HistorySentenceWithTime(
|
||||
sentence: sentenceMap[h.sentenceId]!,
|
||||
readAt: h.readAt,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.readAt.compareTo(a.readAt));
|
||||
}
|
||||
|
||||
// ---- 工具使用统计 ----
|
||||
@@ -548,6 +842,86 @@ class AppDatabase extends _$AppDatabase {
|
||||
Future<void> clearHanziCache() {
|
||||
return delete(hanziCaches).go();
|
||||
}
|
||||
|
||||
// ---- 学习计划 ----
|
||||
|
||||
Future<int> insertLearningPlan(LearningPlansCompanion plan) {
|
||||
return into(learningPlans).insert(plan);
|
||||
}
|
||||
|
||||
Future<List<LearningPlan>> getActiveLearningPlans() {
|
||||
return (select(learningPlans)
|
||||
..where((t) => t.isActive.equals(true))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<LearningPlan>> getAllLearningPlans() {
|
||||
return (select(
|
||||
learningPlans,
|
||||
)..orderBy([(t) => OrderingTerm.desc(t.createdAt)])).get();
|
||||
}
|
||||
|
||||
Future<LearningPlan?> getLearningPlan(int id) {
|
||||
return (select(
|
||||
learningPlans,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> updateLearningPlan(LearningPlansCompanion plan) {
|
||||
return (update(
|
||||
learningPlans,
|
||||
)..where((t) => t.id.equals(plan.id.value))).write(plan);
|
||||
}
|
||||
|
||||
Future<void> deleteLearningPlan(int id) {
|
||||
return (delete(learningPlans)..where((t) => t.id.equals(id))).go();
|
||||
}
|
||||
|
||||
// ---- 学习记录 ----
|
||||
|
||||
Future<int> insertLearningRecord(LearningRecordsCompanion record) {
|
||||
return into(learningRecords).insert(record);
|
||||
}
|
||||
|
||||
Future<List<LearningRecord>> getLearningRecords(
|
||||
int planId, {
|
||||
int limit = 50,
|
||||
}) {
|
||||
return (select(learningRecords)
|
||||
..where((t) => t.planId.equals(planId))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.completedAt)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<LearningRecord>> getTodayLearningRecords({int? planId}) {
|
||||
final today = DateTime.now();
|
||||
final startOfDay = DateTime(today.year, today.month, today.day);
|
||||
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||
var query = select(learningRecords)
|
||||
..where((t) => t.completedAt.isBiggerOrEqualValue(startOfDay))
|
||||
..where((t) => t.completedAt.isSmallerThanValue(endOfDay));
|
||||
if (planId != null) {
|
||||
query = query..where((t) => t.planId.equals(planId));
|
||||
}
|
||||
return query.get();
|
||||
}
|
||||
|
||||
Future<int> getLearningRecordCount(int planId) async {
|
||||
final rows =
|
||||
await (selectOnly(learningRecords)
|
||||
..addColumns([learningRecords.id.count()])
|
||||
..where(learningRecords.planId.equals(planId)))
|
||||
.getSingle();
|
||||
return rows.read(learningRecords.id.count()) ?? 0;
|
||||
}
|
||||
|
||||
Future<void> deleteLearningRecordsByPlan(int planId) {
|
||||
return (delete(
|
||||
learningRecords,
|
||||
)..where((t) => t.planId.equals(planId))).go();
|
||||
}
|
||||
}
|
||||
|
||||
class HistorySentenceWithTime {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user