This commit is contained in:
DelLevin-Home
2026-03-08 01:43:26 +08:00
parent 901d6e27d6
commit 0c63242ca0
19 changed files with 531 additions and 364 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

BIN
assets/icon/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

View File

@@ -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<void> _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<void> _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<AppProvider>();
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(

View File

@@ -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<ProfilePage> {
/// 显示设置
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<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
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),
),
),
],
),
);
}
}

View File

@@ -270,11 +270,11 @@ class _StatisticsPageState extends State<StatisticsPage> 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<StatisticsPage> 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<StatisticsPage> 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),
],

View File

@@ -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 {
</D:propfind>''';
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<SyncResult> 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<int> _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<bool> _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<int>);
// 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<int>);
imageCount++;
}
}
// print('WebDAV: Extracted $imageCount images');
return true;
} catch (e) {
// print('WebDAV: Extract ZIP error: $e');
return false;
}
}
/// 获取远程文件信息
Future<Map<String, dynamic>?> _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 = <String, File>{}; // 相对路径 -> 文件
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'<d:href>([^<]+)</d:href>', 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('<d:collection');
// 检查是文件还是目录 - 查找这个 href 对应的 <D:response> 部分Apache 使用大写 D
final responseStart = body.lastIndexOf('<D:response>', match.start);
final responseEnd = body.indexOf('</D:response>', 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)}');
// 检查是否包含 <D:collection/> 或 <d:collection/> 标签Apache WebDAV 使用大写 D
isDirectory = responseSection.contains('<D:collection/>') ||
responseSection.contains('<d:collection/>') ||
responseSection.contains('<D:collection />') ||
responseSection.contains('<d:collection />');
// print('WebDAV: $name contains <D:collection/>: ${responseSection.contains('<D:collection/>')}');
}
// 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;
}
}

View File

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

View File

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