flutter_synckit 0.0.3
flutter_synckit: ^0.0.3 copied to clipboard
一款集成 WebDAV 的轻量级 Flutter 同步库,支持断点续传、离线优先、冲突处理等高级功能。
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_synckit/flutter_synckit.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Sync Kit 示例',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const ResponsiveLayout(),
);
}
}
/// 响应式布局组件
class ResponsiveLayout extends StatefulWidget {
const ResponsiveLayout({super.key});
@override
State<ResponsiveLayout> createState() => _ResponsiveLayoutState();
}
class _ResponsiveLayoutState extends State<ResponsiveLayout> {
final SyncService _syncService = SyncService();
int _selectedIndex = 0;
late final List<NavigationItem> _navigationItems;
@override
void initState() {
super.initState();
_navigationItems = [
NavigationItem(
icon: Icons.sync,
label: '同步',
page: SyncPage(syncService: _syncService),
),
NavigationItem(
icon: Icons.folder,
label: '文件',
page: FilesPage(syncService: _syncService),
),
NavigationItem(
icon: Icons.history,
label: '历史',
page: HistoryPage(syncService: _syncService),
),
NavigationItem(
icon: Icons.settings,
label: '设置',
page: SettingsPage(syncService: _syncService),
),
];
}
@override
void dispose() {
_syncService.dispose();
super.dispose();
}
/// 根据屏幕宽度判断布局类型
ScreenType _getScreenType(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) return ScreenType.mobile;
if (width < 1200) return ScreenType.tablet;
return ScreenType.desktop;
}
@override
Widget build(BuildContext context) {
final screenType = _getScreenType(context);
return Scaffold(
body: switch (screenType) {
ScreenType.mobile => _buildMobileLayout(),
ScreenType.tablet => _buildTabletLayout(),
ScreenType.desktop => _buildDesktopLayout(),
},
);
}
/// 手机布局 - 底部导航栏
Widget _buildMobileLayout() {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _navigationItems.map((item) => item.page).toList(),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: _navigationItems
.map((item) => NavigationDestination(
icon: Icon(item.icon),
label: item.label,
))
.toList(),
),
);
}
/// 平板布局 - 侧边导航栏(可折叠)
Widget _buildTabletLayout() {
return Row(
children: [
NavigationRail(
extended: false,
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: _navigationItems
.map((item) => NavigationRailDestination(
icon: Icon(item.icon),
label: Text(item.label),
))
.toList(),
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: _navigationItems[_selectedIndex].page,
),
],
);
}
/// 桌面布局 - 扩展侧边导航栏 + 多列内容
Widget _buildDesktopLayout() {
return Row(
children: [
NavigationRail(
extended: true,
minExtendedWidth: 200,
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: _navigationItems
.map((item) => NavigationRailDestination(
icon: Icon(item.icon),
label: Text(item.label),
))
.toList(),
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: _navigationItems[_selectedIndex].page,
),
],
);
}
}
enum ScreenType { mobile, tablet, desktop }
class NavigationItem {
final IconData icon;
final String label;
final Widget page;
NavigationItem({
required this.icon,
required this.label,
required this.page,
});
}
// ==================== 同步服务类 ====================
class SyncService {
final FlutterSyncKit syncKit = FlutterSyncKit();
final List<SyncLog> logs = [];
final ValueNotifier<SyncState> stateNotifier = ValueNotifier(SyncState.idle);
final ValueNotifier<double> progressNotifier = ValueNotifier(0);
final ValueNotifier<List<LocalFileInfo>> localFilesNotifier =
ValueNotifier([]);
final ValueNotifier<List<SyncTask>> tasksNotifier = ValueNotifier([]);
bool isInitialized = false;
String? serverUrl;
String? username;
String? localDirectory;
Future<void> initialize(SyncConfig config) async {
await syncKit.initialize(config);
serverUrl = config.serverUrl;
username = config.username;
localDirectory = config.localDirectory;
syncKit.eventStream.listen(_handleEvent);
isInitialized = true;
stateNotifier.value = SyncState.idle;
await refreshLocalFiles();
}
void _handleEvent(SyncEvent event) {
switch (event.type) {
case SyncEventType.syncStarted:
stateNotifier.value = SyncState.syncing;
progressNotifier.value = 0;
addLog('开始同步', SyncLogType.info);
break;
case SyncEventType.syncCompleted:
stateNotifier.value = SyncState.idle;
progressNotifier.value = 100;
addLog('同步完成', SyncLogType.success);
refreshLocalFiles();
break;
case SyncEventType.syncFailed:
stateNotifier.value = SyncState.error;
addLog('同步失败: ${event.message}', SyncLogType.error);
break;
case SyncEventType.taskProgress:
progressNotifier.value = event.progress ?? 0;
break;
case SyncEventType.conflictDetected:
addLog('检测到冲突: ${event.file?.name}', SyncLogType.warning);
break;
case SyncEventType.taskCompleted:
addLog('完成: ${event.task?.file.name}', SyncLogType.success);
break;
case SyncEventType.taskFailed:
addLog('失败: ${event.task?.file.name}', SyncLogType.error);
break;
default:
break;
}
}
void addLog(String message, SyncLogType type) {
logs.insert(
0,
SyncLog(
message: message,
type: type,
timestamp: DateTime.now(),
));
if (logs.length > 100) {
logs.removeLast();
}
}
Future<void> refreshLocalFiles() async {
if (localDirectory == null) return;
final dir = Directory(localDirectory!);
if (!await dir.exists()) return;
final files = <LocalFileInfo>[];
await for (final entity in dir.list(recursive: true)) {
if (entity is File) {
final stat = await entity.stat();
files.add(LocalFileInfo(
path: entity.path,
name: entity.path.split('/').last,
size: stat.size,
modifiedTime: stat.modified,
));
}
}
localFilesNotifier.value = files;
}
Future<void> dispose() async {
await syncKit.dispose();
stateNotifier.dispose();
progressNotifier.dispose();
localFilesNotifier.dispose();
tasksNotifier.dispose();
}
}
class SyncLog {
final String message;
final SyncLogType type;
final DateTime timestamp;
SyncLog({
required this.message,
required this.type,
required this.timestamp,
});
}
enum SyncLogType { info, success, warning, error }
class LocalFileInfo {
final String path;
final String name;
final int size;
final DateTime modifiedTime;
LocalFileInfo({
required this.path,
required this.name,
required this.size,
required this.modifiedTime,
});
}
// ==================== 同步页面(响应式) ====================
class SyncPage extends StatefulWidget {
final SyncService syncService;
const SyncPage({super.key, required this.syncService});
@override
State<SyncPage> createState() => _SyncPageState();
}
class _SyncPageState extends State<SyncPage> {
final TextEditingController _serverUrlController = TextEditingController();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _remoteDirController = TextEditingController();
@override
void initState() {
super.initState();
_serverUrlController.text = 'https://dav.jianguoyun.com/dav/';
_remoteDirController.text = '/';
}
@override
void dispose() {
_serverUrlController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_remoteDirController.dispose();
super.dispose();
}
Future<void> _initialize() async {
try {
final appDir = await getApplicationDocumentsDirectory();
final syncDir = Directory('${appDir.path}/sync_data');
if (!await syncDir.exists()) {
await syncDir.create(recursive: true);
}
await widget.syncService.initialize(
SyncConfig(
serverUrl: _serverUrlController.text,
username: _usernameController.text.isEmpty
? null
: _usernameController.text,
password: _passwordController.text.isEmpty
? null
: _passwordController.text,
localDirectory: syncDir.path,
remoteDirectory: _remoteDirController.text.isEmpty
? '/'
: _remoteDirController.text,
conflictStrategy: ConflictStrategy.smartMerge,
enableChecksum: true,
),
);
setState(() {});
widget.syncService.addLog('初始化成功', SyncLogType.success);
} catch (e) {
widget.syncService.addLog('初始化失败: $e', SyncLogType.error);
}
}
Future<void> _startSync() async {
if (!widget.syncService.isInitialized) {
widget.syncService.addLog('请先初始化', SyncLogType.warning);
return;
}
try {
final result = await widget.syncService.syncKit.sync();
widget.syncService.addLog(
'同步结果: 成功 ${result.successCount}/${result.totalFiles}',
result.failedCount == 0 ? SyncLogType.success : SyncLogType.warning,
);
} catch (e) {
widget.syncService.addLog('同步异常: $e', SyncLogType.error);
}
}
Future<void> _createTestFile() async {
try {
final appDir = await getApplicationDocumentsDirectory();
final testFile = File(
'${appDir.path}/sync_data/test_${DateTime.now().millisecondsSinceEpoch}.txt',
);
await testFile.writeAsString(
'这是一个测试文件,创建于 ${DateTime.now()}\n\n'
'Flutter Sync Kit 是一款集成 WebDAV 的轻量级同步库。\n'
'支持断点续传、离线优先、冲突处理等高级功能。',
);
widget.syncService
.addLog('创建测试文件: ${testFile.path.split('/').last}', SyncLogType.info);
await widget.syncService.refreshLocalFiles();
} catch (e) {
widget.syncService.addLog('创建测试文件失败: $e', SyncLogType.error);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('同步'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 900;
if (isWide) {
// 宽屏布局:左右分栏
return Row(
children: [
// 左侧:配置和状态
SizedBox(
width: 450,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildConfigCard(),
const SizedBox(height: 16),
_buildStatusCard(),
],
),
),
),
const VerticalDivider(width: 1),
// 右侧:日志
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildLogCard(),
),
),
],
);
} else {
// 窄屏布局:上下堆叠
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildConfigCard(),
const SizedBox(height: 16),
_buildStatusCard(),
const SizedBox(height: 16),
SizedBox(
height: 400,
child: _buildLogCard(),
),
],
),
);
}
},
),
);
}
Widget _buildConfigCard() {
final isInitialized = widget.syncService.isInitialized;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.cloud, size: 24),
const SizedBox(width: 8),
const Text(
'WebDAV 配置',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Spacer(),
if (isInitialized)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'已连接',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _serverUrlController,
decoration: const InputDecoration(
labelText: '服务器地址',
hintText: 'https://dav.jianguoyun.com/dav/',
prefixIcon: Icon(Icons.link),
border: OutlineInputBorder(),
),
enabled: !isInitialized,
),
const SizedBox(height: 12),
TextField(
controller: _remoteDirController,
decoration: const InputDecoration(
labelText: '远程目录',
hintText: '/flutter_sync_kit/',
prefixIcon: Icon(Icons.folder_shared),
border: OutlineInputBorder(),
),
enabled: !isInitialized,
),
const SizedBox(height: 12),
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 400) {
return Row(
children: [
Expanded(
child: TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: '用户名',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
enabled: !isInitialized,
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
obscureText: true,
enabled: !isInitialized,
),
),
],
);
} else {
return Column(
children: [
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: '用户名',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
enabled: !isInitialized,
),
const SizedBox(height: 12),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
obscureText: true,
enabled: !isInitialized,
),
],
);
}
},
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: isInitialized ? null : _initialize,
icon: const Icon(Icons.login),
label: const Text('连接'),
),
ElevatedButton.icon(
onPressed: isInitialized ? _startSync : null,
icon: const Icon(Icons.sync),
label: const Text('开始同步'),
),
ElevatedButton.icon(
onPressed: _createTestFile,
icon: const Icon(Icons.add),
label: const Text('创建测试文件'),
),
],
),
],
),
),
);
}
Widget _buildStatusCard() {
return ValueListenableBuilder<SyncState>(
valueListenable: widget.syncService.stateNotifier,
builder: (context, state, child) {
return ValueListenableBuilder<double>(
valueListenable: widget.syncService.progressNotifier,
builder: (context, progress, child) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'同步状态',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Row(
children: [
_buildStatusIndicator(state),
const SizedBox(width: 12),
Text(
_getStatusText(state),
style: const TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress / 100,
minHeight: 8,
),
),
const SizedBox(height: 8),
Text(
'进度: ${progress.toStringAsFixed(1)}%',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
},
);
},
);
}
Widget _buildStatusIndicator(SyncState state) {
Color color;
IconData icon;
switch (state) {
case SyncState.syncing:
color = Colors.blue;
icon = Icons.sync;
break;
case SyncState.error:
color = Colors.red;
icon = Icons.error;
break;
case SyncState.idle:
color = Colors.green;
icon = Icons.check_circle;
break;
default:
color = Colors.grey;
icon = Icons.circle;
}
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withAlpha(25),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color),
);
}
String _getStatusText(SyncState state) {
switch (state) {
case SyncState.syncing:
return '同步中...';
case SyncState.error:
return '同步出错';
case SyncState.idle:
return '就绪';
default:
return '未初始化';
}
}
Widget _buildLogCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.article, size: 20),
const SizedBox(width: 8),
const Text(
'同步日志',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const Spacer(),
TextButton.icon(
onPressed: () =>
setState(() => widget.syncService.logs.clear()),
icon: const Icon(Icons.clear_all, size: 18),
label: const Text('清空'),
),
],
),
const SizedBox(height: 12),
Expanded(
child: ListenableBuilder(
listenable: widget.syncService.stateNotifier,
builder: (context, child) {
return ListView.builder(
reverse: true,
itemCount: widget.syncService.logs.length,
itemBuilder: (context, index) {
final log = widget.syncService.logs[index];
return _buildLogItem(log);
},
);
},
),
),
],
),
),
);
}
Widget _buildLogItem(SyncLog log) {
Color color;
IconData icon;
switch (log.type) {
case SyncLogType.success:
color = Colors.green;
icon = Icons.check_circle;
break;
case SyncLogType.error:
color = Colors.red;
icon = Icons.error;
break;
case SyncLogType.warning:
color = Colors.orange;
icon = Icons.warning;
break;
default:
color = Colors.blue;
icon = Icons.info;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
log.message,
style: TextStyle(fontSize: 13, color: color.withAlpha(230)),
),
Text(
'${log.timestamp.hour.toString().padLeft(2, '0')}:${log.timestamp.minute.toString().padLeft(2, '0')}:${log.timestamp.second.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
}
// ==================== 文件页面(响应式网格) ====================
class FilesPage extends StatefulWidget {
final SyncService syncService;
const FilesPage({super.key, required this.syncService});
@override
State<FilesPage> createState() => _FilesPageState();
}
class _FilesPageState extends State<FilesPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('本地文件'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
onPressed: () => widget.syncService.refreshLocalFiles(),
icon: const Icon(Icons.refresh),
),
],
),
body: ValueListenableBuilder<List<LocalFileInfo>>(
valueListenable: widget.syncService.localFilesNotifier,
builder: (context, files, child) {
if (files.isEmpty) {
return _buildEmptyState();
}
return LayoutBuilder(
builder: (context, constraints) {
// 根据屏幕宽度决定列数
int crossAxisCount;
if (constraints.maxWidth < 600) {
crossAxisCount = 1; // 手机:单列列表
} else if (constraints.maxWidth < 900) {
crossAxisCount = 2; // 小平板:双列
} else if (constraints.maxWidth < 1200) {
crossAxisCount = 3; // 大平板:三列
} else {
crossAxisCount = 4; // 桌面:四列
}
if (crossAxisCount == 1) {
// 手机使用列表视图
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: files.length,
itemBuilder: (context, index) {
return _buildFileListItem(files[index]);
},
);
} else {
// 平板/桌面使用网格视图
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: files.length,
itemBuilder: (context, index) {
return _buildFileGridItem(files[index]);
},
);
}
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showCreateFileDialog(),
child: const Icon(Icons.add),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_open,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(
'暂无文件',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () async {
final appDir = await getApplicationDocumentsDirectory();
final syncDir = Directory('${appDir.path}/sync_data');
if (await syncDir.exists()) {
widget.syncService
.addLog('打开文件夹: ${syncDir.path}', SyncLogType.info);
}
},
icon: const Icon(Icons.folder_open),
label: const Text('打开同步文件夹'),
),
],
),
);
}
Widget _buildFileListItem(LocalFileInfo file) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: Icon(
_getFileIcon(file.name),
color: Theme.of(context).colorScheme.primary,
),
title: Text(file.name, overflow: TextOverflow.ellipsis),
subtitle: Text(
'${_formatFileSize(file.size)} · ${_formatDate(file.modifiedTime)}',
),
trailing: PopupMenuButton<String>(
onSelected: (value) async {
switch (value) {
case 'view':
_viewFile(file);
break;
case 'upload':
_uploadFile(file);
break;
case 'delete':
_deleteFile(file);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'view',
child: Row(
children: [
Icon(Icons.visibility),
SizedBox(width: 8),
Text('查看'),
],
),
),
const PopupMenuItem(
value: 'upload',
child: Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('立即上传'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('删除', style: TextStyle(color: Colors.red)),
],
),
),
],
),
),
);
}
Widget _buildFileGridItem(LocalFileInfo file) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _viewFile(file),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
_getFileIcon(file.name),
size: 40,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
file.name,
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatFileSize(file.size),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
_formatDate(file.modifiedTime),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
PopupMenuButton<String>(
onSelected: (value) async {
switch (value) {
case 'view':
_viewFile(file);
break;
case 'upload':
_uploadFile(file);
break;
case 'delete':
_deleteFile(file);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'view',
child: Row(
children: [
Icon(Icons.visibility, size: 20),
SizedBox(width: 8),
Text('查看'),
],
),
),
const PopupMenuItem(
value: 'upload',
child: Row(
children: [
Icon(Icons.upload, size: 20),
SizedBox(width: 8),
Text('上传'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 20, color: Colors.red),
SizedBox(width: 8),
Text('删除', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
),
),
),
);
}
IconData _getFileIcon(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
switch (ext) {
case 'txt':
return Icons.description;
case 'pdf':
return Icons.picture_as_pdf;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return Icons.image;
case 'mp4':
case 'avi':
case 'mov':
return Icons.video_file;
case 'mp3':
case 'wav':
case 'flac':
return Icons.audio_file;
default:
return Icons.insert_drive_file;
}
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
void _viewFile(LocalFileInfo file) async {
try {
final content = await File(file.path).readAsString();
if (mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(file.name),
content: SingleChildScrollView(
child: SelectableText(content),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
),
);
}
} catch (e) {
widget.syncService.addLog('无法查看文件: $e', SyncLogType.error);
}
}
void _uploadFile(LocalFileInfo file) async {
if (!widget.syncService.isInitialized) {
widget.syncService.addLog('请先初始化', SyncLogType.warning);
return;
}
try {
await widget.syncService.syncKit.upload(file.path);
widget.syncService.addLog('上传成功: ${file.name}', SyncLogType.success);
} catch (e) {
widget.syncService.addLog('上传失败: $e', SyncLogType.error);
}
}
void _deleteFile(LocalFileInfo file) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除 "${file.name}" 吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
try {
await File(file.path).delete();
widget.syncService
.addLog('已删除: ${file.name}', SyncLogType.info);
await widget.syncService.refreshLocalFiles();
} catch (e) {
widget.syncService.addLog('删除失败: $e', SyncLogType.error);
}
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
);
}
void _showCreateFileDialog() {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('创建新文件'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
labelText: '文件名',
hintText: 'example.txt',
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
if (controller.text.isNotEmpty) {
Navigator.pop(context);
try {
final appDir = await getApplicationDocumentsDirectory();
final file =
File('${appDir.path}/sync_data/${controller.text}');
await file.writeAsString('');
widget.syncService
.addLog('创建文件: ${controller.text}', SyncLogType.success);
await widget.syncService.refreshLocalFiles();
} catch (e) {
widget.syncService.addLog('创建失败: $e', SyncLogType.error);
}
}
},
child: const Text('创建'),
),
],
),
);
}
}
// ==================== 历史页面 ====================
class HistoryPage extends StatelessWidget {
final SyncService syncService;
const HistoryPage({super.key, required this.syncService});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('同步历史'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 600;
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isWide ? 800 : double.infinity,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history,
size: isWide ? 80 : 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(
'同步历史功能开发中',
style: TextStyle(
fontSize: isWide ? 20 : 16,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 8),
Text(
'此功能将在后续版本中添加',
style: TextStyle(
fontSize: isWide ? 16 : 14,
color: Theme.of(context).colorScheme.outlineVariant,
),
),
],
),
),
);
},
),
);
}
}
// ==================== 设置页面(响应式) ====================
class SettingsPage extends StatefulWidget {
final SyncService syncService;
const SettingsPage({super.key, required this.syncService});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
ConflictStrategy _conflictStrategy = ConflictStrategy.smartMerge;
bool _enableChecksum = true;
bool _autoSync = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('设置'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 900;
if (isWide) {
// 宽屏:双列布局
return Row(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _buildSettingsColumn1(),
),
),
const VerticalDivider(width: 1),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _buildSettingsColumn2(),
),
),
],
);
} else {
// 窄屏:单列布局
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSettingsColumn1(),
_buildSettingsColumn2(),
],
),
);
}
},
),
);
}
Widget _buildSettingsColumn1() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
leading: Icon(Icons.sync_alt),
title: Text('同步设置'),
subtitle: Text('配置同步行为'),
),
const Divider(),
ListTile(
title: const Text('冲突解决策略'),
subtitle: Text(_getConflictStrategyText()),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showConflictStrategyDialog(),
),
SwitchListTile(
title: const Text('启用文件校验'),
subtitle: const Text('使用 SHA-256 确保文件完整性'),
value: _enableChecksum,
onChanged: (value) {
setState(() {
_enableChecksum = value;
});
},
),
SwitchListTile(
title: const Text('自动同步'),
subtitle: const Text('应用启动时自动开始同步'),
value: _autoSync,
onChanged: (value) {
setState(() {
_autoSync = value;
});
},
),
],
);
}
Widget _buildSettingsColumn2() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
leading: Icon(Icons.storage),
title: Text('存储设置'),
subtitle: Text('管理本地存储'),
),
const Divider(),
ListTile(
title: const Text('清除所有数据'),
subtitle: const Text('删除所有本地同步数据和缓存'),
leading: const Icon(Icons.delete_forever, color: Colors.red),
textColor: Colors.red,
onTap: () => _showClearDataDialog(),
),
const Divider(),
const ListTile(
leading: Icon(Icons.info),
title: Text('关于'),
subtitle: Text('Flutter Sync Kit 示例应用'),
),
const Divider(),
const ListTile(
title: Text('版本'),
subtitle: Text('1.0.0'),
),
ListTile(
title: const Text('开源协议'),
subtitle: const Text('MIT License'),
onTap: () {
// 显示许可证
},
),
],
);
}
String _getConflictStrategyText() {
switch (_conflictStrategy) {
case ConflictStrategy.localFirst:
return '本地优先';
case ConflictStrategy.remoteFirst:
return '远程优先';
case ConflictStrategy.smartMerge:
return '智能合并';
}
}
void _showConflictStrategyDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择冲突解决策略'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildStrategyOption(
'本地优先',
'本地版本覆盖远程版本',
ConflictStrategy.localFirst,
),
_buildStrategyOption(
'远程优先',
'远程版本覆盖本地版本',
ConflictStrategy.remoteFirst,
),
_buildStrategyOption(
'智能合并',
'根据时间戳自动选择',
ConflictStrategy.smartMerge,
),
],
),
),
);
}
Widget _buildStrategyOption(
String title,
String subtitle,
ConflictStrategy strategy,
) {
final isSelected = _conflictStrategy == strategy;
return ListTile(
title: Text(title),
subtitle: Text(subtitle),
leading: Radio<ConflictStrategy>(
value: strategy,
groupValue: _conflictStrategy,
onChanged: (value) {
setState(() {
_conflictStrategy = value!;
});
Navigator.pop(context);
},
),
selected: isSelected,
onTap: () {
setState(() {
_conflictStrategy = strategy;
});
Navigator.pop(context);
},
);
}
void _showClearDataDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认清除数据'),
content: const Text('此操作将删除所有本地同步数据和缓存,无法恢复。确定要继续吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
try {
await widget.syncService.syncKit.clearAll();
widget.syncService.addLog('已清除所有数据', SyncLogType.info);
await widget.syncService.refreshLocalFiles();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('数据已清除')),
);
}
} catch (e) {
widget.syncService.addLog('清除数据失败: $e', SyncLogType.error);
}
},
child: const Text('清除', style: TextStyle(color: Colors.red)),
),
],
),
);
}
}