b02daf1448
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、Ctrl/Cmd+F搜索、Esc 关闭搜索、鼠标右滑返回上级。 - 移动端同步状态页、同步统计卡片、同步任务/累计统计事件、同步页布局更新。 - Android 相册同步默认路径改为 DCIM/Camera,下载路径改用 external_path。 - Rust sync-core 更新:Android 日志依赖、AlbumUpload/AlbumDownload、MirrorWcf 统计修复、Linux FUSE镜像挂载与读写同步支持。 - 依赖更新:external_path、fl_chart、pdfrx 升级,并修了 pdfrx 新版本的deprecated API。 - 新增文件包括:sort_options.dart、sync_page_android.dart、sync_stats_card.dart、platform/fuse.rs、sync_engine/fuse.rs。
769 lines
25 KiB
Dart
769 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../../data/models/sync_task_model.dart';
|
|
import '../../providers/sync_provider.dart';
|
|
import '../../widgets/sync_stats_card.dart';
|
|
import 'sync_settings_page.dart';
|
|
|
|
/// 移动端同步详情页面 - 展示实时同步状态和任务列表
|
|
class SyncPageAndroid extends StatefulWidget {
|
|
const SyncPageAndroid({super.key});
|
|
|
|
@override
|
|
State<SyncPageAndroid> createState() => _SyncPageAndroidState();
|
|
}
|
|
|
|
class _SyncPageAndroidState extends State<SyncPageAndroid> {
|
|
final Set<String> _expandedTasks = {};
|
|
final Set<String> _loadingDetails = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.read<SyncProvider>().loadRecentTasks();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final sync = context.watch<SyncProvider>();
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('文件同步'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.settings_outlined),
|
|
onPressed: () => _navigateToSettings(),
|
|
tooltip: '同步设置',
|
|
),
|
|
],
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: () async {
|
|
final sync = context.read<SyncProvider>();
|
|
sync.invalidateAllTaskDetails();
|
|
await sync.loadRecentTasks();
|
|
},
|
|
child: CustomScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
child: _buildHeader(sync, theme),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
|
child: SyncStatsCard(
|
|
uploaded: sync.cumUploaded,
|
|
downloaded: sync.cumDownloaded,
|
|
renamed: sync.cumRenamed,
|
|
moved: sync.cumMoved,
|
|
conflicts: sync.cumConflicts,
|
|
failed: sync.cumFailed,
|
|
deletedLocal: sync.cumDeletedLocal,
|
|
deletedRemote: sync.cumDeletedRemote,
|
|
skipped: sync.cumSkipped,
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.sync_outlined,
|
|
size: 18,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'同步任务',
|
|
style: theme.textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
_buildTaskList(sync, theme),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 100)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _navigateToSettings() {
|
|
Navigator.of(
|
|
context,
|
|
).push(MaterialPageRoute(builder: (_) => const SyncSettingsPage()));
|
|
}
|
|
|
|
Widget _buildHeader(SyncProvider sync, ThemeData theme) {
|
|
final isActive = sync.isActive;
|
|
final isPaused = sync.isPaused;
|
|
final hasError = sync.hasError;
|
|
|
|
Color statusColor;
|
|
String statusText;
|
|
|
|
if (isActive) {
|
|
statusColor = theme.colorScheme.primary;
|
|
statusText = _syncModeLabel(sync);
|
|
} else if (isPaused) {
|
|
statusColor = Colors.orange;
|
|
statusText = '已暂停';
|
|
} else if (hasError) {
|
|
statusColor = theme.colorScheme.error;
|
|
statusText = '同步错误';
|
|
} else if (sync.state == SyncState.stopped) {
|
|
statusColor = theme.disabledColor;
|
|
statusText = '已停止';
|
|
} else {
|
|
statusColor = theme.disabledColor;
|
|
statusText = '未启动';
|
|
}
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
theme.colorScheme.primaryContainer.withValues(alpha: 0.3),
|
|
theme.colorScheme.tertiaryContainer.withValues(alpha: 0.3),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 顶部:状态文字居中
|
|
Center(
|
|
child: Text(
|
|
statusText,
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: statusColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 中间:左侧旋转圆 + 右侧统计
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// 左侧:旋转圆形指示器
|
|
SizedBox(
|
|
width: 96,
|
|
height: 96,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
if (isActive)
|
|
SizedBox(
|
|
width: 96,
|
|
height: 96,
|
|
child: CircularProgressIndicator(
|
|
value: sync.activeTotalCount > 0
|
|
? sync.activeProgress
|
|
: null,
|
|
strokeWidth: 6,
|
|
strokeCap: StrokeCap.round,
|
|
backgroundColor:
|
|
theme.colorScheme.surfaceContainerHighest,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
statusColor,
|
|
),
|
|
),
|
|
)
|
|
else
|
|
SizedBox(
|
|
width: 96,
|
|
height: 96,
|
|
child: CircularProgressIndicator(
|
|
value: 0,
|
|
strokeWidth: 6,
|
|
backgroundColor:
|
|
theme.colorScheme.surfaceContainerHighest,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
statusColor,
|
|
),
|
|
),
|
|
),
|
|
// 中心文字
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (isActive && sync.activeTotalCount > 0)
|
|
Text(
|
|
'${(sync.activeProgress * 100).toInt()}%',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: statusColor,
|
|
),
|
|
),
|
|
Text(
|
|
isActive
|
|
? (sync.state == SyncState.continuous
|
|
? '持续同步'
|
|
: '同步中')
|
|
: isPaused
|
|
? '已暂停'
|
|
: hasError
|
|
? '错误'
|
|
: '未启动',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.hintColor,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 24),
|
|
// 右侧:统计行
|
|
IntrinsicWidth(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildStatRow(
|
|
Icons.file_upload_outlined,
|
|
'${sync.cumUploaded}',
|
|
'已上传',
|
|
Colors.blue,
|
|
theme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildStatRow(
|
|
Icons.file_download_outlined,
|
|
'${sync.cumDownloaded}',
|
|
'已下载',
|
|
Colors.green,
|
|
theme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildStatRow(
|
|
Icons.warning_amber_outlined,
|
|
'${sync.cumConflicts}',
|
|
'冲突',
|
|
Colors.orange,
|
|
theme,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (isActive && sync.currentFile != null) ...[
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
sync.currentFile!,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.hintColor,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
),
|
|
],
|
|
if (isActive && sync.activeTotalCount > 0) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'${sync.activeCompletedCount} / ${sync.activeTotalCount} 文件',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.hintColor,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 16),
|
|
// 操作按钮
|
|
Row(
|
|
children: [
|
|
if (!sync.isActive && !sync.isPaused)
|
|
Expanded(
|
|
child: FilledButton.icon(
|
|
onPressed: () => _navigateToSettings(),
|
|
icon: const Icon(Icons.play_arrow, size: 18),
|
|
label: const Text('开始同步'),
|
|
),
|
|
),
|
|
if (sync.isActive)
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => sync.pause(),
|
|
icon: const Icon(Icons.pause, size: 18),
|
|
label: const Text('暂停'),
|
|
),
|
|
),
|
|
if (sync.isPaused)
|
|
Expanded(
|
|
child: FilledButton.icon(
|
|
onPressed: () => sync.resume(),
|
|
icon: const Icon(Icons.play_arrow, size: 18),
|
|
label: const Text('恢复'),
|
|
),
|
|
),
|
|
if (sync.isActive || isPaused) ...[
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _stopSync(sync),
|
|
icon: const Icon(Icons.stop, size: 18),
|
|
label: const Text('停止'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: theme.colorScheme.error,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatRow(
|
|
IconData icon,
|
|
String value,
|
|
String label,
|
|
Color color,
|
|
ThemeData theme,
|
|
) {
|
|
return Row(
|
|
children: [
|
|
Icon(icon, size: 16, color: color),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
value,
|
|
style: theme.textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
label,
|
|
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTaskList(SyncProvider sync, ThemeData theme) {
|
|
// 活跃任务 + 已完成任务
|
|
final activeTasks = sync.activeTasks;
|
|
final completedTasks = sync.recentTasks
|
|
.where(
|
|
(t) =>
|
|
(t.status == 'completed' ||
|
|
t.status == 'failed' ||
|
|
t.status == 'cancelled') &&
|
|
t.totalCount > 0,
|
|
)
|
|
.toList();
|
|
final allTasks = [...activeTasks, ...completedTasks];
|
|
|
|
if (allTasks.isEmpty) {
|
|
return SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.cloud_off, size: 48, color: theme.hintColor),
|
|
const SizedBox(height: 12),
|
|
Text('暂无同步任务', style: TextStyle(color: theme.hintColor)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
sliver: SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) => _buildTaskCard(allTasks[index], sync, theme),
|
|
childCount: allTasks.length,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTaskCard(
|
|
SyncTaskModel task,
|
|
SyncProvider sync,
|
|
ThemeData theme,
|
|
) {
|
|
final isExpanded = _expandedTasks.contains(task.id);
|
|
final isRunning = task.status == 'running';
|
|
final isFailed = task.status == 'failed';
|
|
final isCompleted = task.status == 'completed';
|
|
|
|
Color statusColor = switch (task.status) {
|
|
'running' => theme.colorScheme.primary,
|
|
'completed' => Colors.green,
|
|
'failed' => theme.colorScheme.error,
|
|
'cancelled' => theme.hintColor,
|
|
_ => theme.hintColor,
|
|
};
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.03),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
InkWell(
|
|
onTap: () => _toggleTaskExpand(task.id),
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
// 状态图标
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: statusColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Icon(
|
|
isRunning
|
|
? Icons.sync
|
|
: isFailed
|
|
? Icons.error_outline
|
|
: isCompleted
|
|
? Icons.check_circle_outline
|
|
: Icons.cloud_off,
|
|
color: statusColor,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
// 任务信息
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: statusColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
task.statusLabel,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: statusColor,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
task.triggerLabel,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.hintColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
// 进度条
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: isRunning && task.totalCount > 0
|
|
? task.progress
|
|
: (isCompleted ? 1.0 : null),
|
|
minHeight: 4,
|
|
backgroundColor:
|
|
theme.colorScheme.surfaceContainerHighest,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
statusColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'${task.completedCount}/${task.totalCount}',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
if (task.failedCount > 0)
|
|
Text(
|
|
'失败${task.failedCount}',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
fontSize: 11,
|
|
color: theme.colorScheme.error,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Icon(
|
|
isExpanded ? Icons.expand_less : Icons.expand_more,
|
|
size: 20,
|
|
color: theme.hintColor,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (isExpanded) _buildTaskDetailList(task, sync, theme),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTaskDetailList(
|
|
SyncTaskModel task,
|
|
SyncProvider sync,
|
|
ThemeData theme,
|
|
) {
|
|
final items = sync.getCachedTaskDetail(task.id);
|
|
|
|
if (_loadingDetails.contains(task.id)) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (items == null || items.isEmpty) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Text('暂无任务项', style: TextStyle(color: theme.hintColor)),
|
|
);
|
|
}
|
|
|
|
final hasMore = sync.hasMoreTaskDetail(task.id);
|
|
|
|
return Column(
|
|
children: [
|
|
Divider(
|
|
height: 1,
|
|
indent: 16,
|
|
endIndent: 16,
|
|
color: theme.dividerColor,
|
|
),
|
|
...items.map((item) => _buildTaskItemTile(item, theme)),
|
|
if (hasMore)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: TextButton.icon(
|
|
onPressed: () => _loadMoreDetail(task.id),
|
|
icon: const Icon(Icons.expand_more, size: 16),
|
|
label: const Text('加载更多'),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTaskItemTile(SyncTaskItemModel item, ThemeData theme) {
|
|
Color actionColor = switch (item.actionType) {
|
|
'upload' => Colors.blue,
|
|
'download' => Colors.green,
|
|
'create_placeholder' => Colors.teal,
|
|
'delete_local' || 'delete_remote' => theme.colorScheme.error,
|
|
'rename' || 'move' => Colors.orange,
|
|
'conflict_resolve' => Colors.purple,
|
|
_ => theme.colorScheme.primary,
|
|
};
|
|
|
|
Color itemStatusColor = switch (item.status) {
|
|
'completed' => Colors.green,
|
|
'failed' => theme.colorScheme.error,
|
|
'running' => theme.colorScheme.primary,
|
|
_ => theme.hintColor,
|
|
};
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
// 操作类型图标
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: actionColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
item.actionType == 'upload'
|
|
? Icons.file_upload_outlined
|
|
: item.actionType == 'download'
|
|
? Icons.file_download_outlined
|
|
: item.actionType == 'delete_local' ||
|
|
item.actionType == 'delete_remote'
|
|
? Icons.delete_outline
|
|
: item.actionType == 'rename'
|
|
? Icons.edit_outlined
|
|
: item.actionType == 'move'
|
|
? Icons.drive_file_move_outline
|
|
: Icons.sync_outlined,
|
|
color: actionColor,
|
|
size: 14,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
// 文件名 + 状态
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.filename,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
),
|
|
if (item.errorMessage != null)
|
|
Text(
|
|
item.errorMessage!,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
fontSize: 10,
|
|
color: theme.colorScheme.error,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// 状态标签
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: itemStatusColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
item.statusLabel,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
fontSize: 10,
|
|
color: itemStatusColor,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _syncModeLabel(SyncProvider sync) {
|
|
final mode = sync.persistedConfig?.syncMode ?? 'full';
|
|
return switch (mode) {
|
|
'full' => '全量同步中',
|
|
'upload_only' => '仅上传中',
|
|
'download_only' => '仅下载中',
|
|
'album_upload' => '相册上传中',
|
|
'album_download' => '相册下载中',
|
|
'mirror_wcf' => '镜像同步中',
|
|
_ => '同步中',
|
|
};
|
|
}
|
|
|
|
void _toggleTaskExpand(String taskId) {
|
|
final sync = context.read<SyncProvider>();
|
|
if (_expandedTasks.contains(taskId)) {
|
|
setState(() => _expandedTasks.remove(taskId));
|
|
sync.unwatchTaskDetail(taskId);
|
|
return;
|
|
}
|
|
|
|
setState(() => _expandedTasks.add(taskId));
|
|
sync.watchTaskDetail(taskId);
|
|
|
|
if (sync.getCachedTaskDetail(taskId) == null &&
|
|
!_loadingDetails.contains(taskId)) {
|
|
setState(() => _loadingDetails.add(taskId));
|
|
sync.getTaskDetail(taskId).whenComplete(() {
|
|
if (mounted) {
|
|
setState(() => _loadingDetails.remove(taskId));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void _loadMoreDetail(String taskId) {
|
|
context.read<SyncProvider>().loadMoreTaskDetail(taskId);
|
|
}
|
|
|
|
Future<void> _stopSync(SyncProvider sync) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('停止同步'),
|
|
content: const Text('确定要停止文件同步吗?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('取消'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: Theme.of(ctx).colorScheme.error,
|
|
),
|
|
child: const Text('停止'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true) {
|
|
await sync.stop();
|
|
}
|
|
}
|
|
}
|