主要合入内容:
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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:
@@ -4,10 +4,12 @@ import 'dart:ui';
|
||||
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:cloudreve4_flutter/data/models/file_model.dart';
|
||||
import 'package:cloudreve4_flutter/services/file_service.dart';
|
||||
import 'package:cloudreve4_flutter/services/upload_service.dart';
|
||||
import '../../../core/utils/file_utils.dart';
|
||||
import '../../../core/constants/sort_options.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -41,6 +43,8 @@ class _FilesPageState extends State<FilesPage> {
|
||||
bool _isFirstLoad = true;
|
||||
FileModel? _infoFile;
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final ScrollController _breadcrumbController = ScrollController();
|
||||
|
||||
// FAB 状态
|
||||
bool _isFabVisible = true;
|
||||
@@ -50,24 +54,37 @@ class _FilesPageState extends State<FilesPage> {
|
||||
// 桌面端拖拽状态
|
||||
bool _isDraggingOver = false;
|
||||
|
||||
// 滑动手势追踪
|
||||
Offset? _swipeStartPos;
|
||||
DateTime? _swipeStartTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScrollForPagination);
|
||||
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth >= 1000) {
|
||||
fileManager.setViewType(FileViewType.grid);
|
||||
} else {
|
||||
fileManager.setViewType(FileViewType.list);
|
||||
}
|
||||
fileManager.restoreSortOption();
|
||||
if (_isFirstLoad) {
|
||||
fileManager.loadFiles();
|
||||
_isFirstLoad = false;
|
||||
}
|
||||
final downloadManager = Provider.of<DownloadManagerProvider>(context, listen: false);
|
||||
final downloadManager = Provider.of<DownloadManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
downloadManager.initialize();
|
||||
}
|
||||
});
|
||||
@@ -75,8 +92,13 @@ class _FilesPageState extends State<FilesPage> {
|
||||
// 上传完成 → 自动刷新当前目录文件列表
|
||||
UploadService.instance.onUploadCompleted = (targetPath, fileName) {
|
||||
if (!mounted) return;
|
||||
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
|
||||
final normalizedCurrent = FileUtils.toCloudreveUri(fileManager.currentPath);
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final normalizedCurrent = FileUtils.toCloudreveUri(
|
||||
fileManager.currentPath,
|
||||
);
|
||||
if (targetPath == normalizedCurrent) {
|
||||
final fileUri = targetPath.endsWith('/')
|
||||
? '$targetPath$fileName'
|
||||
@@ -88,11 +110,74 @@ class _FilesPageState extends State<FilesPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_onScrollForPagination);
|
||||
_scrollController.dispose();
|
||||
_breadcrumbController.dispose();
|
||||
_fabShowTimer?.cancel();
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
UploadService.instance.onUploadCompleted = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScrollForPagination() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!fileManager.hasMore ||
|
||||
fileManager.isLoadingMore ||
|
||||
fileManager.isLoading) {
|
||||
return;
|
||||
}
|
||||
final position = _scrollController.position;
|
||||
if (position.pixels >= position.maxScrollExtent - 320) {
|
||||
fileManager.loadMoreFiles();
|
||||
}
|
||||
}
|
||||
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
if (!mounted || event is! KeyDownEvent) return false;
|
||||
// Ctrl+F / Cmd+F → 打开搜索
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyF &&
|
||||
(HardwareKeyboard.instance.isControlPressed ||
|
||||
HardwareKeyboard.instance.isMetaPressed)) {
|
||||
if (ModalRoute.of(context)?.isCurrent == false) return false;
|
||||
final nav = Provider.of<NavigationProvider>(context, listen: false);
|
||||
if (nav.currentIndex == 1 && !SearchDialog.isShowing) {
|
||||
SearchDialog.show(context);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onPointerDown(PointerDownEvent event) {
|
||||
_swipeStartPos = event.position;
|
||||
_swipeStartTime = DateTime.now();
|
||||
}
|
||||
|
||||
void _onPointerUp(PointerUpEvent event) {
|
||||
if (_swipeStartPos == null || _swipeStartTime == null) return;
|
||||
final dx = event.position.dx - _swipeStartPos!.dx;
|
||||
final dy = event.position.dy - _swipeStartPos!.dy;
|
||||
final duration = DateTime.now().difference(_swipeStartTime!);
|
||||
_swipeStartPos = null;
|
||||
_swipeStartTime = null;
|
||||
// 从右往左快速滑动 → 返回上一级
|
||||
if (dx < -150 &&
|
||||
dx.abs() > dy.abs() * 1.5 &&
|
||||
duration.inMilliseconds < 500) {
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (fileManager.currentPath != '/') {
|
||||
fileManager.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showFileInfo(FileModel file) {
|
||||
setState(() => _infoFile = file);
|
||||
// 等待下一帧 rebuild 完成,endDrawer 从 null 变为非 null 后再打开
|
||||
@@ -101,10 +186,7 @@ class _FilesPageState extends State<FilesPage> {
|
||||
});
|
||||
}
|
||||
|
||||
void _showSelectionMore(
|
||||
FileModel file,
|
||||
FileManagerProvider fileManager,
|
||||
) {
|
||||
void _showSelectionMore(FileModel file, FileManagerProvider fileManager) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
@@ -116,7 +198,11 @@ class _FilesPageState extends State<FilesPage> {
|
||||
title: const Text('重命名'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
FileOperationDialogs.showRenameDialog(context, fileManager, file);
|
||||
FileOperationDialogs.showRenameDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -179,13 +265,17 @@ class _FilesPageState extends State<FilesPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = MediaQuery.of(context).size.width >= 1000;
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: _buildAppBar(context),
|
||||
body: _buildBody(context),
|
||||
bottomNavigationBar: _buildBottomBar(context),
|
||||
endDrawer: _infoFile != null ? FileInfoPanel(file: _infoFile!) : null,
|
||||
floatingActionButton: isDesktop ? null : _buildSpeedDialFAB(context),
|
||||
return Listener(
|
||||
onPointerDown: _onPointerDown,
|
||||
onPointerUp: _onPointerUp,
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: _buildAppBar(context),
|
||||
body: _buildBody(context),
|
||||
bottomNavigationBar: _buildBottomBar(context),
|
||||
endDrawer: _infoFile != null ? FileInfoPanel(file: _infoFile!) : null,
|
||||
floatingActionButton: isDesktop ? null : _buildSpeedDialFAB(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,7 +290,13 @@ class _FilesPageState extends State<FilesPage> {
|
||||
if (fileManager.currentPath == '/') {
|
||||
return const Text('文件');
|
||||
}
|
||||
return _buildDesktopBreadcrumb(context, fileManager);
|
||||
final segments = fileManager.currentPath
|
||||
.split('/')
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
return Text(
|
||||
segments.isNotEmpty ? _decodePathSegment(segments.last) : '文件',
|
||||
);
|
||||
}
|
||||
return _buildMobileBreadcrumb(context, fileManager);
|
||||
},
|
||||
@@ -224,60 +320,30 @@ class _FilesPageState extends State<FilesPage> {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
Widget _buildDesktopBreadcrumb(BuildContext context, FileManagerProvider fileManager) {
|
||||
Widget _buildMobileBreadcrumb(
|
||||
BuildContext context,
|
||||
FileManagerProvider fileManager,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final pathParts = fileManager.currentPath.split('/');
|
||||
pathParts.removeWhere((part) => part.isEmpty);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => fileManager.currentPath != '/' ? fileManager.enterFolder('/') : null,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Icon(LucideIcons.home, size: 18, color: colorScheme.primary),
|
||||
),
|
||||
),
|
||||
for (int i = 0; i < pathParts.length; i++) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Icon(LucideIcons.chevronRight, size: 14, color: theme.hintColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final targetPath = '/${pathParts.sublist(0, i + 1).join('/')}';
|
||||
if (targetPath != fileManager.currentPath) fileManager.enterFolder(targetPath);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Text(
|
||||
_decodePathSegment(pathParts[i]),
|
||||
style: TextStyle(
|
||||
color: i == pathParts.length - 1 ? colorScheme.onSurface : colorScheme.primary,
|
||||
fontWeight: i == pathParts.length - 1 ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileBreadcrumb(BuildContext context, FileManagerProvider fileManager) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final pathParts = fileManager.currentPath.split('/');
|
||||
pathParts.removeWhere((part) => part.isEmpty);
|
||||
// 路径变化后自动滚动到末尾
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_breadcrumbController.hasClients) {
|
||||
_breadcrumbController.animateTo(
|
||||
_breadcrumbController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
key: const PageStorageKey('mobile_breadcrumb'),
|
||||
controller: _breadcrumbController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
_buildBreadcrumbChip(
|
||||
@@ -285,12 +351,18 @@ class _FilesPageState extends State<FilesPage> {
|
||||
label: '文件',
|
||||
icon: LucideIcons.home,
|
||||
color: colorScheme.primary,
|
||||
onTap: () => fileManager.currentPath != '/' ? fileManager.enterFolder('/') : null,
|
||||
onTap: () => fileManager.currentPath != '/'
|
||||
? fileManager.enterFolder('/')
|
||||
: null,
|
||||
),
|
||||
for (int i = 0; i < pathParts.length; i++) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Icon(LucideIcons.chevronRight, size: 14, color: theme.hintColor.withValues(alpha: 0.5)),
|
||||
child: Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 14,
|
||||
color: theme.hintColor.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
_buildBreadcrumbChip(
|
||||
context,
|
||||
@@ -300,7 +372,9 @@ class _FilesPageState extends State<FilesPage> {
|
||||
isLast: i == pathParts.length - 1,
|
||||
onTap: () {
|
||||
final targetPath = '/${pathParts.sublist(0, i + 1).join('/')}';
|
||||
if (targetPath != fileManager.currentPath) fileManager.enterFolder(targetPath);
|
||||
if (targetPath != fileManager.currentPath) {
|
||||
fileManager.enterFolder(targetPath);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -324,7 +398,9 @@ class _FilesPageState extends State<FilesPage> {
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isLast ? color.withValues(alpha: 0.15) : color.withValues(alpha: 0.06),
|
||||
color: isLast
|
||||
? color.withValues(alpha: 0.15)
|
||||
: color.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Row(
|
||||
@@ -358,12 +434,19 @@ class _FilesPageState extends State<FilesPage> {
|
||||
Consumer<FileManagerProvider>(
|
||||
builder: (context, fileManager, child) {
|
||||
return IconButton(
|
||||
icon: Icon(fileManager.isLoading ? Icons.hourglass_empty : Icons.refresh),
|
||||
icon: Icon(
|
||||
fileManager.isLoading ? Icons.hourglass_empty : Icons.refresh,
|
||||
),
|
||||
onPressed: () => fileManager.refreshFiles(),
|
||||
tooltip: '刷新',
|
||||
);
|
||||
},
|
||||
),
|
||||
Consumer<FileManagerProvider>(
|
||||
builder: (context, fileManager, child) {
|
||||
return _buildSortMenu(fileManager);
|
||||
},
|
||||
),
|
||||
Consumer<FileManagerProvider>(
|
||||
builder: (context, fileManager, child) {
|
||||
final icon = fileManager.viewType == FileViewType.list
|
||||
@@ -378,14 +461,19 @@ class _FilesPageState extends State<FilesPage> {
|
||||
: FileViewType.list,
|
||||
);
|
||||
},
|
||||
tooltip: fileManager.viewType == FileViewType.list ? '网格视图' : '列表视图',
|
||||
tooltip: fileManager.viewType == FileViewType.list
|
||||
? '网格视图'
|
||||
: '列表视图',
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
FileOperationDialogs.showCreateDialog(context, fileManager);
|
||||
},
|
||||
tooltip: '新建',
|
||||
@@ -397,7 +485,8 @@ class _FilesPageState extends State<FilesPage> {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cloud_download),
|
||||
onPressed: () => Provider.of<NavigationProvider>(context, listen: false).setIndex(2),
|
||||
onPressed: () =>
|
||||
Provider.of<NavigationProvider>(context, listen: false).setIndex(2),
|
||||
tooltip: '下载',
|
||||
),
|
||||
];
|
||||
@@ -405,6 +494,11 @@ class _FilesPageState extends State<FilesPage> {
|
||||
|
||||
List<Widget> _buildMobileActions() {
|
||||
return [
|
||||
Consumer<FileManagerProvider>(
|
||||
builder: (context, fileManager, child) {
|
||||
return _buildSortMenu(fileManager);
|
||||
},
|
||||
),
|
||||
Consumer<FileManagerProvider>(
|
||||
builder: (context, fileManager, child) {
|
||||
final icon = fileManager.viewType == FileViewType.list
|
||||
@@ -419,13 +513,52 @@ class _FilesPageState extends State<FilesPage> {
|
||||
: FileViewType.list,
|
||||
);
|
||||
},
|
||||
tooltip: fileManager.viewType == FileViewType.list ? '网格视图' : '列表视图',
|
||||
tooltip: fileManager.viewType == FileViewType.list
|
||||
? '网格视图'
|
||||
: '列表视图',
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildSortMenu(FileManagerProvider fileManager) {
|
||||
final allOptions = [
|
||||
for (final field in SortField.values)
|
||||
for (final dir in SortDirection.values) SortOption(field, dir),
|
||||
];
|
||||
|
||||
return PopupMenuButton<SortOption>(
|
||||
icon: const Icon(LucideIcons.arrowUpDown),
|
||||
tooltip: '排序',
|
||||
position: PopupMenuPosition.under,
|
||||
onSelected: (option) => fileManager.setSortOption(option),
|
||||
itemBuilder: (context) => allOptions.map((option) {
|
||||
final isSelected = fileManager.sortOption == option;
|
||||
return PopupMenuItem<SortOption>(
|
||||
value: option,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 24),
|
||||
Text(option.menuLabel),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- SpeedDial FAB ----
|
||||
|
||||
Widget _buildSpeedDialFAB(BuildContext context) {
|
||||
@@ -471,8 +604,16 @@ class _FilesPageState extends State<FilesPage> {
|
||||
isDark: isDark,
|
||||
colorScheme: colorScheme,
|
||||
onTap: () {
|
||||
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
|
||||
_onFabSubAction(() => FileOperationDialogs.showCreateDialog(context, fileManager));
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
_onFabSubAction(
|
||||
() => FileOperationDialogs.showCreateDialog(
|
||||
context,
|
||||
fileManager,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildFabSubItem(
|
||||
@@ -482,7 +623,10 @@ class _FilesPageState extends State<FilesPage> {
|
||||
label: '离线下载',
|
||||
isDark: isDark,
|
||||
colorScheme: colorScheme,
|
||||
onTap: () => _onFabSubAction(() => Navigator.of(context).pushNamed(RouteNames.remoteDownload)),
|
||||
onTap: () => _onFabSubAction(
|
||||
() =>
|
||||
Navigator.of(context).pushNamed(RouteNames.remoteDownload),
|
||||
),
|
||||
),
|
||||
Consumer<FileManagerProvider>(
|
||||
builder: (context, fileManager, _) {
|
||||
@@ -568,7 +712,10 @@ class _FilesPageState extends State<FilesPage> {
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 7,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Colors.white.withValues(alpha: 0.12)
|
||||
@@ -649,7 +796,8 @@ class _FilesPageState extends State<FilesPage> {
|
||||
final isDesktop = MediaQuery.of(context).size.width >= 1000;
|
||||
final child = _buildFileList(context);
|
||||
|
||||
if (!isDesktop || !Platform.isWindows && !Platform.isLinux && !Platform.isMacOS) {
|
||||
if (!isDesktop ||
|
||||
!Platform.isWindows && !Platform.isLinux && !Platform.isMacOS) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@@ -673,13 +821,19 @@ class _FilesPageState extends State<FilesPage> {
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.85),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surface.withValues(alpha: 0.85),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(LucideIcons.upload, size: 48, color: Theme.of(context).colorScheme.primary),
|
||||
Icon(
|
||||
LucideIcons.upload,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'释放文件以上传到当前目录',
|
||||
@@ -709,8 +863,14 @@ class _FilesPageState extends State<FilesPage> {
|
||||
}
|
||||
if (files.isEmpty) return;
|
||||
|
||||
final uploadManager = Provider.of<UploadManagerProvider>(context, listen: false);
|
||||
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
|
||||
final uploadManager = Provider.of<UploadManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final fileManager = Provider.of<FileManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
uploadManager.markShouldShowDialog();
|
||||
uploadManager.startUpload(files, fileManager.currentPath);
|
||||
ToastHelper.info('已添加 ${files.length} 个文件到上传队列');
|
||||
@@ -740,12 +900,19 @@ class _FilesPageState extends State<FilesPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorView(BuildContext context, FileManagerProvider fileManager) {
|
||||
Widget _buildErrorView(
|
||||
BuildContext context,
|
||||
FileManagerProvider fileManager,
|
||||
) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.error),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
fileManager.errorMessage!,
|
||||
@@ -783,23 +950,38 @@ class _FilesPageState extends State<FilesPage> {
|
||||
Widget _buildListView(BuildContext context, FileManagerProvider fileManager) {
|
||||
final isDesktop = MediaQuery.of(context).size.width >= 1000;
|
||||
final showCheckbox = fileManager.hasSelection;
|
||||
final itemCount =
|
||||
fileManager.files.length +
|
||||
(fileManager.hasMore || fileManager.isLoadingMore ? 1 : 0);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (isDesktop) FileListHeader(showCheckbox: showCheckbox),
|
||||
if (isDesktop)
|
||||
FileListHeader(
|
||||
showCheckbox: showCheckbox,
|
||||
currentSort: fileManager.sortOption,
|
||||
onSort: (option) => fileManager.setSortOption(option),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _onRefresh(fileManager),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScrollNotification,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
key: PageStorageKey('files_list_${fileManager.currentPath}'),
|
||||
cacheExtent: 900,
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
itemCount: fileManager.files.length,
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= fileManager.files.length) {
|
||||
return _buildLoadMoreIndicator(context, fileManager);
|
||||
}
|
||||
final file = fileManager.files[index];
|
||||
final isSelected = fileManager.selectedFiles.contains(file.path);
|
||||
final isSelected = fileManager.selectedFiles.contains(
|
||||
file.path,
|
||||
);
|
||||
|
||||
return FileListItem(
|
||||
key: ValueKey('file_${file.id}'),
|
||||
@@ -821,14 +1003,40 @@ class _FilesPageState extends State<FilesPage> {
|
||||
}
|
||||
},
|
||||
onSelect: () => fileManager.toggleSelection(file.path),
|
||||
onDownload: !file.isFolder ? () => _downloadFile(context, fileManager, file) : null,
|
||||
onOpenInBrowser: !file.isFolder ? () => _openInBrowser(context, file) : null,
|
||||
onOpenInCloudreveApp: !file.isFolder ? () => _openInCloudreveApp(context, file) : null,
|
||||
onRename: () => FileOperationDialogs.showRenameDialog(context, fileManager, file),
|
||||
onMove: () => FileOperationDialogs.showMoveDialog(context, fileManager, file, false),
|
||||
onCopy: () => FileOperationDialogs.showMoveDialog(context, fileManager, file, true),
|
||||
onShare: () => FileOperationDialogs.showShareDialog(context, file),
|
||||
onDelete: () => FileOperationDialogs.showDeleteSingleConfirmation(context, fileManager, file),
|
||||
onDownload: !file.isFolder
|
||||
? () => _downloadFile(context, fileManager, file)
|
||||
: null,
|
||||
onOpenInBrowser: !file.isFolder
|
||||
? () => _openInBrowser(context, file)
|
||||
: null,
|
||||
onOpenInCloudreveApp: !file.isFolder
|
||||
? () => _openInCloudreveApp(context, file)
|
||||
: null,
|
||||
onRename: () => FileOperationDialogs.showRenameDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
),
|
||||
onMove: () => FileOperationDialogs.showMoveDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
false,
|
||||
),
|
||||
onCopy: () => FileOperationDialogs.showMoveDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
true,
|
||||
),
|
||||
onShare: () =>
|
||||
FileOperationDialogs.showShareDialog(context, file),
|
||||
onDelete: () =>
|
||||
FileOperationDialogs.showDeleteSingleConfirmation(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
),
|
||||
onInfo: () => _showFileInfo(file),
|
||||
);
|
||||
},
|
||||
@@ -857,15 +1065,20 @@ class _FilesPageState extends State<FilesPage> {
|
||||
crossAxisCount = 5;
|
||||
}
|
||||
|
||||
final itemWidth = (availableWidth - spacing * (crossAxisCount - 1)) / crossAxisCount;
|
||||
final itemWidth =
|
||||
(availableWidth - spacing * (crossAxisCount - 1)) / crossAxisCount;
|
||||
final childAspectRatio = itemWidth / 160;
|
||||
final showCheckbox = fileManager.hasSelection;
|
||||
final itemCount =
|
||||
fileManager.files.length +
|
||||
(fileManager.hasMore || fileManager.isLoadingMore ? 1 : 0);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _onRefresh(fileManager),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScrollNotification,
|
||||
child: GridView.builder(
|
||||
controller: _scrollController,
|
||||
key: PageStorageKey('files_grid_${fileManager.currentPath}'),
|
||||
cacheExtent: 1100,
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
@@ -876,8 +1089,11 @@ class _FilesPageState extends State<FilesPage> {
|
||||
crossAxisSpacing: spacing / 2,
|
||||
childAspectRatio: childAspectRatio,
|
||||
),
|
||||
itemCount: fileManager.files.length,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= fileManager.files.length) {
|
||||
return _buildGridLoadMoreIndicator(context, fileManager);
|
||||
}
|
||||
final file = fileManager.files[index];
|
||||
final isSelected = fileManager.selectedFiles.contains(file.path);
|
||||
|
||||
@@ -900,14 +1116,39 @@ class _FilesPageState extends State<FilesPage> {
|
||||
}
|
||||
},
|
||||
onSelect: () => fileManager.toggleSelection(file.path),
|
||||
onDownload: !file.isFolder ? () => _downloadFile(context, fileManager, file) : null,
|
||||
onOpenInBrowser: !file.isFolder ? () => _openInBrowser(context, file) : null,
|
||||
onOpenInCloudreveApp: !file.isFolder ? () => _openInCloudreveApp(context, file) : null,
|
||||
onRename: () => FileOperationDialogs.showRenameDialog(context, fileManager, file),
|
||||
onMove: () => FileOperationDialogs.showMoveDialog(context, fileManager, file, false),
|
||||
onCopy: () => FileOperationDialogs.showMoveDialog(context, fileManager, file, true),
|
||||
onShare: () => FileOperationDialogs.showShareDialog(context, file),
|
||||
onDelete: () => FileOperationDialogs.showDeleteSingleConfirmation(context, fileManager, file),
|
||||
onDownload: !file.isFolder
|
||||
? () => _downloadFile(context, fileManager, file)
|
||||
: null,
|
||||
onOpenInBrowser: !file.isFolder
|
||||
? () => _openInBrowser(context, file)
|
||||
: null,
|
||||
onOpenInCloudreveApp: !file.isFolder
|
||||
? () => _openInCloudreveApp(context, file)
|
||||
: null,
|
||||
onRename: () => FileOperationDialogs.showRenameDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
),
|
||||
onMove: () => FileOperationDialogs.showMoveDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
false,
|
||||
),
|
||||
onCopy: () => FileOperationDialogs.showMoveDialog(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
true,
|
||||
),
|
||||
onShare: () =>
|
||||
FileOperationDialogs.showShareDialog(context, file),
|
||||
onDelete: () => FileOperationDialogs.showDeleteSingleConfirmation(
|
||||
context,
|
||||
fileManager,
|
||||
file,
|
||||
),
|
||||
onInfo: () => _showFileInfo(file),
|
||||
);
|
||||
},
|
||||
@@ -916,6 +1157,62 @@ class _FilesPageState extends State<FilesPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadMoreIndicator(
|
||||
BuildContext context,
|
||||
FileManagerProvider fileManager,
|
||||
) {
|
||||
if (fileManager.isLoadingMore) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => fileManager.loadMoreFiles(),
|
||||
icon: const Icon(LucideIcons.chevronsDown, size: 16),
|
||||
label: const Text('加载更多'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridLoadMoreIndicator(
|
||||
BuildContext context,
|
||||
FileManagerProvider fileManager,
|
||||
) {
|
||||
if (fileManager.isLoadingMore) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => fileManager.loadMoreFiles(),
|
||||
icon: const Icon(LucideIcons.chevronsDown, size: 16),
|
||||
label: const Text('加载更多'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth >= 1000;
|
||||
@@ -930,29 +1227,29 @@ class _FilesPageState extends State<FilesPage> {
|
||||
onSelectAll: () => fileManager.selectAll(),
|
||||
onMore: fileManager.selectedFiles.length == 1
|
||||
? () => _showSelectionMore(
|
||||
fileManager.files.firstWhere(
|
||||
(f) => f.path == fileManager.selectedFiles.first,
|
||||
),
|
||||
fileManager,
|
||||
)
|
||||
fileManager.files.firstWhere(
|
||||
(f) => f.path == fileManager.selectedFiles.first,
|
||||
),
|
||||
fileManager,
|
||||
)
|
||||
: null,
|
||||
onMove: () => FileOperationDialogs.showBatchMoveDialog(
|
||||
context,
|
||||
fileManager,
|
||||
fileManager.selectedFiles,
|
||||
false,
|
||||
),
|
||||
context,
|
||||
fileManager,
|
||||
fileManager.selectedFiles,
|
||||
false,
|
||||
),
|
||||
onCopy: () => FileOperationDialogs.showBatchMoveDialog(
|
||||
context,
|
||||
fileManager,
|
||||
fileManager.selectedFiles,
|
||||
true,
|
||||
),
|
||||
context,
|
||||
fileManager,
|
||||
fileManager.selectedFiles,
|
||||
true,
|
||||
),
|
||||
onDelete: () => FileOperationDialogs.showDeleteConfirmation(
|
||||
context,
|
||||
fileManager,
|
||||
fileManager.selectedFiles,
|
||||
),
|
||||
context,
|
||||
fileManager,
|
||||
fileManager.selectedFiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -976,11 +1273,17 @@ class _FilesPageState extends State<FilesPage> {
|
||||
} else if (FileTypeUtils.isAudio(file.name)) {
|
||||
Navigator.of(context).pushNamed(RouteNames.audioPreview, arguments: file);
|
||||
} else if (FileTypeUtils.isMarkdown(file.name)) {
|
||||
Navigator.of(context).pushNamed(RouteNames.markdownPreview, arguments: file);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamed(RouteNames.markdownPreview, arguments: file);
|
||||
} else if (FileTypeUtils.isTextCode(file.name)) {
|
||||
Navigator.of(context).pushNamed(RouteNames.documentPreview, arguments: file);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamed(RouteNames.documentPreview, arguments: file);
|
||||
} else {
|
||||
ToastHelper.info('暂不支持预览 ${FileTypeUtils.getFileTypeDescription(file.name)}');
|
||||
ToastHelper.info(
|
||||
'暂不支持预览 ${FileTypeUtils.getFileTypeDescription(file.name)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,7 +1292,10 @@ class _FilesPageState extends State<FilesPage> {
|
||||
FileManagerProvider fileManager,
|
||||
FileModel file,
|
||||
) async {
|
||||
final downloadManager = Provider.of<DownloadManagerProvider>(context, listen: false);
|
||||
final downloadManager = Provider.of<DownloadManagerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final task = await downloadManager.addDownloadTask(
|
||||
fileName: file.name,
|
||||
fileUri: file.relativePath,
|
||||
@@ -1037,11 +1343,8 @@ class _FilesPageState extends State<FilesPage> {
|
||||
}
|
||||
|
||||
void _openInCloudreveApp(BuildContext context, FileModel file) {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteNames.cloudreveFileApp,
|
||||
arguments: {
|
||||
'file': file,
|
||||
},
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamed(RouteNames.cloudreveFileApp, arguments: {'file': file});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user