新增本地自动备份

This commit is contained in:
DelLevin-Home
2026-03-08 13:34:57 +08:00
parent bb7b2c0191
commit c894921472
7 changed files with 744 additions and 174 deletions

View File

@@ -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')}';
}
}

View File

@@ -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();
});
}

View File

@@ -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(
'同步方向',