import 'dart:async'; import 'dart:io'; 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'; import 'package:url_launcher/url_launcher.dart'; import '../../providers/file_manager_provider.dart'; import '../../providers/download_manager_provider.dart'; import '../../providers/upload_manager_provider.dart'; import '../../providers/navigation_provider.dart'; import '../../widgets/file_list_item.dart'; import '../../widgets/file_grid_item.dart'; import '../../widgets/file_list_header.dart'; import '../../widgets/file_breadcrumb.dart'; import '../../widgets/selection_toolbar.dart'; import '../../widgets/empty_folder_view.dart'; import '../../widgets/upload_dialog.dart'; import '../../widgets/file_operation_dialogs.dart'; import '../../widgets/file_info_dialog.dart'; import '../../widgets/search_dialog.dart'; import '../../widgets/toast_helper.dart'; import '../../../router/app_router.dart'; import '../../../core/utils/file_type_utils.dart'; class FilesPage extends StatefulWidget { const FilesPage({super.key}); @override State createState() => _FilesPageState(); } class _FilesPageState extends State { bool _isFirstLoad = true; FileModel? _infoFile; final GlobalKey _scaffoldKey = GlobalKey(); final ScrollController _scrollController = ScrollController(); final ScrollController _breadcrumbController = ScrollController(); // FAB 状态 bool _isFabVisible = true; bool _isFabExpanded = false; Timer? _fabShowTimer; // 桌面端拖拽状态 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( 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( context, listen: false, ); downloadManager.initialize(); } }); // 上传完成 → 自动刷新当前目录文件列表 UploadService.instance.onUploadCompleted = (targetPath, fileName) { if (!mounted) return; final fileManager = Provider.of( context, listen: false, ); final normalizedCurrent = FileUtils.toCloudreveUri( fileManager.currentPath, ); if (targetPath == normalizedCurrent) { final fileUri = targetPath.endsWith('/') ? '$targetPath$fileName' : '$targetPath/$fileName'; fileManager.addFileByUri(fileUri); } }; } @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( 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(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( context, listen: false, ); if (fileManager.currentPath != '/') { fileManager.goBack(); } } } void _showFileInfo(FileModel file) { setState(() => _infoFile = file); // 等待下一帧 rebuild 完成,endDrawer 从 null 变为非 null 后再打开 WidgetsBinding.instance.addPostFrameCallback((_) { _scaffoldKey.currentState?.openEndDrawer(); }); } void _showSelectionMore(FileModel file, FileManagerProvider fileManager) { showModalBottomSheet( context: context, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.edit), title: const Text('重命名'), onTap: () { Navigator.of(sheetContext).pop(); FileOperationDialogs.showRenameDialog( context, fileManager, file, ); }, ), ListTile( leading: const Icon(Icons.info_outline), title: const Text('查看详情'), onTap: () { Navigator.of(sheetContext).pop(); _showFileInfo(file); }, ), ], ), ), ); } // ---- FAB 显隐控制 ---- void _hideFab() { _fabShowTimer?.cancel(); if (_isFabVisible) { setState(() { _isFabVisible = false; _isFabExpanded = false; }); } } void _scheduleShowFab() { _fabShowTimer?.cancel(); _fabShowTimer = Timer(const Duration(seconds: 1), () { if (mounted && !_isFabVisible) { setState(() => _isFabVisible = true); } }); } bool _onScrollNotification(ScrollNotification notification) { if (notification is ScrollStartNotification || notification is ScrollUpdateNotification) { _hideFab(); } else if (notification is ScrollEndNotification) { _scheduleShowFab(); } return false; } void _toggleFabExpanded() { setState(() => _isFabExpanded = !_isFabExpanded); } void _onFabSubAction(VoidCallback action) { setState(() => _isFabExpanded = false); action(); } // ---- 构建方法 ---- @override Widget build(BuildContext context) { final isDesktop = MediaQuery.of(context).size.width >= 1000; 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), ), ); } PreferredSizeWidget _buildAppBar(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isDesktop = screenWidth >= 1000; return AppBar( title: Consumer( builder: (context, fileManager, child) { if (isDesktop) { if (fileManager.currentPath == '/') { return const Text('文件'); } final segments = fileManager.currentPath .split('/') .where((s) => s.isNotEmpty) .toList(); return Text( segments.isNotEmpty ? _decodePathSegment(segments.last) : '文件', ); } return _buildMobileBreadcrumb(context, fileManager); }, ), actions: isDesktop ? _buildDesktopActions() : _buildMobileActions(), ); } /// 循环解码路径段,处理多重 URL 编码(如 %25E4%25B8%25AD → 中文) String _decodePathSegment(String segment) { var decoded = segment; for (var i = 0; i < 5; i++) { try { final next = Uri.decodeComponent(decoded); if (next == decoded) break; decoded = next; } catch (_) { break; } } return decoded; } 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( controller: _breadcrumbController, scrollDirection: Axis.horizontal, children: [ _buildBreadcrumbChip( context, label: '文件', icon: LucideIcons.home, color: colorScheme.primary, 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), ), ), _buildBreadcrumbChip( context, label: _decodePathSegment(pathParts[i]), icon: null, color: colorScheme.primary, isLast: i == pathParts.length - 1, onTap: () { final targetPath = '/${pathParts.sublist(0, i + 1).join('/')}'; if (targetPath != fileManager.currentPath) { fileManager.enterFolder(targetPath); } }, ), ], ], ), ); } Widget _buildBreadcrumbChip( BuildContext context, { required String label, required IconData? icon, required Color color, bool isLast = false, VoidCallback? onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: Container( height: 28, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: isLast ? color.withValues(alpha: 0.15) : color.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(14), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 14, color: color), const SizedBox(width: 3), ], Text( label, style: TextStyle( color: color, fontWeight: isLast ? FontWeight.w600 : FontWeight.w500, fontSize: 12, ), ), ], ), ), ); } List _buildDesktopActions() { return [ IconButton( icon: const Icon(LucideIcons.search), onPressed: () => SearchDialog.show(context), tooltip: '搜索', ), Consumer( builder: (context, fileManager, child) { return IconButton( icon: Icon( fileManager.isLoading ? Icons.hourglass_empty : Icons.refresh, ), onPressed: () => fileManager.refreshFiles(), tooltip: '刷新', ); }, ), Consumer( builder: (context, fileManager, child) { return _buildSortMenu(fileManager); }, ), Consumer( builder: (context, fileManager, child) { final icon = fileManager.viewType == FileViewType.list ? Icons.grid_view : Icons.view_list; return IconButton( icon: Icon(icon), onPressed: () { fileManager.setViewType( fileManager.viewType == FileViewType.list ? FileViewType.grid : FileViewType.list, ); }, tooltip: fileManager.viewType == FileViewType.list ? '网格视图' : '列表视图', ); }, ), IconButton( icon: const Icon(Icons.add), onPressed: () { final fileManager = Provider.of( context, listen: false, ); FileOperationDialogs.showCreateDialog(context, fileManager); }, tooltip: '新建', ), IconButton( icon: const Icon(Icons.cloud_upload), onPressed: () => showUploadDialog(context), tooltip: '上传', ), IconButton( icon: const Icon(Icons.cloud_download), onPressed: () => Provider.of(context, listen: false).setIndex(2), tooltip: '下载', ), ]; } List _buildMobileActions() { return [ Consumer( builder: (context, fileManager, child) { return _buildSortMenu(fileManager); }, ), Consumer( builder: (context, fileManager, child) { final icon = fileManager.viewType == FileViewType.list ? Icons.grid_view : Icons.view_list; return IconButton( icon: Icon(icon), onPressed: () { fileManager.setViewType( fileManager.viewType == FileViewType.list ? FileViewType.grid : 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( 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( 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) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final isDark = theme.brightness == Brightness.dark; return AnimatedSlide( offset: _isFabVisible ? Offset.zero : const Offset(0, 2), duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: AnimatedOpacity( opacity: _isFabVisible ? 1.0 : 0.0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildFabSubItem( context: context, index: 0, icon: LucideIcons.search, label: '搜索', isDark: isDark, colorScheme: colorScheme, onTap: () => _onFabSubAction(() => SearchDialog.show(context)), ), _buildFabSubItem( context: context, index: 1, icon: LucideIcons.upload, label: '上传', isDark: isDark, colorScheme: colorScheme, onTap: () => _onFabSubAction(() => showUploadDialog(context)), ), _buildFabSubItem( context: context, index: 2, icon: LucideIcons.folderPlus, label: '新建文件夹', isDark: isDark, colorScheme: colorScheme, onTap: () { final fileManager = Provider.of( context, listen: false, ); _onFabSubAction( () => FileOperationDialogs.showCreateDialog( context, fileManager, ), ); }, ), _buildFabSubItem( context: context, index: 3, icon: LucideIcons.download, label: '离线下载', isDark: isDark, colorScheme: colorScheme, onTap: () => _onFabSubAction( () => Navigator.of(context).pushNamed(RouteNames.remoteDownload), ), ), Consumer( builder: (context, fileManager, _) { final isListView = fileManager.viewType == FileViewType.list; return _buildFabSubItem( context: context, index: 4, icon: isListView ? LucideIcons.layoutGrid : LucideIcons.list, label: isListView ? '网格视图' : '列表视图', isDark: isDark, colorScheme: colorScheme, onTap: () { _onFabSubAction(() { fileManager.setViewType( isListView ? FileViewType.grid : FileViewType.list, ); }); }, ); }, ), // 主按钮:与子按钮同风格同尺寸 Padding( padding: const EdgeInsets.only(bottom: 4, right: 4), child: AnimatedScale( scale: _isFabExpanded ? 1.0 : 1.08, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: _buildFabButton( isDark: isDark, colorScheme: colorScheme, onTap: _toggleFabExpanded, child: AnimatedRotation( turns: _isFabExpanded ? 0.125 : 0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: Icon( LucideIcons.plus, color: colorScheme.primary, size: 22, ), ), ), ), ), ], ), ), ); } Widget _buildFabSubItem({ required BuildContext context, required int index, required IconData icon, required String label, required bool isDark, required ColorScheme colorScheme, required VoidCallback onTap, }) { final staggerDelay = Duration(milliseconds: 50 * index); return AnimatedSlide( offset: _isFabExpanded ? Offset.zero : const Offset(0, 1.2), duration: const Duration(milliseconds: 250) + staggerDelay, curve: Curves.easeOutCubic, child: AnimatedOpacity( opacity: _isFabExpanded ? 1.0 : 0.0, duration: const Duration(milliseconds: 200) + staggerDelay, curve: Curves.easeOut, child: AnimatedScale( scale: _isFabExpanded ? 1.0 : 0.4, duration: const Duration(milliseconds: 250) + staggerDelay, curve: Curves.easeOutCubic, child: Padding( padding: const EdgeInsets.only(bottom: 14, right: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ ClipRRect( borderRadius: BorderRadius.circular(10), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 7, ), decoration: BoxDecoration( color: isDark ? Colors.white.withValues(alpha: 0.12) : Colors.white.withValues(alpha: 0.75), borderRadius: BorderRadius.circular(10), border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.white.withValues(alpha: 0.4), ), ), child: Text( label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: isDark ? Colors.white : Colors.grey.shade800, ), ), ), ), ), const SizedBox(width: 12), _buildFabButton( isDark: isDark, colorScheme: colorScheme, onTap: onTap, child: Icon(icon, size: 20, color: colorScheme.primary), ), ], ), ), ), ), ); } /// 统一的毛玻璃圆形按钮 Widget _buildFabButton({ required bool isDark, required ColorScheme colorScheme, required VoidCallback onTap, required Widget child, }) { const size = 44.0; const radius = 22.0; return ClipRRect( borderRadius: BorderRadius.circular(radius), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( width: size, height: size, decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: isDark ? 0.2 : 0.12), borderRadius: BorderRadius.circular(radius), border: Border.all( color: colorScheme.primary.withValues(alpha: isDark ? 0.25 : 0.2), ), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(radius), onTap: onTap, child: Center(child: child), ), ), ), ), ); } // ---- Body ---- Widget _buildBody(BuildContext context) { final isDesktop = MediaQuery.of(context).size.width >= 1000; final child = _buildFileList(context); if (!isDesktop || !Platform.isWindows && !Platform.isLinux && !Platform.isMacOS) { return child; } return DropTarget( onDragEntered: (_) => setState(() => _isDraggingOver = true), onDragExited: (_) => setState(() => _isDraggingOver = false), onDragDone: (details) { setState(() => _isDraggingOver = false); _handleDroppedFiles(details.files); }, child: Stack( children: [ child, if (_isDraggingOver) IgnorePointer( child: Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.primary, width: 3, strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: BorderRadius.circular(8), 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, ), const SizedBox(height: 12), Text( '释放文件以上传到当前目录', style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 16, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), ], ), ); } void _handleDroppedFiles(List droppedFiles) { final files = []; for (final xFile in droppedFiles) { final path = xFile.path; if (path.isNotEmpty) { files.add(File(path)); } } if (files.isEmpty) return; final uploadManager = Provider.of( context, listen: false, ); final fileManager = Provider.of( context, listen: false, ); uploadManager.markShouldShowDialog(); uploadManager.startUpload(files, fileManager.currentPath); ToastHelper.info('已添加 ${files.length} 个文件到上传队列'); } Widget _buildFileList(BuildContext context) { return Consumer( builder: (context, fileManager, child) { if (fileManager.isLoading) { return const Center(child: CircularProgressIndicator()); } if (fileManager.errorMessage != null) { return _buildErrorView(context, fileManager); } if (fileManager.files.isEmpty) { return EmptyFolderView(currentPath: fileManager.currentPath); } if (fileManager.viewType == FileViewType.list) { return _buildListView(context, fileManager); } return _buildGridView(context, 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, ), const SizedBox(height: 16), Text( fileManager.errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: Theme.of(context).hintColor), ), const SizedBox(height: 16), FilledButton( onPressed: () => fileManager.loadFiles(), child: const Text('重试'), ), ], ), ); } Future _onRefresh(FileManagerProvider fileManager) async { final result = await fileManager.refreshFiles(); if (!mounted) return; if (fileManager.errorMessage != null) { ToastHelper.error(fileManager.errorMessage!); return; } if (result.isUnchanged) { ToastHelper.info('列表已是最新'); } else { final parts = []; if (result.added > 0) parts.add('新增 ${result.added} 项'); if (result.removed > 0) parts.add('移除 ${result.removed} 项'); if (result.updated > 0) parts.add('更新 ${result.updated} 项'); ToastHelper.success('已刷新:${parts.join(',')}'); } } 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, currentSort: fileManager.sortOption, onSort: (option) => fileManager.setSortOption(option), ), Expanded( child: RefreshIndicator( onRefresh: () => _onRefresh(fileManager), child: NotificationListener( onNotification: _onScrollNotification, child: ListView.builder( controller: _scrollController, key: PageStorageKey('files_list_${fileManager.currentPath}'), cacheExtent: 900, 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, ); return FileListItem( key: ValueKey('file_${file.id}'), file: file, isSelected: isSelected, isHighlighted: file.path == fileManager.highlightPath, showCheckbox: showCheckbox, index: index, isDesktop: isDesktop, onTap: () { _hideFab(); _scheduleShowFab(); if (showCheckbox) { fileManager.toggleSelection(file.path); } else if (file.isFolder) { fileManager.enterFolder(file.relativePath); } else { _openFile(context, file); } }, 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, ), onInfo: () => _showFileInfo(file), ); }, ), ), ), ), ], ); } Widget _buildGridView(BuildContext context, FileManagerProvider fileManager) { final screenWidth = MediaQuery.sizeOf(context).width; final padding = 16.0; final spacing = 16.0; final availableWidth = screenWidth - padding * 2; int crossAxisCount; if (screenWidth < 400) { crossAxisCount = 2; } else if (screenWidth < 600) { crossAxisCount = 3; } else if (screenWidth < 900) { crossAxisCount = 4; } else { crossAxisCount = 5; } 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( onNotification: _onScrollNotification, child: GridView.builder( controller: _scrollController, key: PageStorageKey('files_grid_${fileManager.currentPath}'), cacheExtent: 1100, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisSpacing: spacing / 2, crossAxisSpacing: spacing / 2, childAspectRatio: childAspectRatio, ), 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); return FileGridItem( key: ValueKey('file_grid_${file.id}'), file: file, isSelected: isSelected, isHighlighted: file.path == fileManager.highlightPath, showCheckbox: showCheckbox, contextHint: fileManager.contextHint, onTap: () { _hideFab(); _scheduleShowFab(); if (showCheckbox) { fileManager.toggleSelection(file.path); } else if (file.isFolder) { fileManager.enterFolder(file.relativePath); } else { _openFile(context, file); } }, 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, ), onInfo: () => _showFileInfo(file), ); }, ), ), ); } 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; return Consumer( builder: (context, fileManager, child) { if (fileManager.hasSelection) { return SelectionToolbar( selectionCount: fileManager.selectedFiles.length, totalCount: fileManager.files.length, onCancel: () => fileManager.clearSelection(), onSelectAll: () => fileManager.selectAll(), onMore: fileManager.selectedFiles.length == 1 ? () => _showSelectionMore( fileManager.files.firstWhere( (f) => f.path == fileManager.selectedFiles.first, ), fileManager, ) : null, onMove: () => FileOperationDialogs.showBatchMoveDialog( context, fileManager, fileManager.selectedFiles, false, ), onCopy: () => FileOperationDialogs.showBatchMoveDialog( context, fileManager, fileManager.selectedFiles, true, ), onDelete: () => FileOperationDialogs.showDeleteConfirmation( context, fileManager, fileManager.selectedFiles, ), ); } if (!isDesktop) return const SizedBox.shrink(); return FileBreadcrumb( currentPath: fileManager.currentPath, onPathTap: (path) => fileManager.enterFolder(path), ); }, ); } void _openFile(BuildContext context, FileModel file) { if (FileTypeUtils.isImage(file.name)) { Navigator.of(context).pushNamed(RouteNames.imagePreview, arguments: file); } else if (FileTypeUtils.isPdf(file.name)) { Navigator.of(context).pushNamed(RouteNames.pdfPreview, arguments: file); } else if (FileTypeUtils.isVideo(file.name)) { Navigator.of(context).pushNamed(RouteNames.videoPreview, arguments: file); } 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); } else if (FileTypeUtils.isTextCode(file.name)) { Navigator.of( context, ).pushNamed(RouteNames.documentPreview, arguments: file); } else { ToastHelper.info( '暂不支持预览 ${FileTypeUtils.getFileTypeDescription(file.name)}', ); } } Future _downloadFile( BuildContext context, FileManagerProvider fileManager, FileModel file, ) async { final downloadManager = Provider.of( context, listen: false, ); final task = await downloadManager.addDownloadTask( fileName: file.name, fileUri: file.relativePath, fileSize: file.size, ); if (task != null) { if (context.mounted) { ToastHelper.info('文件已在下载列表中'); } return; } if (context.mounted) { ToastHelper.info('开始下载,查看任务页'); } } Future _openInBrowser(BuildContext context, FileModel file) async { try { final response = await FileService().getDownloadUrls( uris: [file.relativePath], download: true, ); final urls = response['urls'] as List? ?? []; if (urls.isNotEmpty) { final urlData = urls[0] as Map; final url = urlData['url'] as String; final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.platformDefault); } else { if (context.mounted) { ToastHelper.error('无法打开链接: $uri'); } } } } catch (e) { if (context.mounted) { ToastHelper.failure('获取下载链接失败: $e'); } } } void _openInCloudreveApp(BuildContext context, FileModel file) { Navigator.of( context, ).pushNamed(RouteNames.cloudreveFileApp, arguments: {'file': file}); } }