主要合入内容:

- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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
+78 -14
View File
@@ -1,9 +1,11 @@
import 'package:cloudreve4_flutter/presentation/providers/admin_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/auth_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/download_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/file_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/navigation_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/sync_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/upload_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/user_setting_provider.dart';
import 'package:cloudreve4_flutter/presentation/widgets/announcement_dialog.dart';
import 'package:cloudreve4_flutter/presentation/widgets/gesture_handler_mixin.dart';
import 'package:cloudreve4_flutter/presentation/widgets/glassmorphism_container.dart';
@@ -57,12 +59,18 @@ class _AppShellState extends State<AppShell>
final Set<int> _visitedPageIndexes = <int>{0};
late AnimationController _syncSpinController;
String? _lastClipboardShareId;
String? _lastUserId;
bool _cachedShowSyncTab = false;
bool get _showSyncTab =>
defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS;
static bool _shouldShowSyncTab(double screenWidth) {
if (defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS) {
return true;
}
return screenWidth >= 800;
}
List<Widget> get _pages => _showSyncTab
List<Widget> _pages(bool showSyncTab) => showSyncTab
? [
const OverviewPage(),
const FilesPage(),
@@ -78,7 +86,7 @@ class _AppShellState extends State<AppShell>
];
int _clampedIndex(int index) {
final maxIndex = _pages.length - 1;
final maxIndex = _pages(_cachedShowSyncTab).length - 1;
if (index < 0) return 0;
if (index > maxIndex) return maxIndex;
return index;
@@ -95,6 +103,7 @@ class _AppShellState extends State<AppShell>
WidgetsBinding.instance.addPostFrameCallback((_) {
_showPostLoginAnnouncement();
_checkClipboardShareLink();
_lastUserId = context.read<AuthProvider>().user?.id;
});
}
@@ -112,6 +121,56 @@ class _AppShellState extends State<AppShell>
}
}
void _handleTabSelected(int index) {
final nav = Provider.of<NavigationProvider>(context, listen: false);
nav.setIndex(index);
final userSetting = Provider.of<UserSettingProvider>(
context,
listen: false,
);
if (index == 0 || index == _pages(_cachedShowSyncTab).length - 1) {
userSetting.loadCapacity();
}
}
void _checkUserChange(AuthProvider auth) {
final currentUserId = auth.user?.id;
if (_lastUserId != null && currentUserId != _lastUserId) {
_lastUserId = currentUserId;
Future.microtask(() {
if (mounted) _resetProvidersOnUserChange();
});
} else if (_lastUserId == null && currentUserId != null) {
_lastUserId = currentUserId;
}
}
void _resetProvidersOnUserChange() {
final fileManager = Provider.of<FileManagerProvider>(
context,
listen: false,
);
final userSetting = Provider.of<UserSettingProvider>(
context,
listen: false,
);
final admin = Provider.of<AdminProvider>(context, listen: false);
final sync = Provider.of<SyncProvider>(context, listen: false);
fileManager.clearFiles();
userSetting.clear();
admin.clear();
if (sync.engineInitialized) {
sync.resetSync();
}
final nav = Provider.of<NavigationProvider>(context, listen: false);
nav.setIndex(0);
userSetting.loadCapacity();
}
Future<void> _showPostLoginAnnouncement() async {
final authProvider = context.read<AuthProvider>();
if (!authProvider.isAuthenticated) return;
@@ -193,6 +252,7 @@ class _AppShellState extends State<AppShell>
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth >= 1000;
_cachedShowSyncTab = _shouldShowSyncTab(screenWidth);
return PopScope(
canPop: false,
@@ -217,8 +277,9 @@ class _AppShellState extends State<AppShell>
}
}
},
child: Consumer<NavigationProvider>(
builder: (context, navProvider, _) {
child: Consumer2<AuthProvider, NavigationProvider>(
builder: (context, auth, navProvider, _) {
_checkUserChange(auth);
if (isDesktop) {
return _buildDesktopLayout(context, navProvider);
}
@@ -229,7 +290,7 @@ class _AppShellState extends State<AppShell>
}
Widget _buildPageContent(BuildContext context, int currentIndex) {
final pages = _pages;
final pages = _pages(_cachedShowSyncTab);
final visibleIndex = _clampedIndex(currentIndex);
_visitedPageIndexes.add(visibleIndex);
@@ -297,7 +358,7 @@ class _AppShellState extends State<AppShell>
return NavigationBar(
height: 64,
selectedIndex: _clampedIndex(navProvider.currentIndex),
onDestinationSelected: (i) => navProvider.setIndex(i),
onDestinationSelected: _handleTabSelected,
destinations: [
const NavigationDestination(
icon: Icon(LucideIcons.layoutDashboard),
@@ -322,7 +383,7 @@ class _AppShellState extends State<AppShell>
),
label: '任务',
),
if (_showSyncTab)
if (_cachedShowSyncTab)
NavigationDestination(
icon: Consumer<SyncProvider>(
builder: (context, sync, _) {
@@ -373,15 +434,18 @@ class _AppShellState extends State<AppShell>
children: [
NavigationRail(
selectedIndex: _clampedIndex(navProvider.currentIndex),
onDestinationSelected: (i) => navProvider.setIndex(i),
onDestinationSelected: _handleTabSelected,
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: GestureDetector(
onTap: () => navProvider.setIndex(_pages.length - 1),
onTap: () =>
navProvider.setIndex(_pages(_cachedShowSyncTab).length - 1),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: navProvider.currentIndex == _pages.length - 1
border:
navProvider.currentIndex ==
_pages(_cachedShowSyncTab).length - 1
? Border.all(
color: theme.colorScheme.primary,
width: 2.5,
@@ -424,7 +488,7 @@ class _AppShellState extends State<AppShell>
selectedIcon: const Icon(LucideIcons.listChecks, weight: 700),
label: const Text('任务'),
),
if (_showSyncTab)
if (_cachedShowSyncTab)
NavigationRailDestination(
icon: Consumer<SyncProvider>(
builder: (context, sync, _) {