flutter_synckit 0.0.3 copy "flutter_synckit: ^0.0.3" to clipboard
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)),
          ),
        ],
      ),
    );
  }
}
0
likes
140
points
83
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

一款集成 WebDAV 的轻量级 Flutter 同步库,支持断点续传、离线优先、冲突处理等高级功能。

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

async, crypto, flutter, http, path, path_provider, shared_preferences, sqflite, stream_transform, xml

More

Packages that depend on flutter_synckit