generated from dellevin/template
新增本地自动备份
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../utils/backup_service.dart';
|
||||
import '../utils/auto_backup_service.dart';
|
||||
import '../utils/toast_util.dart';
|
||||
|
||||
/// 本地备份页面
|
||||
@@ -16,6 +17,30 @@ class BackupPage extends StatefulWidget {
|
||||
class _BackupPageState extends State<BackupPage> {
|
||||
bool _isExporting = false;
|
||||
bool _isImporting = false;
|
||||
bool _autoBackupEnabled = false;
|
||||
bool _isLoading = true;
|
||||
List<File> _autoBackupFiles = [];
|
||||
String? _backupDirPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAutoBackupStatus();
|
||||
}
|
||||
|
||||
Future<void> _loadAutoBackupStatus() async {
|
||||
final enabled = await AutoBackupService.instance.getEnabled();
|
||||
final files = await AutoBackupService.instance.getBackupFiles();
|
||||
final dirPath = await AutoBackupService.instance.getBackupDirectoryPath();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_autoBackupEnabled = enabled;
|
||||
_autoBackupFiles = files;
|
||||
_backupDirPath = dirPath;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -24,9 +49,22 @@ class _BackupPageState extends State<BackupPage> {
|
||||
appBar: AppBar(
|
||||
title: const Text('本地备份'),
|
||||
),
|
||||
body: ListView(
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
// 自动备份开关
|
||||
_buildAutoBackupSection(),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 自动备份文件列表
|
||||
if (_autoBackupFiles.isNotEmpty) ...[
|
||||
_buildBackupFilesSection(),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
// 导出数据
|
||||
_buildSection(
|
||||
title: '导出数据',
|
||||
@@ -298,4 +336,189 @@ class _BackupPageState extends State<BackupPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建自动备份区域
|
||||
Widget _buildAutoBackupSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.schedule,
|
||||
size: 24,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'自动本地备份',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _autoBackupEnabled,
|
||||
onChanged: (value) async {
|
||||
setState(() => _autoBackupEnabled = value);
|
||||
await AutoBackupService.instance.setEnabled(value);
|
||||
if (value) {
|
||||
ToastUtil.show(context, '自动备份已开启,每2分钟备份一次');
|
||||
} else {
|
||||
ToastUtil.show(context, '自动备份已关闭');
|
||||
}
|
||||
// 刷新文件列表
|
||||
await _loadAutoBackupStatus();
|
||||
},
|
||||
activeColor: const Color(0xFF1A1A1A),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'每隔2分钟自动备份到下载目录/mooknote文件夹,最多保留10个备份文件',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF666666),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
if (_backupDirPath != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'备份位置: $_backupDirPath',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建备份文件列表区域
|
||||
Widget _buildBackupFilesSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.folder_outlined,
|
||||
size: 24,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'自动备份文件 (${_autoBackupFiles.length}/10)',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
..._autoBackupFiles.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final file = entry.value;
|
||||
final fileName = file.path.split('/').last;
|
||||
final stat = file.statSync();
|
||||
final size = _formatFileSize(stat.size);
|
||||
final modified = _formatDateTime(stat.modified);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: index < _autoBackupFiles.length - 1
|
||||
? const BorderSide(color: Color(0xFFE5E5E5))
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.backup,
|
||||
size: 18,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$modified · $size',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (index == 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: const Text(
|
||||
'最新',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化文件大小
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
|
||||
/// 格式化日期时间
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.month}/${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,33 +119,60 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
// 图片区域(仅在纯文本模式下显示)
|
||||
if (note.contentType == 'plain_text' && note.images.isNotEmpty)
|
||||
Container(
|
||||
height: 120,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: note.images.length,
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showImagePreview(context, note.images, index),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 图片标题
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.image_outlined,
|
||||
size: 14,
|
||||
color: Color(0xFF999999),
|
||||
),
|
||||
child: Image.file(
|
||||
File(note.images[index]),
|
||||
fit: BoxFit.cover,
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'图片 (${note.images.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 图片列表
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: note.images.length,
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showImagePreview(context, note.images, index),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Image.file(
|
||||
File(note.images[index]),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -282,16 +309,17 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
/// 构建纯文本内容
|
||||
Widget _buildPlainTextContent(Note note) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
child: SelectableText(
|
||||
note.content,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: 15,
|
||||
color: Color(0xFF1A1A1A),
|
||||
height: 1.8,
|
||||
height: 1.9,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -316,7 +344,12 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
|
||||
/// 跳转到编辑页面
|
||||
void _navigateToEdit(BuildContext context) {
|
||||
Navigator.pushNamed(context, '/note-form', arguments: widget.note).then((_) {
|
||||
// 从 Provider 获取最新的笔记数据,确保图片等字段是最新的
|
||||
final currentNote = context.read<AppProvider>().notes.firstWhere(
|
||||
(n) => n.id == widget.note.id,
|
||||
orElse: () => widget.note,
|
||||
);
|
||||
Navigator.pushNamed(context, '/note-form', arguments: currentNote).then((_) {
|
||||
context.read<AppProvider>().loadNotes();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,14 +21,12 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
bool _isLoading = false;
|
||||
bool _isConfigured = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _isAutoSyncEnabled = false;
|
||||
SyncDirection _syncDirection = SyncDirection.upload;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadConfig();
|
||||
_loadAutoSyncStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -40,33 +38,6 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 加载自动同步状态
|
||||
Future<void> _loadAutoSyncStatus() async {
|
||||
final enabled = await WebDAVService.instance.isAutoSyncEnabled();
|
||||
setState(() => _isAutoSyncEnabled = enabled);
|
||||
}
|
||||
|
||||
/// 切换自动同步
|
||||
Future<void> _toggleAutoSync(bool value) async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
if (value) {
|
||||
await WebDAVService.instance.startAutoSync();
|
||||
ToastUtil.show(context, '自动备份已开启,每5分钟执行一次');
|
||||
} else {
|
||||
await WebDAVService.instance.stopAutoSync();
|
||||
ToastUtil.show(context, '自动备份已关闭');
|
||||
}
|
||||
|
||||
setState(() => _isAutoSyncEnabled = value);
|
||||
} catch (e) {
|
||||
ToastUtil.show(context, '操作失败: $e');
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载已保存的配置
|
||||
Future<void> _loadConfig() async {
|
||||
final config = await WebDAVService.instance.getConfig();
|
||||
@@ -357,56 +328,6 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
if (_isConfigured) ...[
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 自动备份开关
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.schedule,
|
||||
size: 20,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'自动备份',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'每5分钟自动备份一次,保留最近10个备份',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF999999),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _isAutoSyncEnabled,
|
||||
onChanged: _isLoading ? null : _toggleAutoSync,
|
||||
activeColor: const Color(0xFF1A1A1A),
|
||||
inactiveThumbColor: const Color(0xFF999999),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 同步方向选择
|
||||
const Text(
|
||||
'同步方向',
|
||||
|
||||
Reference in New Issue
Block a user