主要合入内容:

- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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:
2026-06-04 07:11:43 +08:00
parent 5ee6ba1e28
commit b02daf1448
56 changed files with 7589 additions and 1200 deletions
@@ -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?;
});