测试
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 67 KiB |
@@ -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>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 14 KiB |
4
android/app/src/main/res/values/colors.xml
Normal 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
|
After Width: | Height: | Size: 332 KiB |
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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: '远程数据库不存在');
|
||||
return SyncResult(success: false, message: '解压 ZIP 失败');
|
||||
}
|
||||
// 删除临时 ZIP 文件
|
||||
await zipFile.delete();
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
120
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:
|
||||
|
||||
11
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
|
||||
|
||||