优化webdav恢复

This commit is contained in:
DelLevin-Home
2026-05-22 22:42:54 +08:00
parent 6c3e55fead
commit af2ac16521
7 changed files with 348 additions and 883 deletions

View File

@@ -3,7 +3,8 @@
"allow": [
"Bash(flutter analyze *)",
"Bash(python _fix_script.py)",
"Bash(dart analyze *)"
"Bash(dart analyze *)",
"Bash(dart run *)"
]
}
}

View File

@@ -264,6 +264,14 @@ class _MainContentPageState extends State<MainContentPage> {
Navigator.pop(context);
}
// 下载了数据则刷新界面
if (result.success && result.needReload && context.mounted) {
final provider = context.read<AppProvider>();
await provider.loadMovies();
await provider.loadBooks();
await provider.loadNotes();
}
// 显示结果
if (context.mounted) {
final isSuccess = result.success;

View File

@@ -233,76 +233,25 @@ class _ProfilePageState extends State<ProfilePage> {
final notes = provider.notes;
final movieCount = movies.where((m) => !m.isDeleted).length;
final watchedCount = movies.where((m) => m.status == 'watched' && !m.isDeleted).length;
final watchingCount = movies.where((m) => m.status == 'watching' && !m.isDeleted).length;
final wantToWatchCount = movies.where((m) => m.status == 'want_to_watch' && !m.isDeleted).length;
final bookCount = books.length;
final readCount = books.where((b) => b.status == 'read').length;
final readingCount = books.where((b) => b.status == 'reading').length;
final wantToReadCount = books.where((b) => b.status == 'want_to_read').length;
final noteCount = notes.length;
final movieRatings = movies
.where((m) => m.rating != null && !m.isDeleted)
.map((m) => m.rating!);
final avgMovieRating = movieRatings.isNotEmpty
? movieRatings.reduce((a, b) => a + b) / movieRatings.length
: 0.0;
final bookRatings = books
.where((b) => b.rating != null)
.map((b) => b.rating!);
final avgBookRating = bookRatings.isNotEmpty
? bookRatings.reduce((a, b) => a + b) / bookRatings.length
: 0.0;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 主统计卡片
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('观影', movieCount, '', Icons.movie_outlined),
_buildStatCard('阅读', bookCount, '', Icons.menu_book_outlined),
_buildStatCard('笔记', noteCount, '', Icons.note_outlined),
],
),
),
const SizedBox(height: 24),
// 详情统计
_buildSectionTitle('观影详情'),
const SizedBox(height: 12),
_buildDetailGrid([
_buildDetailItem('已看', watchedCount, Icons.check_circle_outline),
_buildDetailItem('在看', watchingCount, Icons.play_circle_outline),
_buildDetailItem('想看', wantToWatchCount, Icons.bookmark_border),
_buildDetailItem('均分', avgMovieRating > 0 ? avgMovieRating.toStringAsFixed(1) : '-', Icons.star_outline),
]),
const SizedBox(height: 20),
_buildSectionTitle('阅读详情'),
const SizedBox(height: 12),
_buildDetailGrid([
_buildDetailItem('已读', readCount, Icons.check_circle_outline),
_buildDetailItem('在读', readingCount, Icons.play_circle_outline),
_buildDetailItem('想读', wantToReadCount, Icons.bookmark_border),
_buildDetailItem('均分', avgBookRating > 0 ? avgBookRating.toStringAsFixed(1) : '-', Icons.star_outline),
]),
],
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('观影', movieCount, '', Icons.movie_outlined),
_buildStatCard('阅读', bookCount, '', Icons.menu_book_outlined),
_buildStatCard('笔记', noteCount, '', Icons.note_outlined),
],
),
),
);
},
@@ -354,63 +303,6 @@ class _ProfilePageState extends State<ProfilePage> {
);
}
/// 详情网格
Widget _buildDetailGrid(List<Widget> children) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
),
);
}
/// 详情项
Widget _buildDetailItem(String label, dynamic value, IconData icon) {
return Column(
children: [
Icon(
icon,
size: 18,
color: const Color(0xFF999999),
),
const SizedBox(height: 6),
Text(
'$value',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF999999),
),
),
],
);
}
/// 区块标题
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
),
);
}
/// 功能菜单
Widget _buildMenuSection() {
return Container(

View File

@@ -96,6 +96,16 @@ class _StatisticsPageState extends State<StatisticsPage> {
final readingBooks = books.where((b) => b.status == 'reading').length;
final wantToReadBooks = books.where((b) => b.status == 'want_to_read').length;
final movieRatings = movies.where((m) => m.rating != null && !m.isDeleted).map((m) => m.rating!);
final avgMovieRating = movieRatings.isNotEmpty
? movieRatings.reduce((a, b) => a + b) / movieRatings.length
: null;
final bookRatings = books.where((b) => b.rating != null).map((b) => b.rating!);
final avgBookRating = bookRatings.isNotEmpty
? bookRatings.reduce((a, b) => a + b) / bookRatings.length
: null;
final children = <Widget>[];
// 数据总览
@@ -106,25 +116,25 @@ class _StatisticsPageState extends State<StatisticsPage> {
// 影视状态分布
if (_showMovies) {
children.add(_buildSectionTitle('影视状态分布'));
children.add(_buildSectionTitle('影视'));
children.add(const SizedBox(height: 16));
children.add(_buildStatusDistribution([
_StatusData('已看', watchedMovies, const Color(0xFF1A1A1A)),
_StatusData('在看', watchingMovies, const Color(0xFF666666)),
_StatusData('想看', wantToWatchMovies, const Color(0xFF999999)),
]));
], avgRating: avgMovieRating));
children.add(const SizedBox(height: 32));
}
// 书籍状态分布
if (_showBooks) {
children.add(_buildSectionTitle('书籍状态分布'));
children.add(_buildSectionTitle('书籍'));
children.add(const SizedBox(height: 16));
children.add(_buildStatusDistribution([
_StatusData('已读', readBooks, const Color(0xFF1A1A1A)),
_StatusData('在读', readingBooks, const Color(0xFF666666)),
_StatusData('想读', wantToReadBooks, const Color(0xFF999999)),
]));
], avgRating: avgBookRating));
children.add(const SizedBox(height: 32));
}
@@ -295,7 +305,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
);
}
Widget _buildStatusDistribution(List<_StatusData> data) {
Widget _buildStatusDistribution(List<_StatusData> data, {double? avgRating}) {
final total = data.fold(0, (sum, item) => sum + item.count);
return Container(
@@ -305,26 +315,41 @@ class _StatisticsPageState extends State<StatisticsPage> {
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: data.map((item) {
final percentage = total > 0 ? (item.count / total * 100).toStringAsFixed(1) : '0';
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
...data.map((item) {
final percentage = total > 0 ? (item.count / total * 100).toStringAsFixed(1) : '0';
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
width: 10, height: 10,
decoration: BoxDecoration(color: item.color, borderRadius: BorderRadius.circular(2)),
),
const SizedBox(width: 12),
Text(item.label, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const Spacer(),
Text('${item.count}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(width: 8),
Text('($percentage%)', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))),
],
),
);
}),
if (avgRating != null) ...[
const Divider(height: 1, color: Color(0xFFE8E8E8)),
const SizedBox(height: 12),
Row(
children: [
Container(
width: 10, height: 10,
decoration: BoxDecoration(color: item.color, borderRadius: BorderRadius.circular(2)),
),
const SizedBox(width: 12),
Text(item.label, style: const TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const Icon(Icons.star, size: 16, color: Color(0xFF999999)),
const SizedBox(width: 10),
const Text('平均评分', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const Spacer(),
Text('${item.count}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(width: 8),
Text('($percentage%)', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))),
Text(avgRating.toStringAsFixed(1), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
],
),
);
}).toList(),
],
],
),
);
}

View File

@@ -524,6 +524,144 @@ class BackupService {
}
}
/// 从 ZIP 字节数据恢复(供 WebDAV 同步等场景使用)
Future<ImportResult> restoreFromZipBytes(Uint8List zipBytes) async {
try {
final archive = ZipDecoder().decodeBytes(zipBytes);
// 查找 data.json
final dataFile = archive.findFile('data.json');
if (dataFile == null) {
return ImportResult.error('备份文件中没有找到数据文件');
}
final jsonString = utf8.decode(dataFile.content as List<int>);
final backupData = jsonDecode(jsonString) as Map<String, dynamic>;
// 记录图片文件名到新路径的映射
final imagePathMap = <String, String>{};
int imageCount = 0;
// 解压图片到应用目录,保持目录结构
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory(path.join(appDir.path, 'images'));
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
for (final archiveFile in archive) {
if (archiveFile.name.startsWith('images/')) {
final relativePath = archiveFile.name.substring(7); // 去掉 'images/' 前缀
final outputFile = File(path.join(imagesDir.path, relativePath));
final parentDir = outputFile.parent;
if (!await parentDir.exists()) {
await parentDir.create(recursive: true);
}
await outputFile.writeAsBytes(archiveFile.content as List<int>);
final fileName = path.basename(archiveFile.name);
imagePathMap[fileName] = outputFile.path;
imageCount++;
}
}
// 验证备份格式
if (!backupData.containsKey('data')) {
return ImportResult.error('无效的备份文件格式');
}
// 导入数据
final data = backupData['data'] as Map<String, dynamic>;
final db = await DatabaseHelper.instance.database;
await db.transaction((txn) async {
// 清空现有数据
await txn.delete('movie_reviews');
await txn.delete('movie_posters');
await txn.delete('movies');
await txn.delete('books');
await txn.delete('notes');
if (data.containsKey('movies')) {
final movies = data['movies'] as List<dynamic>;
for (final movie in movies) {
final movieMap = _convertToDbMap(movie);
final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap);
await txn.insert('movies', updatedMap);
}
}
if (data.containsKey('books')) {
final books = data['books'] as List<dynamic>;
for (final book in books) {
final bookMap = _convertToDbMap(book);
final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap);
await txn.insert('books', updatedMap);
}
}
if (data.containsKey('notes')) {
final notes = data['notes'] as List<dynamic>;
for (final note in notes) {
final noteMap = _convertToDbMap(note);
final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap);
await txn.insert('notes', updatedMap);
}
}
if (data.containsKey('movie_reviews')) {
final reviews = data['movie_reviews'] as List<dynamic>;
for (final review in reviews) {
await txn.insert('movie_reviews', _convertToDbMap(review));
}
}
if (data.containsKey('movie_posters')) {
final posters = data['movie_posters'] as List<dynamic>;
for (final poster in posters) {
final posterMap = _convertToDbMap(poster);
final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap);
await txn.insert('movie_posters', updatedMap);
}
}
});
// 恢复用户个人信息
if (backupData.containsKey('userInfo')) {
final userInfo = backupData['userInfo'] as Map<String, dynamic>;
final userPrefs = UserPrefs();
if (userInfo.containsKey('nickname')) {
await userPrefs.setNickname(userInfo['nickname'] as String);
}
if (userInfo.containsKey('motto')) {
await userPrefs.setMotto(userInfo['motto'] as String);
}
if (userInfo.containsKey('avatarPath')) {
final avatarPath = userInfo['avatarPath'] as String?;
if (avatarPath != null && avatarPath.isNotEmpty) {
final fileName = path.basename(avatarPath);
if (imagePathMap.containsKey(fileName)) {
await userPrefs.setAvatarPath(imagePathMap[fileName]!);
}
}
}
}
// 统计导入数量
final stats = <String, int>{};
if (data.containsKey('movies')) stats['影视'] = (data['movies'] as List).length;
if (data.containsKey('books')) stats['书籍'] = (data['books'] as List).length;
if (data.containsKey('notes')) stats['笔记'] = (data['notes'] as List).length;
if (data.containsKey('movie_reviews')) stats['影评'] = (data['movie_reviews'] as List).length;
if (data.containsKey('movie_posters')) stats['海报'] = (data['movie_posters'] as List).length;
if (imageCount > 0) stats['图片'] = imageCount;
return ImportResult.success(stats);
} catch (e) {
return ImportResult.error('恢复失败: $e');
}
}
/// 将动态类型转换为数据库可用的 Map
Map<String, dynamic> _convertToDbMap(dynamic item) {
if (item is Map<String, dynamic>) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
name: mooknote
description: "app for tracking movies, books, and notes"
publish_to: 'none'
version: 0.1.7+2
version: 0.1.8
environment:
sdk: ^3.5.0