feat: 新增壁纸图库组件和编辑器功能优化

- 新增壁纸图库相关组件(WallpaperGalleryView/WallpaperSearchBar等)
- 优化编辑器主题服务和系统UI管理
- 新增虚线边框和拖拽描边风格支持
- 完善今日诗词服务和阅读报告功能
- 修复多个UI问题和空指针异常
- 更新依赖库版本和SVG资源
- 优化交互动画和状态管理
- 补充文档和API测试脚本
This commit is contained in:
Developer
2026-05-05 05:03:33 +08:00
parent 839e118cdb
commit b5157c19f4
230 changed files with 44325 additions and 19116 deletions

View File

@@ -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);

View File

@@ -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