主要合入内容:
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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。
This commit is contained in:
@@ -63,10 +63,7 @@ class AdminProvider extends ChangeNotifier {
|
||||
Future<void> loadAll() async {
|
||||
_setState(AdminState.loading);
|
||||
try {
|
||||
await Future.wait([
|
||||
loadGroups(),
|
||||
loadUsers(),
|
||||
]);
|
||||
await Future.wait([loadGroups(), loadUsers()]);
|
||||
_setState(AdminState.idle);
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
@@ -213,6 +210,19 @@ class AdminProvider extends ChangeNotifier {
|
||||
|
||||
bool isUserSelected(int userId) => _selectedUserIds.contains(userId);
|
||||
|
||||
/// 清除管理员数据(切换账号时调用)
|
||||
void clear() {
|
||||
_groups = [];
|
||||
_users = [];
|
||||
_groupsPagination = null;
|
||||
_usersPagination = null;
|
||||
_selectedUserIds.clear();
|
||||
_isSelectingUsers = false;
|
||||
_errorMessage = null;
|
||||
_state = AdminState.idle;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setState(AdminState state) {
|
||||
_state = state;
|
||||
notifyListeners();
|
||||
|
||||
@@ -3,7 +3,10 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../data/models/file_model.dart';
|
||||
import '../../services/file_service.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
import '../../services/thumbnail_service.dart';
|
||||
import '../../core/constants/sort_options.dart';
|
||||
import '../../core/constants/storage_keys.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../core/utils/file_utils.dart';
|
||||
|
||||
@@ -15,7 +18,11 @@ class RefreshResult {
|
||||
final int added;
|
||||
final int removed;
|
||||
final int updated;
|
||||
const RefreshResult({required this.added, required this.removed, required this.updated});
|
||||
const RefreshResult({
|
||||
required this.added,
|
||||
required this.removed,
|
||||
required this.updated,
|
||||
});
|
||||
bool get isUnchanged => added == 0 && removed == 0 && updated == 0;
|
||||
}
|
||||
|
||||
@@ -25,8 +32,11 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
List<FileModel> _files = [];
|
||||
List<String> _selectedFiles = [];
|
||||
FileViewType _viewType = FileViewType.list;
|
||||
SortOption _sortOption = SortOption.default_;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingMore = false;
|
||||
bool _hasMore = true;
|
||||
String? _nextPageToken;
|
||||
String? _errorMessage;
|
||||
String? _contextHint;
|
||||
String? _highlightPath;
|
||||
@@ -36,15 +46,21 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
List<FileModel> get files => _files;
|
||||
List<String> get selectedFiles => _selectedFiles;
|
||||
FileViewType get viewType => _viewType;
|
||||
SortOption get sortOption => _sortOption;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
bool get hasMore => _hasMore;
|
||||
String? get nextPageToken => _nextPageToken;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String? get contextHint => _contextHint;
|
||||
bool get hasSelection => _selectedFiles.isNotEmpty;
|
||||
String? get highlightPath => _highlightPath;
|
||||
|
||||
/// 加载文件列表
|
||||
Future<void> loadFiles({bool refresh = false, Duration timeout = const Duration(seconds: 5)}) async {
|
||||
Future<void> loadFiles({
|
||||
bool refresh = false,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
if (refresh) {
|
||||
_selectedFiles.clear();
|
||||
}
|
||||
@@ -52,13 +68,18 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_nextPageToken = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await FileService().listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
).timeout(timeout);
|
||||
final response = await FileService()
|
||||
.listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
orderBy: _sortOption.field.apiKey,
|
||||
orderDirection: _sortOption.direction.apiKey,
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
final List<dynamic> filesData = response['files'] as List<dynamic>? ?? [];
|
||||
final pagination = response['pagination'] as Map<String, dynamic>? ?? {};
|
||||
@@ -67,7 +88,8 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
_files = filesData
|
||||
.map((f) => FileModel.fromJson(f as Map<String, dynamic>))
|
||||
.toList();
|
||||
_hasMore = pagination['next_token'] != null;
|
||||
_nextPageToken = pagination['next_token'] as String?;
|
||||
_hasMore = _nextPageToken != null;
|
||||
_contextHint = response['context_hint'] as String?;
|
||||
});
|
||||
} on TimeoutException {
|
||||
@@ -87,12 +109,62 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载更多文件(分页)
|
||||
Future<void> loadMoreFiles({
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
if (_isLoadingMore || _nextPageToken == null) return;
|
||||
|
||||
setState(() {
|
||||
_isLoadingMore = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await FileService()
|
||||
.listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
orderBy: _sortOption.field.apiKey,
|
||||
orderDirection: _sortOption.direction.apiKey,
|
||||
nextPageToken: _nextPageToken,
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
final List<dynamic> filesData = response['files'] as List<dynamic>? ?? [];
|
||||
final pagination = response['pagination'] as Map<String, dynamic>? ?? {};
|
||||
final newFiles = filesData
|
||||
.map((f) => FileModel.fromJson(f as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
final existingIds = _files.map((e) => e.id).toSet();
|
||||
_files.addAll(newFiles.where((f) => !existingIds.contains(f.id)));
|
||||
_nextPageToken = pagination['next_token'] as String?;
|
||||
_hasMore = _nextPageToken != null;
|
||||
});
|
||||
} on TimeoutException {
|
||||
setState(() {
|
||||
_errorMessage = '加载更多超时,请重试';
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorMessage = e.toString();
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 进入文件夹
|
||||
Future<void> enterFolder(String path) async {
|
||||
_currentPath = path;
|
||||
_selectedFiles.clear();
|
||||
_highlightPath = null;
|
||||
_highlightTimer?.cancel();
|
||||
_nextPageToken = null;
|
||||
ThumbnailService.instance.clearAll();
|
||||
await loadFiles();
|
||||
}
|
||||
@@ -111,6 +183,7 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
_selectedFiles.clear();
|
||||
_highlightPath = null;
|
||||
_highlightTimer?.cancel();
|
||||
_nextPageToken = null;
|
||||
ThumbnailService.instance.clearAll();
|
||||
notifyListeners();
|
||||
await loadFiles();
|
||||
@@ -144,6 +217,30 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 设置排序选项并重新加载
|
||||
Future<void> setSortOption(SortOption option) async {
|
||||
if (_sortOption == option) return;
|
||||
_sortOption = option;
|
||||
notifyListeners();
|
||||
await StorageService.instance.setString(
|
||||
StorageKeys.fileSortOption,
|
||||
option.toKey(),
|
||||
);
|
||||
await loadFiles(refresh: true);
|
||||
}
|
||||
|
||||
/// 从持久化恢复排序偏好
|
||||
Future<void> restoreSortOption() async {
|
||||
final key = await StorageService.instance.getString(
|
||||
StorageKeys.fileSortOption,
|
||||
);
|
||||
final option = SortOption.fromKey(key);
|
||||
if (option != _sortOption) {
|
||||
_sortOption = option;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置错误信息
|
||||
void setErrorMessage(String? message) {
|
||||
_errorMessage = message;
|
||||
@@ -224,7 +321,11 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 移动文件(增量更新)
|
||||
Future<String?> moveFiles(List<String> uris, String destination, {bool copy = false}) async {
|
||||
Future<String?> moveFiles(
|
||||
List<String> uris,
|
||||
String destination, {
|
||||
bool copy = false,
|
||||
}) async {
|
||||
try {
|
||||
await FileService().moveFiles(uris: uris, dst: destination, copy: copy);
|
||||
clearSelection();
|
||||
@@ -253,7 +354,10 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
/// 重命名文件(原地更新,不刷新列表)
|
||||
Future<String?> renameFile(String path, String newName) async {
|
||||
try {
|
||||
final response = await FileService().renameFile(uri: path, newName: newName);
|
||||
final response = await FileService().renameFile(
|
||||
uri: path,
|
||||
newName: newName,
|
||||
);
|
||||
if (response.isEmpty) {
|
||||
await loadFiles();
|
||||
return null;
|
||||
@@ -319,21 +423,29 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
_selectedFiles = [];
|
||||
_currentPath = '/';
|
||||
_errorMessage = null;
|
||||
_nextPageToken = null;
|
||||
_hasMore = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// 智能刷新 - 只更新差异部分
|
||||
Future<RefreshResult> refreshFiles({Duration timeout = const Duration(seconds: 5)}) async {
|
||||
/// 智能刷新 - 只更新差异部分(仅刷新首页)
|
||||
Future<RefreshResult> refreshFiles({
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await FileService().listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
).timeout(timeout);
|
||||
final response = await FileService()
|
||||
.listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
orderBy: _sortOption.field.apiKey,
|
||||
orderDirection: _sortOption.direction.apiKey,
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
final List<dynamic> filesData = response['files'] as List<dynamic>? ?? [];
|
||||
final newFiles = filesData
|
||||
@@ -378,9 +490,11 @@ class FileManagerProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
final pagination = response['pagination'] as Map<String, dynamic>?;
|
||||
setState(() {
|
||||
_files = updatedFiles;
|
||||
_hasMore = response['pagination']?['next_token'] != null;
|
||||
_nextPageToken = pagination?['next_token'] as String?;
|
||||
_hasMore = _nextPageToken != null;
|
||||
_contextHint = response['context_hint'] as String?;
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ enum SyncState {
|
||||
}
|
||||
|
||||
class SyncProvider extends ChangeNotifier {
|
||||
|
||||
SyncState _state = SyncState.idle;
|
||||
String? _errorMessage;
|
||||
SyncSummaryModel? _lastSummary;
|
||||
@@ -48,6 +47,17 @@ class SyncProvider extends ChangeNotifier {
|
||||
// 持久化的同步配置
|
||||
SyncConfigModel? _persistedConfig;
|
||||
|
||||
// 累积统计(跨所有 Worker 实时汇总,不仅仅是 lastSummary)
|
||||
int _cumUploaded = 0;
|
||||
int _cumDownloaded = 0;
|
||||
int _cumRenamed = 0;
|
||||
int _cumMoved = 0;
|
||||
int _cumFailed = 0;
|
||||
int _cumConflicts = 0;
|
||||
int _cumDeletedLocal = 0;
|
||||
int _cumDeletedRemote = 0;
|
||||
int _cumSkipped = 0;
|
||||
|
||||
SyncState get state => _state;
|
||||
String? get errorMessage => _errorMessage;
|
||||
SyncSummaryModel? get lastSummary => _lastSummary;
|
||||
@@ -62,6 +72,70 @@ class SyncProvider extends ChangeNotifier {
|
||||
List<SyncTaskModel> get recentTasks => _recentTasks;
|
||||
int get activeWorkerCount => _activeWorkerCount;
|
||||
|
||||
/// 累积上传数(所有 Worker 汇总)
|
||||
int get cumUploaded => _cumUploaded;
|
||||
|
||||
/// 累积下载数(所有 Worker 汇总)
|
||||
int get cumDownloaded => _cumDownloaded;
|
||||
|
||||
/// 累积重命名数
|
||||
int get cumRenamed => _cumRenamed;
|
||||
|
||||
/// 累积移动数
|
||||
int get cumMoved => _cumMoved;
|
||||
|
||||
/// 累积失败数
|
||||
int get cumFailed => _cumFailed;
|
||||
|
||||
/// 累积冲突数
|
||||
int get cumConflicts => _cumConflicts;
|
||||
|
||||
/// 累积删本地数
|
||||
int get cumDeletedLocal => _cumDeletedLocal;
|
||||
|
||||
/// 累积删远程数
|
||||
int get cumDeletedRemote => _cumDeletedRemote;
|
||||
|
||||
/// 累积跳过数
|
||||
int get cumSkipped => _cumSkipped;
|
||||
|
||||
/// 累积总计操作数
|
||||
int get cumTotal =>
|
||||
_cumUploaded +
|
||||
_cumDownloaded +
|
||||
_cumRenamed +
|
||||
_cumMoved +
|
||||
_cumFailed +
|
||||
_cumConflicts +
|
||||
_cumDeletedLocal +
|
||||
_cumDeletedRemote +
|
||||
_cumSkipped;
|
||||
|
||||
/// 从活跃任务聚合的已完成数
|
||||
int get activeCompletedCount {
|
||||
int sum = 0;
|
||||
for (final t in _activeTasks) {
|
||||
sum += t.completedCount;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// 从活跃任务聚合的总数
|
||||
int get activeTotalCount {
|
||||
int sum = 0;
|
||||
for (final t in _activeTasks) {
|
||||
sum += t.totalCount;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// 从活跃任务聚合的进度(0.0~1.0)
|
||||
double get activeProgress {
|
||||
final total = activeTotalCount;
|
||||
if (total == 0) return 0.0;
|
||||
return activeCompletedCount / total;
|
||||
}
|
||||
|
||||
bool get isActive =>
|
||||
_state == SyncState.initializing ||
|
||||
_state == SyncState.initialSync ||
|
||||
@@ -75,8 +149,7 @@ class SyncProvider extends ChangeNotifier {
|
||||
bool _engineInitialized = false;
|
||||
bool get engineInitialized => _engineInitialized;
|
||||
|
||||
double get progress =>
|
||||
_totalFiles > 0 ? _syncedFiles / _totalFiles : 0.0;
|
||||
double get progress => _totalFiles > 0 ? _syncedFiles / _totalFiles : 0.0;
|
||||
|
||||
/// 从持久化存储恢复同步配置和状态
|
||||
Future<void> restoreFromStorage() async {
|
||||
@@ -89,17 +162,25 @@ class SyncProvider extends ChangeNotifier {
|
||||
refreshToken: configMap['refreshToken'] as String? ?? '',
|
||||
localRoot: configMap['localRoot'] as String? ?? '',
|
||||
remoteRoot: configMap['remoteRoot'] as String? ?? 'cloudreve://my',
|
||||
syncMode: configMap['syncMode'] as String? ?? 'full',
|
||||
conflictStrategy: configMap['conflictStrategy'] as String? ?? 'keep_both',
|
||||
wcfDeleteMode: configMap['wcfDeleteMode'] as String? ?? 'wcf_delete_local_only',
|
||||
maxConcurrentTransfers: configMap['maxConcurrentTransfers'] as int? ?? 3,
|
||||
// 旧版 'album' 迁移为 'album_upload'
|
||||
syncMode: (configMap['syncMode'] as String? ?? 'full') == 'album'
|
||||
? 'album_upload'
|
||||
: (configMap['syncMode'] as String? ?? 'full'),
|
||||
conflictStrategy:
|
||||
configMap['conflictStrategy'] as String? ?? 'keep_both',
|
||||
wcfDeleteMode:
|
||||
configMap['wcfDeleteMode'] as String? ?? 'wcf_delete_local_only',
|
||||
maxConcurrentTransfers:
|
||||
configMap['maxConcurrentTransfers'] as int? ?? 3,
|
||||
bandwidthLimitKbps: configMap['bandwidthLimitKbps'] as int? ?? 0,
|
||||
maxWorkers: configMap['maxWorkers'] as int? ?? 0,
|
||||
dataDir: configMap['dataDir'] as String? ?? '',
|
||||
clientId: configMap['clientId'] as String? ?? '',
|
||||
logLevel: configMap['logLevel'] as String? ?? 'info',
|
||||
);
|
||||
AppLogger.i('恢复同步配置: 模式=${_persistedConfig!.syncMode}, 冲突=${_persistedConfig!.conflictStrategy}, 并发=${_persistedConfig!.maxConcurrentTransfers}, 带宽=${_persistedConfig!.bandwidthLimitKbps}kbps, maxWorkers=${_persistedConfig!.maxWorkers}');
|
||||
AppLogger.i(
|
||||
'恢复同步配置: 模式=${_persistedConfig!.syncMode}, 冲突=${_persistedConfig!.conflictStrategy}, 并发=${_persistedConfig!.maxConcurrentTransfers}, 带宽=${_persistedConfig!.bandwidthLimitKbps}kbps, maxWorkers=${_persistedConfig!.maxWorkers}',
|
||||
);
|
||||
} catch (e) {
|
||||
AppLogger.e('恢复同步配置失败: $e');
|
||||
}
|
||||
@@ -109,6 +190,23 @@ class SyncProvider extends ChangeNotifier {
|
||||
if (savedState != null && savedState != 'idle' && savedState != 'stopped') {
|
||||
AppLogger.i('恢复同步状态: $savedState');
|
||||
}
|
||||
|
||||
// 恢复累积统计
|
||||
final cumStats = await StorageService.instance.getSyncCumStats();
|
||||
if (cumStats != null) {
|
||||
_cumUploaded = cumStats['uploaded'] ?? 0;
|
||||
_cumDownloaded = cumStats['downloaded'] ?? 0;
|
||||
_cumRenamed = cumStats['renamed'] ?? 0;
|
||||
_cumMoved = cumStats['moved'] ?? 0;
|
||||
_cumFailed = cumStats['failed'] ?? 0;
|
||||
_cumConflicts = cumStats['conflicts'] ?? 0;
|
||||
_cumDeletedLocal = cumStats['deleted_local'] ?? 0;
|
||||
_cumDeletedRemote = cumStats['deleted_remote'] ?? 0;
|
||||
_cumSkipped = cumStats['skipped'] ?? 0;
|
||||
AppLogger.i(
|
||||
'恢复累积统计: 上传=$_cumUploaded, 下载=$_cumDownloaded, 失败=$_cumFailed, 冲突=$_cumConflicts, 删本地=$_cumDeletedLocal, 删远程=$_cumDeletedRemote, 跳过=$_cumSkipped',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存同步配置到持久化存储
|
||||
@@ -132,6 +230,47 @@ class SyncProvider extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
/// 持久化累积统计
|
||||
Future<void> _persistCumStats() async {
|
||||
await StorageService.instance.setSyncCumStats({
|
||||
'uploaded': _cumUploaded,
|
||||
'downloaded': _cumDownloaded,
|
||||
'renamed': _cumRenamed,
|
||||
'moved': _cumMoved,
|
||||
'failed': _cumFailed,
|
||||
'conflicts': _cumConflicts,
|
||||
'deleted_local': _cumDeletedLocal,
|
||||
'deleted_remote': _cumDeletedRemote,
|
||||
'skipped': _cumSkipped,
|
||||
});
|
||||
}
|
||||
|
||||
/// 从 Rust DB 加载累积统计(权威数据源)
|
||||
/// 取 DB 和内存中的较大值,避免覆盖事件已递增的增量
|
||||
Future<void> _loadCumStatsFromDb() async {
|
||||
try {
|
||||
final stats = await SyncService.instance.getCumStats();
|
||||
_cumUploaded = _max(_cumUploaded, stats['uploaded'] ?? 0);
|
||||
_cumDownloaded = _max(_cumDownloaded, stats['downloaded'] ?? 0);
|
||||
_cumRenamed = _max(_cumRenamed, stats['renamed'] ?? 0);
|
||||
_cumMoved = _max(_cumMoved, stats['moved'] ?? 0);
|
||||
_cumFailed = _max(_cumFailed, stats['failed'] ?? 0);
|
||||
_cumConflicts = _max(_cumConflicts, stats['conflicts'] ?? 0);
|
||||
_cumDeletedLocal = _max(_cumDeletedLocal, stats['deleted_local'] ?? 0);
|
||||
_cumDeletedRemote = _max(_cumDeletedRemote, stats['deleted_remote'] ?? 0);
|
||||
_cumSkipped = _max(_cumSkipped, stats['skipped'] ?? 0);
|
||||
await _persistCumStats();
|
||||
AppLogger.i(
|
||||
'从 DB 校准累积统计: 上传=$_cumUploaded, 下载=$_cumDownloaded, 失败=$_cumFailed, 冲突=$_cumConflicts, 删本地=$_cumDeletedLocal, 删远程=$_cumDeletedRemote, 跳过=$_cumSkipped',
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
AppLogger.e('从 DB 加载累积统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static int _max(int a, int b) => a > b ? a : b;
|
||||
|
||||
/// 持久化同步状态
|
||||
Future<void> _persistState(SyncState state) async {
|
||||
final stateStr = switch (state) {
|
||||
@@ -153,6 +292,7 @@ class SyncProvider extends ChangeNotifier {
|
||||
_syncedFiles = 0;
|
||||
_totalFiles = 0;
|
||||
_currentFile = null;
|
||||
// 不再清零 cum — 从 DB 加载权威数据
|
||||
notifyListeners();
|
||||
|
||||
// 确保 clientId 已注入(Dart 生成、持久化、全层共享)
|
||||
@@ -166,6 +306,10 @@ class SyncProvider extends ChangeNotifier {
|
||||
try {
|
||||
await SyncService.instance.init(configWithClientId);
|
||||
_engineInitialized = true;
|
||||
|
||||
// 从 DB 加载累积统计(权威数据源,替代 SharedPreferences)
|
||||
await _loadCumStatsFromDb();
|
||||
|
||||
_subscribeEvents();
|
||||
|
||||
_state = SyncState.initialSync;
|
||||
@@ -176,22 +320,28 @@ class SyncProvider extends ChangeNotifier {
|
||||
_startPolling();
|
||||
|
||||
// 启动初始同步(后台运行)
|
||||
SyncService.instance.startInitialSync().then((summary) async {
|
||||
_lastSummary = summary;
|
||||
_state = SyncState.continuous;
|
||||
await _persistState(SyncState.continuous);
|
||||
AppLogger.i('初始同步完成');
|
||||
notifyListeners();
|
||||
SyncService.instance
|
||||
.startInitialSync()
|
||||
.then((summary) async {
|
||||
_lastSummary = summary;
|
||||
// 不再覆盖 cum — TaskItemUpdated 事件已实时递增
|
||||
// 初始同步完成后从 DB 重新校准(避免事件遗漏)
|
||||
await _loadCumStatsFromDb();
|
||||
_state = SyncState.continuous;
|
||||
await _persistState(SyncState.continuous);
|
||||
AppLogger.i('初始同步完成');
|
||||
notifyListeners();
|
||||
|
||||
// 自动启动持续同步
|
||||
SyncService.instance.startContinuousSync();
|
||||
}).catchError((e) async {
|
||||
_state = SyncState.error;
|
||||
_errorMessage = e.toString();
|
||||
await _persistState(SyncState.error);
|
||||
AppLogger.e('初始同步失败: $e');
|
||||
notifyListeners();
|
||||
});
|
||||
// 自动启动持续同步
|
||||
SyncService.instance.startContinuousSync();
|
||||
})
|
||||
.catchError((e) async {
|
||||
_state = SyncState.error;
|
||||
_errorMessage = e.toString();
|
||||
await _persistState(SyncState.error);
|
||||
AppLogger.e('初始同步失败: $e');
|
||||
notifyListeners();
|
||||
});
|
||||
} catch (e) {
|
||||
_state = SyncState.error;
|
||||
_errorMessage = e.toString();
|
||||
@@ -212,7 +362,10 @@ class SyncProvider extends ChangeNotifier {
|
||||
if (_persistedConfig == null) return;
|
||||
|
||||
final savedState = await StorageService.instance.getSyncState();
|
||||
if (savedState == null || savedState == 'idle' || savedState == 'stopped' || savedState == 'error') {
|
||||
if (savedState == null ||
|
||||
savedState == 'idle' ||
|
||||
savedState == 'stopped' ||
|
||||
savedState == 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -283,6 +436,7 @@ class SyncProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
int _pollErrorCount = 0;
|
||||
int _pollCount = 0;
|
||||
|
||||
Future<void> _pollStatus() async {
|
||||
try {
|
||||
@@ -305,7 +459,8 @@ class SyncProvider extends ChangeNotifier {
|
||||
|
||||
// 轮询活跃任务 + 刷新已完成任务
|
||||
try {
|
||||
final newActiveWorkerCount = await SyncService.instance.getActiveWorkerCount();
|
||||
final newActiveWorkerCount = await SyncService.instance
|
||||
.getActiveWorkerCount();
|
||||
final newActiveTasks = await SyncService.instance.getActiveTasksTyped();
|
||||
|
||||
bool changed = newActiveWorkerCount != _activeWorkerCount;
|
||||
@@ -334,6 +489,43 @@ class SyncProvider extends ChangeNotifier {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 每 5 次轮询从 DB 校准累积统计(兜底,防止事件丢失导致 UI 不同步)
|
||||
_pollCount++;
|
||||
if (_pollCount % 5 == 0 && _engineInitialized) {
|
||||
try {
|
||||
final stats = await SyncService.instance.getCumStats();
|
||||
final dbUploaded = stats['uploaded'] ?? 0;
|
||||
final dbDownloaded = stats['downloaded'] ?? 0;
|
||||
final dbRenamed = stats['renamed'] ?? 0;
|
||||
final dbMoved = stats['moved'] ?? 0;
|
||||
final dbFailed = stats['failed'] ?? 0;
|
||||
final dbConflicts = stats['conflicts'] ?? 0;
|
||||
final dbDeletedLocal = stats['deleted_local'] ?? 0;
|
||||
final dbDeletedRemote = stats['deleted_remote'] ?? 0;
|
||||
final dbSkipped = stats['skipped'] ?? 0;
|
||||
if (dbUploaded != _cumUploaded ||
|
||||
dbDownloaded != _cumDownloaded ||
|
||||
dbRenamed != _cumRenamed ||
|
||||
dbMoved != _cumMoved ||
|
||||
dbFailed != _cumFailed ||
|
||||
dbConflicts != _cumConflicts ||
|
||||
dbDeletedLocal != _cumDeletedLocal ||
|
||||
dbDeletedRemote != _cumDeletedRemote ||
|
||||
dbSkipped != _cumSkipped) {
|
||||
_cumUploaded = dbUploaded;
|
||||
_cumDownloaded = dbDownloaded;
|
||||
_cumRenamed = dbRenamed;
|
||||
_cumMoved = dbMoved;
|
||||
_cumFailed = dbFailed;
|
||||
_cumConflicts = dbConflicts;
|
||||
_cumDeletedLocal = dbDeletedLocal;
|
||||
_cumDeletedRemote = dbDeletedRemote;
|
||||
_cumSkipped = dbSkipped;
|
||||
await _persistCumStats();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
_adjustPollInterval();
|
||||
} catch (_) {
|
||||
@@ -439,11 +631,80 @@ class SyncProvider extends ChangeNotifier {
|
||||
_lastSummary = summary;
|
||||
_state = SyncState.continuous;
|
||||
await _persistState(SyncState.continuous);
|
||||
SyncService.instance.startContinuousSync();
|
||||
// 不再在此启动 continuous sync — .then() 回调已启动,避免重复
|
||||
case SyncDiskSpaceWarning():
|
||||
AppLogger.w('磁盘空间不足');
|
||||
case SyncWorkerCompleted():
|
||||
break;
|
||||
case SyncWorkerFailed():
|
||||
break;
|
||||
case SyncTaskItemUpdated(:final taskId, :final action, :final status):
|
||||
AppLogger.d(
|
||||
'[SyncProvider] TaskItemUpdated: taskId=$taskId, action=$action, status=$status',
|
||||
);
|
||||
// 实时更新活跃任务的 completedCount/failedCount
|
||||
final idx = _activeTasks.indexWhere((t) => t.id == taskId);
|
||||
if (idx >= 0) {
|
||||
final old = _activeTasks[idx];
|
||||
if (status == 'completed') {
|
||||
_activeTasks[idx] = SyncTaskModel(
|
||||
id: old.id,
|
||||
trigger: old.trigger,
|
||||
totalCount: old.totalCount,
|
||||
completedCount: old.completedCount + 1,
|
||||
failedCount: old.failedCount,
|
||||
status: old.status,
|
||||
createdAt: old.createdAt,
|
||||
updatedAt: old.updatedAt,
|
||||
finishedAt: old.finishedAt,
|
||||
);
|
||||
} else if (status == 'failed') {
|
||||
_activeTasks[idx] = SyncTaskModel(
|
||||
id: old.id,
|
||||
trigger: old.trigger,
|
||||
totalCount: old.totalCount,
|
||||
completedCount: old.completedCount,
|
||||
failedCount: old.failedCount + 1,
|
||||
status: old.status,
|
||||
createdAt: old.createdAt,
|
||||
updatedAt: old.updatedAt,
|
||||
finishedAt: old.finishedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
// 实时递增累积计数器
|
||||
if (status == 'completed') {
|
||||
switch (action) {
|
||||
case 'upload':
|
||||
_cumUploaded++;
|
||||
AppLogger.d('[SyncProvider] cumUploaded=$_cumUploaded');
|
||||
case 'download':
|
||||
_cumDownloaded++;
|
||||
case 'rename':
|
||||
_cumRenamed++;
|
||||
case 'move':
|
||||
_cumMoved++;
|
||||
case 'conflict_resolve':
|
||||
_cumConflicts++;
|
||||
case 'delete_local':
|
||||
_cumDeletedLocal++;
|
||||
case 'delete_remote':
|
||||
_cumDeletedRemote++;
|
||||
case 'create_placeholder':
|
||||
break;
|
||||
default:
|
||||
AppLogger.w(
|
||||
'[SyncProvider] TaskItemUpdated: 未知 action=$action',
|
||||
);
|
||||
}
|
||||
} else if (status == 'skipped') {
|
||||
_cumSkipped++;
|
||||
} else if (status == 'failed') {
|
||||
_cumFailed++;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
_persistCumStats();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -554,11 +815,11 @@ class SyncProvider extends ChangeNotifier {
|
||||
_adjustPollInterval();
|
||||
}
|
||||
|
||||
/// 重置同步:停止任务 → 清空 DB → 清空本地目录 → 回到初始状态
|
||||
Future<void> resetSync() async {
|
||||
/// 重置同步:停止任务 → 清空 DB → (可选)清空本地目录 → 回到初始状态
|
||||
Future<void> resetSync({bool deleteLocalFiles = true}) async {
|
||||
// 引擎未初始化时仅清空本地状态
|
||||
try {
|
||||
await SyncService.instance.resetSync();
|
||||
await SyncService.instance.resetSync(deleteLocalFiles: deleteLocalFiles);
|
||||
} catch (_) {}
|
||||
_state = SyncState.idle;
|
||||
_errorMessage = null;
|
||||
@@ -569,6 +830,16 @@ class SyncProvider extends ChangeNotifier {
|
||||
_downloadingCount = 0;
|
||||
_currentFile = null;
|
||||
_lastSummary = null;
|
||||
_cumUploaded = 0;
|
||||
_cumDownloaded = 0;
|
||||
_cumRenamed = 0;
|
||||
_cumMoved = 0;
|
||||
_cumFailed = 0;
|
||||
_cumConflicts = 0;
|
||||
_cumDeletedLocal = 0;
|
||||
_cumDeletedRemote = 0;
|
||||
_cumSkipped = 0;
|
||||
_persistCumStats();
|
||||
_activeTasks = [];
|
||||
_recentTasks = [];
|
||||
_activeWorkerCount = 0;
|
||||
|
||||
@@ -46,10 +46,7 @@ class UserSettingProvider extends ChangeNotifier {
|
||||
|
||||
/// 同时加载设置和容量
|
||||
Future<void> loadAll() async {
|
||||
await Future.wait([
|
||||
loadSettings(),
|
||||
loadCapacity(),
|
||||
]);
|
||||
await Future.wait([loadSettings(), loadCapacity()]);
|
||||
}
|
||||
|
||||
/// 修改昵称
|
||||
@@ -244,6 +241,15 @@ class UserSettingProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 清除用户数据(切换账号时调用)
|
||||
void clear() {
|
||||
_settings = null;
|
||||
_capacity = null;
|
||||
_errorMessage = null;
|
||||
_state = UserSettingState.idle;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setState(UserSettingState state) {
|
||||
_state = state;
|
||||
notifyListeners();
|
||||
|
||||
Reference in New Issue
Block a user