This commit is contained in:
DelLevin-Home
2026-03-07 23:20:24 +08:00
parent dc513fb6f6
commit d28c5e562e
24 changed files with 1983 additions and 636 deletions

View File

@@ -5,7 +5,7 @@ import '../providers/app_provider.dart';
import '../utils/backup_service.dart';
import '../utils/toast_util.dart';
/// 数据备份页面
/// 本地备份页面
class BackupPage extends StatefulWidget {
const BackupPage({super.key});
@@ -22,7 +22,7 @@ class _BackupPageState extends State<BackupPage> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('数据备份'),
title: const Text('本地备份'),
),
body: ListView(
padding: const EdgeInsets.all(24),

View File

@@ -2,11 +2,12 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/toast_util.dart';
import '../utils/image_path_helper.dart';
/// 添加/编辑书籍页面 - 紧凑双行布局设计
class BookFormPage extends StatefulWidget {
@@ -649,18 +650,22 @@ class _BookFormPageState extends State<BookFormPage> {
);
if (pickedFile != null) {
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'book_cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, 'book_covers', fileName);
// 生成文件名
final fileName = 'cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
final coverDir = Directory(path.join(appDir.path, 'book_covers'));
if (!await coverDir.exists()) {
await coverDir.create(recursive: true);
}
// 如果是编辑模式使用现有书籍ID如果是新建模式使用临时ID保存时会替换
final bookId = widget.book?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
await File(pickedFile.path).copy(savedPath);
// 保存到新的路径结构: images/books/{bookId}/{fileName}
final targetPath = await ImagePathHelper.instance.getBookCoverPath(
bookId,
fileName
);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
setState(() => _coverPath = savedPath);
await File(pickedFile.path).copy(targetPath);
setState(() => _coverPath = targetPath);
}
} catch (e) {
if (mounted) {
@@ -682,10 +687,19 @@ class _BookFormPageState extends State<BookFormPage> {
final now = DateTime.now();
if (widget.book == null) {
// 生成新的书籍ID
final newBookId = now.millisecondsSinceEpoch.toString();
// 如果有封面需要移动到正确的ID目录
String? finalCoverPath;
if (_coverPath != null && _coverPath!.isNotEmpty) {
finalCoverPath = await _moveCoverToNewId(_coverPath!, newBookId);
}
final newBook = Book(
id: now.millisecondsSinceEpoch.toString(),
id: newBookId,
title: _titleController.text.trim(),
coverPath: _coverPath,
coverPath: finalCoverPath,
authors: _authors,
alternateTitles: _alternateTitles,
publisher: _publisherController.text.trim(),
@@ -721,4 +735,45 @@ class _BookFormPageState extends State<BookFormPage> {
Navigator.pop(context);
}
/// 将封面从临时ID目录移动到新的书籍ID目录
Future<String?> _moveCoverToNewId(String currentPath, String newBookId) async {
// 检查是否已经在正确的目录中(兼容 Windows 路径分隔符)
final normalizedPath = currentPath.replaceAll('\\', '/');
if (normalizedPath.contains('/books/$newBookId/')) {
return currentPath;
}
// 提取文件名
final fileName = p.basename(currentPath);
// 获取新路径
final newPath = await ImagePathHelper.instance.getBookCoverPath(
newBookId,
fileName
);
// 确保目标目录存在
await ImagePathHelper.instance.ensureDirExists(p.dirname(newPath));
// 移动文件
final currentFile = File(currentPath);
if (await currentFile.exists()) {
await currentFile.rename(newPath);
// 删除空的临时目录
final tempDir = Directory(p.dirname(currentPath));
if (await tempDir.exists()) {
try {
await tempDir.delete(recursive: true);
} catch (e) {
// 忽略删除目录失败的情况
}
}
return newPath;
}
return null;
}
}

View File

@@ -106,108 +106,111 @@ class _BookReviewsPageState extends State<BookReviewsPage> {
}
Widget _buildReviewItem(BookReview review) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部:类型标签 + 操作按钮
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? const Color(0xFFF5F5F5)
: const Color(0xFF1A1A1A),
),
child: Text(
review.typeText,
style: TextStyle(
fontSize: 11,
return InkWell(
onLongPress: () => _showDeleteDialog(review),
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部:类型标签 + 操作按钮
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? const Color(0xFF666666)
: Colors.white,
? const Color(0xFFF5F5F5)
: const Color(0xFF1A1A1A),
),
child: Text(
review.typeText,
style: TextStyle(
fontSize: 11,
color: review.reviewType == 1
? const Color(0xFF666666)
: Colors.white,
),
),
),
),
const Spacer(),
// 编辑按钮
GestureDetector(
onTap: () => _navigateToEditReview(review),
child: const Icon(
Icons.edit_outlined,
size: 18,
color: Color(0xFF999999),
),
),
const SizedBox(width: 16),
// 删除按钮
GestureDetector(
onTap: () => _showDeleteDialog(review),
child: const Icon(
Icons.delete_outline,
size: 18,
color: Colors.red,
),
),
],
),
const SizedBox(height: 12),
// 评论内容
Text(
review.content,
maxLines: review.reviewType == 1 ? 3 : 5,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.6,
),
),
const SizedBox(height: 12),
// 底部信息
Row(
children: [
if (review.reviewer.isNotEmpty) ...[
Text(
review.reviewer,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
const Spacer(),
// 编辑按钮
GestureDetector(
onTap: () => _navigateToEditReview(review),
child: const Icon(
Icons.edit_outlined,
size: 18,
color: Color(0xFF999999),
),
),
const SizedBox(width: 16),
// 删除按钮
GestureDetector(
onTap: () => _showDeleteDialog(review),
child: const Icon(
Icons.delete_outline,
size: 18,
color: Colors.red,
),
),
const SizedBox(width: 8),
],
if (review.source.isNotEmpty) ...[
),
const SizedBox(height: 12),
// 评论内容
Text(
review.content,
maxLines: review.reviewType == 1 ? 3 : 5,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.6,
),
),
const SizedBox(height: 12),
// 底部信息
Row(
children: [
if (review.reviewer.isNotEmpty) ...[
Text(
review.reviewer,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
const SizedBox(width: 8),
],
if (review.source.isNotEmpty) ...[
Text(
'来源:${review.source}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
const SizedBox(width: 8),
],
const Spacer(),
Text(
'来源:${review.source}',
_formatDate(review.createdAt),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
const SizedBox(width: 8),
],
const Spacer(),
Text(
_formatDate(review.createdAt),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
],
),
],
),
),
);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'webdav_sync_page.dart';
/// 云同步主页面 - 选择同步方式
/// 云备份主页面 - 选择备份方式
class CloudSyncPage extends StatelessWidget {
const CloudSyncPage({super.key});
@@ -10,17 +10,17 @@ class CloudSyncPage extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('同步'),
title: const Text('备份'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// WebDAV 同步选项
// WebDAV 备份选项
_buildSyncOption(
context,
icon: Icons.storage_outlined,
title: 'WebDAV 同步',
subtitle: '通过 WebDAV 协议同步到个人云盘如坚果云、Nextcloud 等)',
title: 'WebDAV 备份',
subtitle: '通过 WebDAV 协议备份到个人云盘如坚果云、Nextcloud 等)',
onTap: () {
Navigator.push(
context,
@@ -56,7 +56,7 @@ class CloudSyncPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'关于云同步',
'关于云备份',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -65,10 +65,10 @@ class CloudSyncPage extends StatelessWidget {
),
SizedBox(height: 8),
Text(
'• 云同步可以将您的数据备份到远程服务器\n'
'• 支持多台设备之间的数据同步\n'
'• 建议定期进行云同步以确保数据安全\n'
'• 首次同步可能需要较长时间,请保持网络连接',
'• 云备份可以将您的数据备份到远程服务器\n'
'• 支持多台设备之间的数据恢复\n'
'• 建议定期进行云备份以确保数据安全\n'
'• 首次备份可能需要较长时间,请保持网络连接',
style: TextStyle(
fontSize: 13,
color: Color(0xFF666666),

View File

@@ -7,12 +7,12 @@ 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 {
bidirectional, // 双向同步
uploadOnly, // 仅上传
downloadOnly, // 仅下载
uploadOnly, // 上传
downloadOnly, // 下载
}
/// 主内容页 - 观影/阅读/笔记标签页
@@ -44,11 +44,11 @@ class MainContentPage extends StatelessWidget {
return AppBar(
title: Text(_getAppBarTitle(provider)),
actions: [
// 云同步按钮
// 云备份按钮
IconButton(
icon: const Icon(Icons.cloud_sync_outlined),
onPressed: () => _showCloudSyncDialog(context, provider),
tooltip: '同步',
tooltip: '备份',
),
// 搜索按钮
IconButton(
@@ -248,7 +248,7 @@ class MainContentPage extends StatelessWidget {
),
),
child: const Text(
'同步',
'备份',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
@@ -260,27 +260,19 @@ class MainContentPage extends StatelessWidget {
// 同步选项
Column(
children: [
_buildSyncOption(
context,
icon: Icons.sync,
iconColor: const Color(0xFF1A1A1A),
title: '双向同步',
subtitle: '本地和云端数据合并,冲突时以最新为准',
onTap: () {
Navigator.pop(context);
_navigateToSync(context, SyncMode.bidirectional);
},
),
const Divider(height: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
_buildSyncOption(
context,
icon: Icons.cloud_upload,
iconColor: const Color(0xFF1A1A1A),
title: '上传',
subtitle: '将本地数据上传到云端,覆盖云端数据',
onTap: () {
title: '上传',
subtitle: '将本地数据备份到云端',
onTap: () async {
Navigator.pop(context);
_navigateToSync(context, SyncMode.uploadOnly);
// 等待对话框关闭
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
_navigateToSync(context, SyncMode.uploadOnly);
}
},
),
const Divider(height: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
@@ -288,11 +280,15 @@ class MainContentPage extends StatelessWidget {
context,
icon: Icons.cloud_download,
iconColor: const Color(0xFF1A1A1A),
title: '下载',
subtitle: '从云端下载数据到本地,覆盖本地数据',
onTap: () {
title: '下载',
subtitle: '从云端恢复数据到本地',
onTap: () async {
Navigator.pop(context);
_navigateToSync(context, SyncMode.downloadOnly);
// 等待对话框关闭
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
_navigateToSync(context, SyncMode.downloadOnly);
}
},
),
],
@@ -395,30 +391,35 @@ class MainContentPage extends StatelessWidget {
);
}
/// 导航到同步页面
void _navigateToSync(BuildContext context, SyncMode mode) {
// TODO: 打开 WebDAV 同步页面并传递同步模式
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => WebDAVSyncPage(syncMode: mode),
// ),
// );
/// 执行云备份操作
Future<void> _navigateToSync(BuildContext context, SyncMode mode) async {
// 执行同步
final direction = mode == SyncMode.uploadOnly
? SyncDirection.upload
: SyncDirection.download;
// 暂时显示提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('即将开始${_getSyncModeText(mode)}...'),
duration: const Duration(seconds: 2),
),
);
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.bidirectional:
return '双向同步';
case SyncMode.uploadOnly:
return '上传';
case SyncMode.downloadOnly:

View File

@@ -2,11 +2,12 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/toast_util.dart';
import '../utils/image_path_helper.dart';
/// 添加/编辑影视页面 - 紧凑双行布局设计
class MovieFormPage extends StatefulWidget {
@@ -691,18 +692,22 @@ class _MovieFormPageState extends State<MovieFormPage> {
);
if (pickedFile != null) {
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, 'movie_posters', fileName);
// 生成文件名
final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
final posterDir = Directory(path.join(appDir.path, 'movie_posters'));
if (!await posterDir.exists()) {
await posterDir.create(recursive: true);
}
// 如果是编辑模式使用现有影视ID如果是新建模式使用临时ID保存时会替换
final movieId = widget.movie?.id ?? DateTime.now().millisecondsSinceEpoch.toString();
await File(pickedFile.path).copy(savedPath);
// 保存到新的路径结构: images/movies/{movieId}/{fileName}
final targetPath = await ImagePathHelper.instance.getMoviePosterPath(
movieId,
fileName
);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
setState(() => _posterPath = savedPath);
await File(pickedFile.path).copy(targetPath);
setState(() => _posterPath = targetPath);
}
} catch (e) {
if (mounted) {
@@ -748,10 +753,19 @@ class _MovieFormPageState extends State<MovieFormPage> {
final now = DateTime.now();
if (widget.movie == null) {
// 生成新的影视ID
final newMovieId = now.millisecondsSinceEpoch.toString();
// 如果有海报需要移动到正确的ID目录
String? finalPosterPath;
if (_posterPath != null && _posterPath!.isNotEmpty) {
finalPosterPath = await _movePosterToNewId(_posterPath!, newMovieId);
}
final newMovie = Movie(
id: now.millisecondsSinceEpoch.toString(),
id: newMovieId,
title: _titleController.text.trim(),
posterPath: _posterPath,
posterPath: finalPosterPath,
releaseDate: _releaseDate,
directors: _directors,
writers: _writers,
@@ -791,4 +805,45 @@ class _MovieFormPageState extends State<MovieFormPage> {
Navigator.pop(context);
}
/// 将海报从临时ID目录移动到新的影视ID目录
Future<String?> _movePosterToNewId(String currentPath, String newMovieId) async {
// 检查是否已经在正确的目录中(兼容 Windows 路径分隔符)
final normalizedPath = currentPath.replaceAll('\\', '/');
if (normalizedPath.contains('/movies/$newMovieId/')) {
return currentPath;
}
// 提取文件名
final fileName = p.basename(currentPath);
// 获取新路径
final newPath = await ImagePathHelper.instance.getMoviePosterPath(
newMovieId,
fileName
);
// 确保目标目录存在
await ImagePathHelper.instance.ensureDirExists(p.dirname(newPath));
// 移动文件
final currentFile = File(currentPath);
if (await currentFile.exists()) {
await currentFile.rename(newPath);
// 删除空的临时目录
final tempDir = Directory(p.dirname(currentPath));
if (await tempDir.exists()) {
try {
await tempDir.delete(recursive: true);
} catch (e) {
// 忽略删除目录失败的情况
}
}
return newPath;
}
return null;
}
}

View File

@@ -3,12 +3,13 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/toast_util.dart';
import '../utils/image_path_helper.dart';
import 'poster_gallery_page.dart';
/// 影视海报墙页面
@@ -197,21 +198,22 @@ class _MoviePostersPageState extends State<MoviePostersPage> {
);
if (pickedFile != null) {
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, 'movie_posters', fileName);
// 生成文件名
final fileName = 'posterimg_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 保存到 posterimgs 子目录: images/movies/{movieId}/posterimgs/{fileName}
final targetPath = await ImagePathHelper.instance.getMoviePosterImgPath(
widget.movie.id,
fileName
);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
final posterDir = Directory(path.join(appDir.path, 'movie_posters'));
if (!await posterDir.exists()) {
await posterDir.create(recursive: true);
}
await File(pickedFile.path).copy(savedPath);
await File(pickedFile.path).copy(targetPath);
final newPoster = MoviePoster(
id: DateTime.now().millisecondsSinceEpoch.toString(),
movieId: widget.movie.id,
posterPath: savedPath,
posterPath: targetPath,
createdAt: DateTime.now(),
);

View File

@@ -101,108 +101,111 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
}
Widget _buildReviewItem(MovieReview review) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部:类型标签 + 操作按钮
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? const Color(0xFFF5F5F5)
: const Color(0xFF1A1A1A),
),
child: Text(
review.typeText,
style: TextStyle(
fontSize: 11,
return InkWell(
onLongPress: () => _showDeleteDialog(review),
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部:类型标签 + 操作按钮
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: review.reviewType == 1
? const Color(0xFF666666)
: Colors.white,
? const Color(0xFFF5F5F5)
: const Color(0xFF1A1A1A),
),
child: Text(
review.typeText,
style: TextStyle(
fontSize: 11,
color: review.reviewType == 1
? const Color(0xFF666666)
: Colors.white,
),
),
),
),
const Spacer(),
// 编辑按钮
GestureDetector(
onTap: () => _navigateToEditReview(review),
child: const Icon(
Icons.edit_outlined,
size: 18,
color: Color(0xFF999999),
),
),
const SizedBox(width: 16),
// 删除按钮
GestureDetector(
onTap: () => _showDeleteDialog(review),
child: const Icon(
Icons.delete_outline,
size: 18,
color: Colors.red,
),
),
],
),
const SizedBox(height: 12),
// 评论内容
Text(
review.content,
maxLines: review.reviewType == 1 ? 3 : 5,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.6,
),
),
const SizedBox(height: 12),
// 底部信息
Row(
children: [
if (review.reviewer.isNotEmpty) ...[
Text(
review.reviewer,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
const Spacer(),
// 编辑按钮
GestureDetector(
onTap: () => _navigateToEditReview(review),
child: const Icon(
Icons.edit_outlined,
size: 18,
color: Color(0xFF999999),
),
),
const SizedBox(width: 16),
// 删除按钮
GestureDetector(
onTap: () => _showDeleteDialog(review),
child: const Icon(
Icons.delete_outline,
size: 18,
color: Colors.red,
),
),
const SizedBox(width: 8),
],
if (review.source.isNotEmpty) ...[
),
const SizedBox(height: 12),
// 评论内容
Text(
review.content,
maxLines: review.reviewType == 1 ? 3 : 5,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.6,
),
),
const SizedBox(height: 12),
// 底部信息
Row(
children: [
if (review.reviewer.isNotEmpty) ...[
Text(
review.reviewer,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
const SizedBox(width: 8),
],
if (review.source.isNotEmpty) ...[
Text(
'来源:${review.source}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
const SizedBox(width: 8),
],
const Spacer(),
Text(
'来源:${review.source}',
_formatDate(review.createdAt),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
const SizedBox(width: 8),
],
const Spacer(),
Text(
_formatDate(review.createdAt),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
],
),
],
),
),
);
}
@@ -259,4 +262,4 @@ class _MovieReviewsPageState extends State<MovieReviewsPage> {
),
);
}
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
@@ -18,33 +19,50 @@ class NoteDetailPage extends StatefulWidget {
class _NoteDetailPageState extends State<NoteDetailPage> {
@override
Widget build(BuildContext context) {
// 从 Provider 获取最新的笔记数据
final note = context.watch<AppProvider>().notes.firstWhere(
(n) => n.id == widget.note.id,
orElse: () => widget.note,
);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(_formatDateTime(widget.note.createdAt)),
title: Text(_getTitle(note.content)),
actions: [
// 格式指示器
if (widget.note.contentType == 'markdown')
// 格式指示器 - 纯文本标记
if (note.contentType == 'markdown')
Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
borderRadius: BorderRadius.circular(2),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.code, size: 14, color: Color(0xFF666666)),
SizedBox(width: 4),
Text(
'Markdown',
style: TextStyle(
fontSize: 12,
color: Color(0xFF666666),
),
),
],
child: const Text(
'MD',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xFF666666),
),
),
)
else
Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(2),
),
child: const Text(
'TXT',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xFF666666),
),
),
),
IconButton(
@@ -57,9 +75,9 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
body: Column(
children: [
// 标签区域
if (widget.note.tags.isNotEmpty)
if (note.tags.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
@@ -70,7 +88,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.note.tags.map((tag) {
children: note.tags.map((tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
@@ -93,11 +111,41 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
// 内容区域
Expanded(
child: widget.note.contentType == 'markdown'
? _buildMarkdownContent()
: _buildPlainTextContent(),
child: note.contentType == 'markdown'
? _buildMarkdownContent(note)
: _buildPlainTextContent(note),
),
// 图片区域(仅在纯文本模式下显示)
if (note.contentType == 'plain_text' && note.images.isNotEmpty)
Container(
height: 120,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
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 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,
),
);
},
),
),
// 底部操作栏
Container(
decoration: const BoxDecoration(
@@ -107,33 +155,53 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'更新于 ${_formatDateTime(widget.note.updatedAt)}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
// 创建时间和更新时间
Row(
children: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20),
color: const Color(0xFF666666),
onPressed: () => _navigateToEdit(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'创建时间:${_formatDateTime(note.createdAt)}',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF999999),
),
),
const SizedBox(height: 4),
Text(
'更新时间:${_formatDateTime(note.updatedAt)}',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF999999),
),
),
],
),
),
const SizedBox(width: 16),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
Row(
children: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20),
color: const Color(0xFF666666),
onPressed: () => _navigateToEdit(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(width: 16),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
@@ -148,9 +216,9 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
}
/// 构建 Markdown 内容
Widget _buildMarkdownContent() {
Widget _buildMarkdownContent(Note note) {
return Markdown(
data: widget.note.content,
data: note.content,
styleSheet: MarkdownStyleSheet(
h1: const TextStyle(
fontSize: 24,
@@ -204,20 +272,24 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
decoration: TextDecoration.underline,
),
),
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(16),
);
}
/// 构建纯文本内容
Widget _buildPlainTextContent() {
Widget _buildPlainTextContent(Note note) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Text(
widget.note.content,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
height: 1.8,
padding: const EdgeInsets.all(16),
child: SizedBox(
width: double.infinity,
child: Text(
note.content,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
height: 1.8,
),
),
),
);
@@ -228,6 +300,17 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
/// 获取标题内容前5个字
String _getTitle(String content) {
if (content.isEmpty) return '无标题';
// 移除换行符和多余空格
final trimmed = content.replaceAll('\n', ' ').trim();
if (trimmed.isEmpty) return '无标题';
// 取前5个字
if (trimmed.length <= 5) return trimmed;
return '${trimmed.substring(0, 5)}...';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/note-form', arguments: widget.note).then((_) {

View File

@@ -1,8 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/toast_util.dart';
import '../utils/image_path_helper.dart';
/// 添加/编辑笔记页面 - 极简书写界面
class NoteFormPage extends StatefulWidget {
@@ -18,8 +22,11 @@ class _NoteFormPageState extends State<NoteFormPage> {
late TextEditingController _contentController;
late DateTime _createdAt;
List<String> _tags = [];
String _contentType = 'markdown'; // markdown / rich_text
List<String> _images = []; // 图片路径列表
String _contentType = 'markdown'; // markdown / plain_text
bool _isEditing = false;
final ImagePicker _picker = ImagePicker();
String? _tempNoteId; // 新建模式时使用的临时笔记ID
@override
void initState() {
@@ -28,6 +35,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
_contentController = TextEditingController(text: note?.content ?? '');
_createdAt = note?.createdAt ?? DateTime.now();
_tags = note != null ? List.from(note.tags) : [];
_images = note != null ? List.from(note.images) : [];
_contentType = note?.contentType ?? 'markdown';
_isEditing = note != null;
}
@@ -93,29 +101,159 @@ class _NoteFormPageState extends State<NoteFormPage> {
),
),
// 书写区域
Expanded(
child: TextField(
controller: _contentController,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
height: 1.6,
),
decoration: InputDecoration(
hintText: _contentType == 'markdown' ? '使用 Markdown 格式书写...' : '开始书写...',
hintStyle: const TextStyle(
// 书写区域纯文本模式下占据35%高度Markdown模式下占据全部
if (_contentType == 'plain_text')
SizedBox(
height: MediaQuery.of(context).size.height * 0.35,
child: TextField(
controller: _contentController,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
color: Color(0xFF1A1A1A),
height: 1.6,
),
decoration: const InputDecoration(
hintText: '开始书写...',
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
),
)
else
Expanded(
child: TextField(
controller: _contentController,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
height: 1.6,
),
decoration: const InputDecoration(
hintText: '使用 Markdown 格式书写...',
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
),
),
// 纯文本模式下的图片区域
if (_contentType == 'plain_text') ...[
// 图片网格区域
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题栏
Row(
children: [
const Text(
'图片',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 8),
Text(
'${_images.length}',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
),
const Spacer(),
// 添加图片按钮
InkWell(
onTap: _pickImage,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(2),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.add,
size: 16,
color: Colors.white,
),
SizedBox(width: 4),
Text(
'添加',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
],
),
),
),
],
),
const SizedBox(height: 12),
// 图片网格4列正方形铺满
Expanded(
child: _images.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_outlined,
size: 48,
color: const Color(0xFFCCCCCC),
),
const SizedBox(height: 8),
const Text(
'点击添加按钮添加图片',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
),
],
),
)
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.0,
),
itemCount: _images.length,
itemBuilder: (context, index) {
return _buildImageItem(index);
},
),
),
],
),
),
),
],
],
),
);
@@ -140,7 +278,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
),
const SizedBox(width: 4),
Text(
_contentType == 'markdown' ? 'Markdown' : '文本',
_contentType == 'markdown' ? 'Markdown' : '文本',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF666666),
@@ -184,12 +322,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
ListTile(
leading: const Icon(Icons.text_fields, size: 20),
title: const Text('纯文本'),
subtitle: const Text('普通文本格式'),
trailing: _contentType == 'rich_text'
subtitle: const Text('普通文本格式,支持图片'),
trailing: _contentType == 'plain_text'
? const Icon(Icons.check, color: Color(0xFF1A1A1A))
: null,
onTap: () {
setState(() => _contentType = 'rich_text');
setState(() => _contentType = 'plain_text');
Navigator.pop(context);
},
),
@@ -344,16 +482,29 @@ class _NoteFormPageState extends State<NoteFormPage> {
content: content,
contentType: _contentType,
tags: _tags,
images: _images,
updatedAt: now,
);
await context.read<AppProvider>().updateNote(updatedNote);
} else {
// 添加新笔记
// 添加新笔记 - 先创建笔记获取ID
final noteId = now.millisecondsSinceEpoch.toString();
// 如果有图片需要移动到正确的ID目录
List<String> finalImages = [];
if (_images.isNotEmpty) {
// 使用保存的临时ID如果没有则使用当前noteId理论上不会走到这里
final oldNoteId = _tempNoteId ?? noteId;
final newNoteId = noteId;
finalImages = await _moveImagesToNewId(oldNoteId, newNoteId);
}
final newNote = Note(
id: now.millisecondsSinceEpoch.toString(),
id: noteId,
content: content,
contentType: _contentType,
tags: _tags,
images: finalImages.isNotEmpty ? finalImages : _images,
createdAt: _createdAt,
updatedAt: now,
);
@@ -366,4 +517,123 @@ class _NoteFormPageState extends State<NoteFormPage> {
Navigator.pop(context);
}
/// 将图片从临时ID目录移动到新ID目录
Future<List<String>> _moveImagesToNewId(String oldNoteId, String newNoteId) async {
final List<String> newPaths = [];
final newDir = await ImagePathHelper.instance.getNoteImagesDir(newNoteId);
for (final imagePath in _images) {
// 使用路径分隔符检查,兼容 Windows 和 Unix
final normalizedPath = imagePath.replaceAll('\\', '/');
if (normalizedPath.contains('/notes/$oldNoteId/')) {
// 需要移动的文件
final fileName = p.basename(imagePath);
final newPath = p.join(newDir, fileName);
await ImagePathHelper.instance.ensureDirExists(newDir);
// 检查源文件是否存在
final sourceFile = File(imagePath);
if (await sourceFile.exists()) {
await sourceFile.rename(newPath);
newPaths.add(newPath);
}
} else {
// 已经在正确位置的文件
newPaths.add(imagePath);
}
}
// 删除旧目录
try {
await ImagePathHelper.instance.deleteNoteImages(oldNoteId);
} catch (e) {
// 忽略删除失败
}
return newPaths;
}
/// 选择图片
Future<void> _pickImage() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1920,
maxHeight: 1920,
imageQuality: 85,
);
if (image != null) {
// 生成唯一的文件名
final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
// 如果是编辑模式使用现有笔记ID如果是新建模式使用临时ID保存时会替换
String noteId;
if (_isEditing) {
noteId = widget.note!.id;
} else {
// 新建模式使用已存在的临时ID或生成新的
noteId = _tempNoteId ?? DateTime.now().millisecondsSinceEpoch.toString();
_tempNoteId = noteId;
}
// 复制图片到应用目录: images/notes/{noteId}/{fileName}
final targetDir = await ImagePathHelper.instance.getNoteImagesDir(noteId);
await ImagePathHelper.instance.ensureDirExists(targetDir);
final targetPath = p.join(targetDir, fileName);
await File(image.path).copy(targetPath);
setState(() => _images.add(targetPath));
}
} catch (e) {
ToastUtil.show(context, '选择图片失败: $e');
}
}
/// 构建图片项
Widget _buildImageItem(int index) {
return InkWell(
onLongPress: () => _showDeleteImageDialog(index),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Image.file(
File(_images[index]),
fit: BoxFit.cover,
),
),
);
}
/// 显示删除图片确认对话框
void _showDeleteImageDialog(int index) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: const Text('确定要删除这张图片吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () {
setState(() => _images.removeAt(index));
Navigator.pop(context);
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
);
}
}

View File

@@ -32,6 +32,8 @@ class NoteTabPage extends StatelessWidget {
return RefreshIndicator(
onRefresh: () async => await provider.loadNotes(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: notes.length,
@@ -50,25 +52,45 @@ class NoteTabPage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.note_outlined,
size: 80,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'暂无笔记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.note_outlined,
size: 40,
color: Color(0xFFCCCCCC),
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('添加笔记'),
onPressed: () {
const SizedBox(height: 24),
const Text(
'暂无笔记',
style: TextStyle(
fontSize: 16,
color: Color(0xFF999999),
),
),
const SizedBox(height: 24),
InkWell(
onTap: () {
Navigator.pushNamed(context, '/note-form');
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
),
child: const Text(
'添加笔记',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
),
),
],
),

View File

@@ -396,7 +396,7 @@ class _ProfilePageState extends State<ProfilePage> {
_buildMenuItem(
icon: Icons.backup_outlined,
title: '数据备份',
title: '本地备份',
onTap: () {
Navigator.push(
context,
@@ -408,7 +408,7 @@ class _ProfilePageState extends State<ProfilePage> {
_buildMenuItem(
icon: Icons.cloud_sync_outlined,
title: '同步',
title: '备份',
onTap: () {
Navigator.push(
context,

View File

@@ -4,7 +4,7 @@ import '../utils/toast_util.dart';
import '../utils/webdav_service.dart';
import '../providers/app_provider.dart';
/// WebDAV 同步页面
/// WebDAV 备份页面
class WebDAVSyncPage extends StatefulWidget {
const WebDAVSyncPage({super.key});
@@ -21,13 +21,14 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
bool _isLoading = false;
bool _isConfigured = false;
bool _obscurePassword = true;
SyncDirection _syncDirection = SyncDirection.bidirectional;
SyncResult? _lastSyncResult;
bool _isAutoSyncEnabled = false;
SyncDirection _syncDirection = SyncDirection.upload;
@override
void initState() {
super.initState();
_loadConfig();
_loadAutoSyncStatus();
}
@override
@@ -39,6 +40,33 @@ 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, '自动备份已开启每2分钟执行一次');
} 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();
@@ -98,13 +126,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
setState(() => _isConfigured = true);
ToastUtil.show(context, result['message'] ?? '连接成功,配置已保存');
// 延迟一下再返回,确保 Toast 显示出来
await Future.delayed(const Duration(milliseconds: 500));
// 如果是首次配置成功,返回 true 给调用方
if (mounted) {
Navigator.maybePop(context, true);
}
// 连接成功后停留在当前页面,不返回上级
} else {
ToastUtil.show(context, result['message'] ?? '连接失败,请检查配置');
}
@@ -128,8 +150,6 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
if (!mounted) return;
setState(() => _lastSyncResult = result);
if (result.success) {
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
@@ -228,7 +248,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('WebDAV 同步'),
title: const Text('WebDAV 备份'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.maybePop(context),
@@ -337,6 +357,56 @@ 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(
'每2分钟自动备份一次保留最近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(
'同步方向',
@@ -351,15 +421,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
children: [
Expanded(
child: _buildDirectionButton(
'双向同步',
SyncDirection.bidirectional,
Icons.sync,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildDirectionButton(
'仅上传',
'上传',
SyncDirection.upload,
Icons.upload,
),
@@ -367,7 +429,7 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
const SizedBox(width: 8),
Expanded(
child: _buildDirectionButton(
'下载',
'下载',
SyncDirection.download,
Icons.download,
),