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?> _getRemoteFileInfo( http.Client client, @@ -378,7 +440,7 @@ class WebDAVService { } return null; } catch (e) { - print('WebDAV: Get remote file info error: $e'); + // print('WebDAV: Get remote file info error: $e'); return null; } } @@ -403,7 +465,7 @@ class WebDAVService { } }); - print('WebDAV: 自动备份已启动,每5分钟执行一次'); + // print('WebDAV: 自动备份已启动,每5分钟执行一次'); } /// 停止自动同步 @@ -415,7 +477,7 @@ class WebDAVService { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_autoSyncKey, false); - print('WebDAV: 自动备份已停止'); + // print('WebDAV: 自动备份已停止'); } /// 检查自动同步状态 @@ -456,7 +518,7 @@ class WebDAVService { final davUrl = '$baseUrl$basePath/$backupFileName'; final davImagesUrl = '$baseUrl$basePath/images'; - print('WebDAV: 开始定时备份到 $davUrl'); + // print('WebDAV: 开始定时备份到 $davUrl'); final client = http.Client(); int uploadedImages = 0; @@ -485,7 +547,7 @@ class WebDAVService { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_lastSyncKey, DateTime.now().toIso8601String()); - print('WebDAV: 定时备份完成 - $backupFileName'); + // print('WebDAV: 定时备份完成 - $backupFileName'); return SyncResult( success: true, @@ -499,7 +561,7 @@ class WebDAVService { client.close(); } } catch (e) { - print('WebDAV: 定时备份错误: $e'); + // print('WebDAV: 定时备份错误: $e'); return SyncResult(success: false, message: '备份失败: $e'); } } @@ -538,7 +600,7 @@ class WebDAVService { final zipEncoder = ZipEncoder(); return zipEncoder.encode(archive); } catch (e) { - print('WebDAV: 创建备份 ZIP 失败: $e'); + // print('WebDAV: 创建备份 ZIP 失败: $e'); return null; } } @@ -586,15 +648,15 @@ class WebDAVService { final oldBackup = backupList.removeAt(0); final deleteUrl = '$baseUrl$basePath/$oldBackup'; await _deleteFile(client, deleteUrl, username, password); - print('WebDAV: 删除旧备份 $oldBackup'); + // print('WebDAV: 删除旧备份 $oldBackup'); } // 保存更新后的列表 await prefs.setString(_backupListKey, jsonEncode(backupList)); - print('WebDAV: 备份列表已更新,当前 ${backupList.length} 个备份'); + // print('WebDAV: 备份列表已更新,当前 ${backupList.length} 个备份'); } catch (e) { - print('WebDAV: 更新备份列表失败: $e'); + // print('WebDAV: 更新备份列表失败: $e'); } } @@ -622,7 +684,7 @@ class WebDAVService { } } } catch (e) { - print('WebDAV: 删除文件失败: $e'); + // print('WebDAV: 删除文件失败: $e'); } } @@ -657,7 +719,7 @@ class WebDAVService { return response.statusCode == 201 || response.statusCode == 204; } catch (e) { - print('WebDAV: 上传失败: $e'); + // print('WebDAV: 上传失败: $e'); return false; } } @@ -696,18 +758,21 @@ class WebDAVService { final localImages = {}; // 相对路径 -> 文件 await _collectLocalImages(localImagesDir, localImages, ''); - print('WebDAV: Local images: ${localImages.length}'); + // print('WebDAV: Local images: ${localImages.length}'); // 递归获取远程所有图片文件 final remoteImages = await _listRemoteImagesRecursive(client, imagesUrl, username, password, ''); - print('WebDAV: Remote images: ${remoteImages.length}'); + // print('WebDAV: Remote images: ${remoteImages.length}'); if (direction == SyncDirection.upload) { // 仅上传:上传所有本地图片 + // print('WebDAV: Starting upload of ${localImages.length} images...'); for (final entry in localImages.entries) { final relativePath = entry.key; final remoteUrl = '$imagesUrl/$relativePath'; + // print('WebDAV: Uploading $relativePath...'); + // 确保远程父目录存在 final parentPath = p.dirname(relativePath); if (parentPath != '.' && parentPath.isNotEmpty) { @@ -716,21 +781,33 @@ class WebDAVService { } final success = await _uploadFile(client, remoteUrl, username, password, entry.value); - if (success) uploaded++; + if (success) { + uploaded++; + // print('WebDAV: Uploaded $relativePath ($uploaded/${localImages.length})'); + } } + // print('WebDAV: Upload complete - $uploaded/${localImages.length} images uploaded'); } else if (direction == SyncDirection.download) { // 仅下载:下载所有远程图片 + // print('WebDAV: Starting download of ${remoteImages.length} images...'); for (final relativePath in remoteImages) { final remoteUrl = '$imagesUrl/$relativePath'; final localFile = File('${localImagesDir.path}/$relativePath'); + + // print('WebDAV: Downloading $relativePath...'); + // 确保父目录存在 await localFile.parent.create(recursive: true); final success = await _downloadFile(client, remoteUrl, username, password, localFile); - if (success) downloaded++; + if (success) { + downloaded++; + // print('WebDAV: Downloaded $relativePath ($downloaded/${remoteImages.length})'); + } } + // print('WebDAV: Download complete - $downloaded/${remoteImages.length} images downloaded'); } } catch (e) { - print('WebDAV: Sync images error: $e'); + // print('WebDAV: Sync images error: $e'); } return _ImageSyncResult(uploaded: uploaded, downloaded: downloaded); @@ -790,10 +867,15 @@ class WebDAVService { if (response.statusCode == 207) { final body = await response.stream.bytesToString(); + // print('WebDAV: PROPFIND response for $relativePath: ${body.length} bytes'); + // print('WebDAV: Response body: $body'); + // 解析响应,提取文件和目录 final hrefMatches = RegExp(r'([^<]+)', caseSensitive: false) .allMatches(body); + // print('WebDAV: Found ${hrefMatches.length} href entries'); + for (final match in hrefMatches) { final href = match.group(1)!; final name = p.basename(href); @@ -802,11 +884,23 @@ class WebDAVService { if (name.isEmpty) continue; if (relativePath.isEmpty && name == 'images') continue; - // 检查是文件还是目录 - final isDirectory = body.substring( - match.start, - match.end + 200 < body.length ? match.end + 200 : body.length, - ).contains(' 部分(Apache 使用大写 D) + final responseStart = body.lastIndexOf('', match.start); + final responseEnd = body.indexOf('', match.start); + bool isDirectory = false; + + if (responseStart != -1 && responseEnd != -1 && responseStart < responseEnd) { + final responseSection = body.substring(responseStart, responseEnd); + // print('WebDAV: Checking $name in section: ${responseSection.substring(0, responseSection.length > 300 ? 300 : responseSection.length)}'); + // 检查是否包含 标签(Apache WebDAV 使用大写 D) + isDirectory = responseSection.contains('') || + responseSection.contains('') || + responseSection.contains('') || + responseSection.contains(''); + // print('WebDAV: $name contains : ${responseSection.contains('')}'); + } + + // print('WebDAV: Found $name - isDirectory: $isDirectory'); if (isDirectory) { // 递归获取子目录中的图片 @@ -818,12 +912,15 @@ class WebDAVService { } else { // 是文件,添加到列表 final filePath = relativePath.isEmpty ? name : '$relativePath/$name'; + // print('WebDAV: Adding file to list: $filePath'); images.add(filePath); } } + } else { + // print('WebDAV: PROPFIND failed with status ${response.statusCode} for $relativePath'); } } catch (e) { - print('WebDAV: List remote images error: $e'); + // print('WebDAV: List remote images error: $e'); } return images; @@ -866,7 +963,7 @@ class WebDAVService { } } } catch (e) { - print('WebDAV: 创建目录失败: $e'); + // print('WebDAV: 创建目录失败: $e'); } } @@ -903,7 +1000,7 @@ class WebDAVService { return response.statusCode == 201 || response.statusCode == 204; } catch (e) { - print('WebDAV: Upload error: $e'); + // print('WebDAV: Upload error: $e'); return false; } } @@ -917,6 +1014,8 @@ class WebDAVService { File localFile, ) async { try { + // print('WebDAV: Downloading from $url to ${localFile.path}'); + var request = http.Request('GET', Uri.parse(url)); request.headers['Authorization'] = _basicAuth(username, password); @@ -927,20 +1026,26 @@ class WebDAVService { response.statusCode == 307 || response.statusCode == 308) { final location = response.headers['location']; if (location != null) { + // print('WebDAV: Following redirect to $location'); request = http.Request('GET', Uri.parse(location)); request.headers['Authorization'] = _basicAuth(username, password); response = await client.send(request); } } + // print('WebDAV: Download response status: ${response.statusCode}'); + if (response.statusCode == 200) { final bytes = await response.stream.toBytes(); await localFile.writeAsBytes(bytes); + // print('WebDAV: Downloaded ${bytes.length} bytes to ${localFile.path}'); return true; + } else { + // print('WebDAV: Download failed with status ${response.statusCode}'); } return false; } catch (e) { - print('WebDAV: Download error: $e'); + // print('WebDAV: Download error: $e'); return false; } } diff --git a/pubspec.lock b/pubspec.lock index fbe9a48..be22f9b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -182,6 +198,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -256,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" image_picker: dependency: "direct main" description: @@ -320,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -528,6 +568,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -741,6 +789,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -749,6 +821,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -805,6 +885,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.dev" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" + url: "https://pub.dev" + source: hosted + version: "4.10.13" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fc0af89d403e1c053f03d023d97550412fa79f35332e2939514c82e6fe633198 + url: "https://pub.dev" + source: hosted + version: "3.23.8" win32: dependency: transitive description: @@ -821,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f7c85b..d749742 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,11 +25,22 @@ dependencies: permission_handler: ^11.3.0 flutter_staggered_grid_view: ^0.7.0 http: ^1.2.0 + url_launcher: ^6.2.5 + webview_flutter: ^4.8.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^4.0.0 + flutter_launcher_icons: ^0.13.1 + +flutter_launcher_icons: + android: "ic_launcher" + ios: false + image_path: "assets/icon/app_icon.png" + adaptive_icon_background: "#FFFFFF" + adaptive_icon_foreground: "assets/icon/app_icon.png" + min_sdk_android: 21 flutter: uses-material-design: true