diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..31c0271
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..619485d
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..3072b20
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..7333bee
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2784e1e
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..5f349f7
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..505306e 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..15202b1 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..ee68795 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..ed69095 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..e7638d3 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/assets/icon/app_icon.png b/assets/icon/app_icon.png
new file mode 100644
index 0000000..a93c14e
Binary files /dev/null and b/assets/icon/app_icon.png differ
diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart
index a328f79..d7e0f87 100644
--- a/lib/pages/main_content_page.dart
+++ b/lib/pages/main_content_page.dart
@@ -5,15 +5,6 @@ import 'movie_tab_page.dart';
import 'book_tab_page.dart';
import 'note_tab_page.dart';
import 'search_page.dart';
-import 'webdav_sync_page.dart';
-import '../utils/webdav_service.dart';
-import '../utils/toast_util.dart';
-
-/// 云备份模式
-enum SyncMode {
- uploadOnly, // 上传
- downloadOnly, // 下载
-}
/// 主内容页 - 观影/阅读/笔记标签页
class MainContentPage extends StatelessWidget {
@@ -44,12 +35,6 @@ class MainContentPage extends StatelessWidget {
return AppBar(
title: Text(_getAppBarTitle(provider)),
actions: [
- // 云备份按钮
- IconButton(
- icon: const Icon(Icons.cloud_sync_outlined),
- onPressed: () => _showCloudSyncDialog(context, provider),
- tooltip: '云备份',
- ),
// 搜索按钮
IconButton(
icon: const Icon(Icons.search),
@@ -183,250 +168,6 @@ class MainContentPage extends StatelessWidget {
);
}
- /// 显示云同步对话框
- Future _showCloudSyncDialog(BuildContext context, AppProvider provider) async {
- // 检查是否已配置 WebDAV
- final config = await WebDAVService.instance.getConfig();
-
- if (!context.mounted) return;
-
- // 如果没有配置,直接跳转到配置页面
- if (config == null || config.isEmpty) {
- // 关闭云同步对话框
- if (Navigator.canPop(context)) {
- Navigator.pop(context);
- }
-
- // 等待对话框关闭完成
- await Future.delayed(const Duration(milliseconds: 100));
-
- if (!context.mounted) return;
-
- // 跳转到配置页面
- await Navigator.push(
- context,
- MaterialPageRoute(builder: (context) => const WebDAVSyncPage()),
- );
-
- // 配置页面返回后,重新检查配置
- if (!context.mounted) return;
-
- final newConfig = await WebDAVService.instance.getConfig();
- if (newConfig != null && newConfig.isNotEmpty) {
- // 配置成功,显示同步选项
- _showSyncOptionsDialog(context, provider);
- }
- return;
- }
-
- // 已配置,显示同步选项
- _showSyncOptionsDialog(context, provider);
- }
-
- /// 显示同步选项对话框
- void _showSyncOptionsDialog(BuildContext context, AppProvider provider) {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return Dialog(
- backgroundColor: Colors.transparent,
- elevation: 0,
- child: Container(
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- // 标题
- Container(
- padding: const EdgeInsets.all(20),
- decoration: const BoxDecoration(
- border: Border(
- bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
- ),
- ),
- child: const Text(
- '云备份',
- style: TextStyle(
- fontSize: 17,
- fontWeight: FontWeight.w600,
- color: Color(0xFF1A1A1A),
- ),
- ),
- ),
-
- // 同步选项
- Column(
- children: [
- _buildSyncOption(
- context,
- icon: Icons.cloud_upload,
- iconColor: const Color(0xFF1A1A1A),
- title: '上传',
- subtitle: '将本地数据备份到云端',
- onTap: () async {
- Navigator.pop(context);
- // 等待对话框关闭
- await Future.delayed(const Duration(milliseconds: 100));
- if (context.mounted) {
- _navigateToSync(context, SyncMode.uploadOnly);
- }
- },
- ),
- const Divider(height: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
- _buildSyncOption(
- context,
- icon: Icons.cloud_download,
- iconColor: const Color(0xFF1A1A1A),
- title: '下载',
- subtitle: '从云端恢复数据到本地',
- onTap: () async {
- Navigator.pop(context);
- // 等待对话框关闭
- await Future.delayed(const Duration(milliseconds: 100));
- if (context.mounted) {
- _navigateToSync(context, SyncMode.downloadOnly);
- }
- },
- ),
- ],
- ),
-
- // 取消按钮
- Container(
- decoration: const BoxDecoration(
- border: Border(
- top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
- ),
- ),
- child: InkWell(
- onTap: () => Navigator.pop(context),
- borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
- child: Container(
- padding: const EdgeInsets.all(16),
- alignment: Alignment.center,
- child: const Text(
- '取消',
- style: TextStyle(
- fontSize: 15,
- color: Color(0xFF999999),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- },
- );
- }
-
- /// 构建同步选项
- Widget _buildSyncOption(
- BuildContext context, {
- required IconData icon,
- required Color iconColor,
- required String title,
- required String subtitle,
- required VoidCallback onTap,
- }) {
- return InkWell(
- onTap: onTap,
- child: Container(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- // 图标
- Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: const Color(0xFFF5F5F5),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Icon(
- icon,
- color: const Color(0xFF1A1A1A),
- size: 20,
- ),
- ),
- const SizedBox(width: 16),
- // 文字
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- title,
- style: const TextStyle(
- fontSize: 15,
- fontWeight: FontWeight.w500,
- color: Color(0xFF1A1A1A),
- ),
- ),
- const SizedBox(height: 4),
- Text(
- subtitle,
- style: const TextStyle(
- fontSize: 13,
- color: Color(0xFF999999),
- ),
- ),
- ],
- ),
- ),
- // 箭头
- const Icon(
- Icons.chevron_right,
- color: Color(0xFFCCCCCC),
- size: 20,
- ),
- ],
- ),
- ),
- );
- }
-
- /// 执行云备份操作
- Future _navigateToSync(BuildContext context, SyncMode mode) async {
- // 执行同步
- final direction = mode == SyncMode.uploadOnly
- ? SyncDirection.upload
- : SyncDirection.download;
-
- final result = await WebDAVService.instance.syncData(direction: direction);
-
- if (!context.mounted) return;
-
- if (result.success) {
- // 如果需要重新加载数据(下载模式)
- if (result.needReload) {
- final provider = context.read();
- await provider.loadMovies();
- await provider.loadBooks();
- await provider.loadNotes();
- }
-
- ToastUtil.show(context, '${_getSyncModeText(mode)}成功');
- } else {
- ToastUtil.show(context, '${_getSyncModeText(mode)}失败: ${result.message}');
- }
- }
-
- /// 获取同步模式文本
- String _getSyncModeText(SyncMode mode) {
- switch (mode) {
- case SyncMode.uploadOnly:
- return '上传';
- case SyncMode.downloadOnly:
- return '下载';
- }
- }
-
/// 显示添加对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
showModalBottomSheet(
diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart
index 8f7604b..18998cb 100644
--- a/lib/pages/profile_page.dart
+++ b/lib/pages/profile_page.dart
@@ -4,6 +4,8 @@ import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:provider/provider.dart';
+// import 'package:url_launcher/url_launcher.dart'; // 改为应用内打开
+import 'package:webview_flutter/webview_flutter.dart';
import '../providers/app_provider.dart';
import '../utils/user_prefs.dart';
import '../utils/toast_util.dart';
@@ -593,6 +595,176 @@ class _ProfilePageState extends State {
/// 显示设置
void _showSettings(BuildContext context) {
- _showToast('设置功能开发中');
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const SettingsPage(),
+ ),
+ );
+ }
+}
+
+/// 设置页面
+class SettingsPage extends StatelessWidget {
+ const SettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ title: const Text('设置'),
+ ),
+ body: ListView(
+ children: [
+ // 使用说明
+ _buildLinkItem(
+ context: context,
+ icon: Icons.help_outline,
+ title: '使用说明',
+ subtitle: '查看应用使用指南',
+ url: 'https://www.iletter.top/',
+ ),
+
+ const Divider(height: 0.5, indent: 24, endIndent: 24),
+
+ // 关于作者
+ _buildLinkItem(
+ context: context,
+ icon: Icons.person_outline,
+ title: '关于作者',
+ subtitle: '了解更多信息',
+ url: 'https://www.iletter.top/',
+ ),
+
+ const Divider(height: 0.5, indent: 24, endIndent: 24),
+ ],
+ ),
+ );
+ }
+
+ /// 构建链接项
+ Widget _buildLinkItem({
+ required BuildContext context,
+ required IconData icon,
+ required String title,
+ required String subtitle,
+ required String url,
+ }) {
+ return ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
+ leading: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF5F5F5),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(
+ icon,
+ color: const Color(0xFF666666),
+ size: 20,
+ ),
+ ),
+ title: Text(
+ title,
+ style: const TextStyle(
+ fontSize: 15,
+ color: Color(0xFF1A1A1A),
+ ),
+ ),
+ subtitle: Text(
+ subtitle,
+ style: const TextStyle(
+ fontSize: 12,
+ color: Color(0xFF999999),
+ ),
+ ),
+ trailing: const Icon(
+ Icons.open_in_new,
+ color: Color(0xFFCCCCCC),
+ size: 18,
+ ),
+ onTap: () => _launchUrl(context, url),
+ );
+ }
+
+ /// 打开链接(应用内打开)
+ void _launchUrl(BuildContext context, String url) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => WebViewPage(url: url),
+ ),
+ );
+ }
+}
+
+/// WebView 页面
+class WebViewPage extends StatefulWidget {
+ final String url;
+
+ const WebViewPage({super.key, required this.url});
+
+ @override
+ State createState() => _WebViewPageState();
+}
+
+class _WebViewPageState extends State {
+ late final WebViewController _controller;
+ bool _isLoading = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(
+ NavigationDelegate(
+ onPageStarted: (String url) {
+ setState(() {
+ _isLoading = true;
+ });
+ },
+ onPageFinished: (String url) {
+ setState(() {
+ _isLoading = false;
+ });
+ },
+ onWebResourceError: (WebResourceError error) {
+ setState(() {
+ _isLoading = false;
+ });
+ },
+ ),
+ )
+ ..loadRequest(Uri.parse(widget.url));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ title: const Text(''),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.refresh),
+ onPressed: () => _controller.reload(),
+ ),
+ ],
+ ),
+ body: Stack(
+ children: [
+ WebViewWidget(controller: _controller),
+ if (_isLoading)
+ const Center(
+ child: CircularProgressIndicator(
+ color: Color(0xFF999999),
+ ),
+ ),
+ ],
+ ),
+ );
}
}
diff --git a/lib/pages/statistics_page.dart b/lib/pages/statistics_page.dart
index 23f8147..06878e7 100644
--- a/lib/pages/statistics_page.dart
+++ b/lib/pages/statistics_page.dart
@@ -270,11 +270,11 @@ class _StatisticsPageState extends State with SingleTickerProvid
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- _buildLegendItem('影视', const Color(0xFF1A1A1A)),
+ _buildLegendItem('影视', const Color(0xFFE53935)),
const SizedBox(width: 24),
- _buildLegendItem('书籍', const Color(0xFF666666)),
+ _buildLegendItem('书籍', const Color(0xFF1E88E5)),
const SizedBox(width: 24),
- _buildLegendItem('笔记', const Color(0xFF999999)),
+ _buildLegendItem('笔记', const Color(0xFF43A047)),
],
),
const SizedBox(height: 32),
@@ -590,35 +590,44 @@ class _StatisticsPageState extends State with SingleTickerProvid
),
borderData: FlBorderData(show: false),
lineBarsData: [
- // 影视折线
+ // 影视折线 - 红色
LineChartBarData(
spots: spotsMovie,
isCurved: true,
- color: const Color(0xFF1A1A1A),
- barWidth: 2,
+ color: const Color(0xFFE53935),
+ barWidth: 2.5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
- belowBarData: BarAreaData(show: false),
+ belowBarData: BarAreaData(
+ show: true,
+ color: const Color(0xFFE53935).withOpacity(0.1),
+ ),
),
- // 书籍折线
+ // 书籍折线 - 蓝色
LineChartBarData(
spots: spotsBook,
isCurved: true,
- color: const Color(0xFF666666),
- barWidth: 2,
+ color: const Color(0xFF1E88E5),
+ barWidth: 2.5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
- belowBarData: BarAreaData(show: false),
+ belowBarData: BarAreaData(
+ show: true,
+ color: const Color(0xFF1E88E5).withOpacity(0.1),
+ ),
),
- // 笔记折线
+ // 笔记折线 - 绿色
LineChartBarData(
spots: spotsNote,
isCurved: true,
- color: const Color(0xFF999999),
- barWidth: 2,
+ color: const Color(0xFF43A047),
+ barWidth: 2.5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
- belowBarData: BarAreaData(show: false),
+ belowBarData: BarAreaData(
+ show: true,
+ color: const Color(0xFF43A047).withOpacity(0.1),
+ ),
),
],
),
@@ -654,11 +663,11 @@ class _StatisticsPageState extends State with SingleTickerProvid
),
child: Column(
children: [
- _buildWeeklyRow('影视新增', totalMovies, const Color(0xFF1A1A1A)),
+ _buildWeeklyRow('影视新增', totalMovies, const Color(0xFFE53935)),
const Divider(height: 24),
- _buildWeeklyRow('书籍新增', totalBooks, const Color(0xFF666666)),
+ _buildWeeklyRow('书籍新增', totalBooks, const Color(0xFF1E88E5)),
const Divider(height: 24),
- _buildWeeklyRow('笔记新增', totalNotes, const Color(0xFF999999)),
+ _buildWeeklyRow('笔记新增', totalNotes, const Color(0xFF43A047)),
const Divider(height: 24),
_buildWeeklyRow('总计', totalMovies + totalBooks + totalNotes, const Color(0xFF1A1A1A), isTotal: true),
],
diff --git a/lib/utils/webdav_service.dart b/lib/utils/webdav_service.dart
index 14758c5..cbcb95d 100644
--- a/lib/utils/webdav_service.dart
+++ b/lib/utils/webdav_service.dart
@@ -127,7 +127,7 @@ class WebDAVService {
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
var davUrl = '$baseUrl$path';
- print('WebDAV: Testing connection to $davUrl');
+ // print('WebDAV: Testing connection to $davUrl');
final client = http.Client();
try {
@@ -142,7 +142,7 @@ class WebDAVService {
''';
var propfindResponse = await client.send(propfindRequest);
- print('WebDAV: PROPFIND status ${propfindResponse.statusCode}');
+ // print('WebDAV: PROPFIND status ${propfindResponse.statusCode}');
if (propfindResponse.statusCode == 301 ||
propfindResponse.statusCode == 302 ||
@@ -169,12 +169,12 @@ class WebDAVService {
} else if (propfindResponse.statusCode == 401) {
return {'success': false, 'message': '认证失败,请检查用户名和密码'};
} else if (propfindResponse.statusCode == 404) {
- print('WebDAV: Directory not found, trying to create...');
+ // print('WebDAV: Directory not found, trying to create...');
} else {
return {'success': false, 'message': '服务器返回错误: ${propfindResponse.statusCode}'};
}
} catch (e) {
- print('WebDAV: PROPFIND error: $e');
+ // print('WebDAV: PROPFIND error: $e');
}
try {
@@ -182,7 +182,7 @@ class WebDAVService {
mkcolRequest.headers['Authorization'] = _basicAuth(username, password);
final mkcolResponse = await client.send(mkcolRequest);
- print('WebDAV: MKCOL status ${mkcolResponse.statusCode}');
+ // print('WebDAV: MKCOL status ${mkcolResponse.statusCode}');
if (mkcolResponse.statusCode == 201) {
return {'success': true, 'message': '连接成功,已创建目录'};
@@ -196,18 +196,18 @@ class WebDAVService {
return {'success': false, 'message': '创建目录失败: ${mkcolResponse.statusCode}'};
}
} catch (e) {
- print('WebDAV: MKCOL error: $e');
+ // print('WebDAV: MKCOL error: $e');
return {'success': false, 'message': '连接失败: $e'};
} finally {
client.close();
}
} catch (e) {
- print('WebDAV test connection error: $e');
+ // print('WebDAV test connection error: $e');
return {'success': false, 'message': '连接失败: $e'};
}
}
- /// 同步数据(保留原有方法用于手动同步)
+ /// 同步数据(使用 ZIP 格式,类似自动备份)
Future syncData({SyncDirection direction = SyncDirection.bidirectional}) async {
final config = await getConfig();
if (config == null) {
@@ -228,8 +228,7 @@ class WebDAVService {
}
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
- var davUrl = '$baseUrl$path/mooknote.db';
- final davImagesUrl = '$baseUrl$path/images';
+ final zipUrl = '$baseUrl$path/mooknote_backup.zip';
final client = http.Client();
int uploadedFiles = 0;
@@ -238,79 +237,91 @@ class WebDAVService {
int downloadedImages = 0;
try {
- final remoteDbInfo = await _getRemoteFileInfo(client, davUrl, username, password);
-
if (direction == SyncDirection.upload) {
- print('WebDAV: Upload only mode');
- final result = await _uploadFile(client, davUrl, username, password, dbFile);
+ // print('WebDAV: Upload ZIP mode');
+ // 创建 ZIP 备份
+ final zipBytes = await _createFullBackupZip(dbFile);
+ if (zipBytes == null) {
+ return SyncResult(success: false, message: '创建备份 ZIP 失败');
+ }
+
+ // 上传 ZIP 文件
+ final result = await _uploadBytes(client, zipUrl, username, password, zipBytes);
if (result) {
- uploadedFiles++;
- final imageResult = await _syncImages(client, davImagesUrl, username, password, SyncDirection.upload);
- uploadedImages = imageResult.uploaded;
+ uploadedFiles = 1;
+ // 统计图片数量
+ final appDir = await getApplicationDocumentsDirectory();
+ final imagesDir = Directory('${appDir.path}/images');
+ if (await imagesDir.exists()) {
+ uploadedImages = await _countImagesInDir(imagesDir);
+ }
+ // print('WebDAV: ZIP uploaded successfully, images: $uploadedImages');
+ } else {
+ return SyncResult(success: false, message: '上传 ZIP 失败');
}
} else if (direction == SyncDirection.download) {
- print('WebDAV: Download only mode');
- if (remoteDbInfo != null) {
- final result = await _downloadFile(client, davUrl, username, password, dbFile);
- if (result) {
- downloadedFiles++;
- // 重新打开数据库以应用新数据
+ // print('WebDAV: Download ZIP mode');
+ // 下载 ZIP 文件
+ final zipFile = File('${dbFile.parent.path}/mooknote_backup_download.zip');
+ final result = await _downloadFile(client, zipUrl, username, password, zipFile);
+
+ if (result && await zipFile.exists()) {
+ downloadedFiles = 1;
+ // 解压 ZIP 文件
+ final extractResult = await _extractBackupZip(zipFile, dbFile);
+ if (extractResult) {
+ // 重新打开数据库
await DatabaseHelper.instance.reopenDatabase();
- final imageResult = await _syncImages(client, davImagesUrl, username, password, SyncDirection.download);
- downloadedImages = imageResult.downloaded;
+ // 统计下载的图片数量
+ final appDir = await getApplicationDocumentsDirectory();
+ final imagesDir = Directory('${appDir.path}/images');
+ if (await imagesDir.exists()) {
+ downloadedImages = await _countImagesInDir(imagesDir);
+ }
+ // print('WebDAV: ZIP downloaded and extracted successfully, images: $downloadedImages');
+ } else {
+ return SyncResult(success: false, message: '解压 ZIP 失败');
}
+ // 删除临时 ZIP 文件
+ await zipFile.delete();
} else {
- return SyncResult(success: false, message: '远程数据库不存在');
+ return SyncResult(success: false, message: '远程备份不存在或下载失败');
}
} else if (direction == SyncDirection.bidirectional) {
// 双向同步:比较时间戳决定上传还是下载
- print('WebDAV: Bidirectional sync mode');
+ // print('WebDAV: Bidirectional sync mode (ZIP)');
+ final remoteZipInfo = await _getRemoteFileInfo(client, zipUrl, username, password);
- if (remoteDbInfo == null) {
+ if (remoteZipInfo == null) {
// 远程不存在,直接上传
- print('WebDAV: Remote DB not found, uploading...');
- final result = await _uploadFile(client, davUrl, username, password, dbFile);
- if (result) {
- uploadedFiles++;
- final imageResult = await _syncImages(client, davImagesUrl, username, password, SyncDirection.upload);
- uploadedImages = imageResult.uploaded;
- }
+ // print('WebDAV: Remote ZIP not found, uploading...');
+ return await syncData(direction: SyncDirection.upload);
} else {
// 远程存在,比较修改时间
final localModified = await dbFile.lastModified();
- final remoteModified = remoteDbInfo['modified'] as DateTime;
+ final remoteModified = remoteZipInfo['modified'] as DateTime;
- print('WebDAV: Local modified: $localModified');
- print('WebDAV: Remote modified: $remoteModified');
+ // print('WebDAV: Local modified: $localModified');
+ // print('WebDAV: Remote modified: $remoteModified');
final timeDiff = localModified.difference(remoteModified).inSeconds;
if (timeDiff > 10) {
// 本地较新,上传
- print('WebDAV: Local is newer, uploading...');
- final result = await _uploadFile(client, davUrl, username, password, dbFile);
- if (result) {
- uploadedFiles++;
- final imageResult = await _syncImages(client, davImagesUrl, username, password, SyncDirection.upload);
- uploadedImages = imageResult.uploaded;
- }
+ // print('WebDAV: Local is newer, uploading...');
+ return await syncData(direction: SyncDirection.upload);
} else if (timeDiff < -10) {
// 远程较新,下载
- print('WebDAV: Remote is newer, downloading...');
- final result = await _downloadFile(client, davUrl, username, password, dbFile);
- if (result) {
- downloadedFiles++;
- // 重新打开数据库以应用新数据
- await DatabaseHelper.instance.reopenDatabase();
- final imageResult = await _syncImages(client, davImagesUrl, username, password, SyncDirection.download);
- downloadedImages = imageResult.downloaded;
- }
+ // print('WebDAV: Remote is newer, downloading...');
+ return await syncData(direction: SyncDirection.download);
} else {
- // 时间相近,仅同步图片
- print('WebDAV: Local and remote are similar, syncing images only...');
- final imageResult = await _syncImages(client, davImagesUrl, username, password, SyncDirection.bidirectional);
- uploadedImages = imageResult.uploaded;
- downloadedImages = imageResult.downloaded;
+ // 时间相近,无需同步
+ // print('WebDAV: Local and remote are similar, no sync needed');
+ return SyncResult(
+ success: true,
+ message: '本地和远程数据相同,无需同步',
+ lastSyncTime: DateTime.now(),
+ );
}
}
}
@@ -334,11 +345,62 @@ class WebDAVService {
client.close();
}
} catch (e) {
- print('WebDAV sync error: $e');
+ // print('WebDAV sync error: $e');
return SyncResult(success: false, message: '同步失败: $e');
}
}
+ /// 统计目录中的图片数量
+ Future _countImagesInDir(Directory dir) async {
+ int count = 0;
+ await for (final entity in dir.list(recursive: true)) {
+ if (entity is File) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /// 解压备份 ZIP 文件
+ Future _extractBackupZip(File zipFile, File dbFile) async {
+ try {
+ final bytes = await zipFile.readAsBytes();
+ final archive = ZipDecoder().decodeBytes(bytes);
+
+ // 解压数据库文件
+ final dbArchiveFile = archive.findFile('mooknote.db');
+ if (dbArchiveFile != null) {
+ await dbFile.writeAsBytes(dbArchiveFile.content as List);
+ // print('WebDAV: Extracted database file');
+ }
+
+ // 解压图片文件
+ final appDir = await getApplicationDocumentsDirectory();
+ final imagesDir = Directory('${appDir.path}/images');
+
+ int imageCount = 0;
+ for (final archiveFile in archive) {
+ if (archiveFile.name.startsWith('images/')) {
+ final relativePath = archiveFile.name.substring(7); // 去掉 'images/' 前缀
+ final localFile = File('${imagesDir.path}/$relativePath');
+
+ // 确保父目录存在
+ await localFile.parent.create(recursive: true);
+
+ // 写入文件
+ await localFile.writeAsBytes(archiveFile.content as List);
+ imageCount++;
+ }
+ }
+ // print('WebDAV: Extracted $imageCount images');
+
+ return true;
+ } catch (e) {
+ // print('WebDAV: Extract ZIP error: $e');
+ return false;
+ }
+ }
+
/// 获取远程文件信息
Future