merge new features

This commit is contained in:
2026-05-27 19:19:11 +08:00
parent 886046a72b
commit 98af110531
37 changed files with 6405 additions and 993 deletions
@@ -0,0 +1,177 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class AnnouncementDialog extends StatefulWidget {
final String title;
final String html;
final String baseUrl;
const AnnouncementDialog({
super.key,
required this.title,
required this.html,
required this.baseUrl,
});
static Future<void> show(
BuildContext context, {
required String title,
required String html,
required String baseUrl,
}) async {
if (html.trim().isEmpty) return;
await showDialog<void>(
context: context,
barrierDismissible: true,
builder: (_) => AnnouncementDialog(
title: title,
html: html,
baseUrl: baseUrl,
),
);
}
@override
State<AnnouncementDialog> createState() => _AnnouncementDialogState();
}
class _AnnouncementDialogState extends State<AnnouncementDialog> {
late final WebViewController _controller;
bool _loading = true;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.transparent)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) {
if (mounted) setState(() => _loading = false);
},
),
);
_load();
}
Future<void> _load() async {
final origin = Uri.parse(widget.baseUrl).origin;
final html = _wrapHtml(widget.html);
await _controller.loadHtmlString(
html,
baseUrl: '$origin/',
);
}
String _wrapHtml(String body) {
final encodedTitle = const HtmlEscape().convert(widget.title);
return '''
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>$encodedTitle</title>
<style>
html, body {
margin: 0;
padding: 0;
background: transparent;
color: #1f2937;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
font-size: 14px;
line-height: 1.65;
overflow-wrap: anywhere;
}
body {
padding: 12px 14px 18px;
box-sizing: border-box;
}
img, video {
max-width: 100% !important;
height: auto !important;
border-radius: 12px;
}
a {
color: #2563eb;
text-decoration: none;
}
fieldset, section, div {
max-width: 100% !important;
box-sizing: border-box !important;
}
</style>
</head>
<body>
$body
</body>
</html>
''';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 28),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.72,
child: Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(18, 14, 8, 10),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withValues(alpha: 0.45),
),
),
),
child: Row(
children: [
Expanded(
child: Text(
widget.title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
),
IconButton(
tooltip: '关闭',
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
),
],
),
),
Expanded(
child: Stack(
children: [
WebViewWidget(controller: _controller),
if (_loading)
const Center(child: CircularProgressIndicator()),
],
),
),
],
),
),
),
);
}
}
+73 -26
View File
@@ -17,6 +17,7 @@ class FileGridItem extends StatelessWidget {
final VoidCallback? onSelect;
final VoidCallback? onDownload;
final VoidCallback? onOpenInBrowser;
final VoidCallback? onOpenInCloudreveApp;
final VoidCallback? onRename;
final VoidCallback? onMove;
final VoidCallback? onCopy;
@@ -38,6 +39,7 @@ class FileGridItem extends StatelessWidget {
this.onSelect,
this.onDownload,
this.onOpenInBrowser,
this.onOpenInCloudreveApp,
this.onRename,
this.onMove,
this.onCopy,
@@ -50,25 +52,27 @@ class FileGridItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Builder(
builder: (builderContext) => LayoutBuilder(
builder: (context, constraints) {
final fontSize = (constraints.maxWidth * 0.1).clamp(10.0, 13.0);
return RepaintBoundary(
child: Builder(
builder: (builderContext) => LayoutBuilder(
builder: (context, constraints) {
final fontSize = (constraints.maxWidth * 0.1).clamp(10.0, 13.0);
return _FileGridItemHover(
file: file,
isSelected: isSelected,
isHighlighted: isHighlighted,
showCheckbox: showCheckbox,
contextHint: contextHint,
fontSize: fontSize,
tapToShowMenu: tapToShowMenu,
onTap: tapToShowMenu ? null : onTap,
onLongPress: () => _showMenu(builderContext),
onSelect: onSelect,
onMore: () => _showMenu(builderContext),
);
},
return _FileGridItemHover(
file: file,
isSelected: isSelected,
isHighlighted: isHighlighted,
showCheckbox: showCheckbox,
contextHint: contextHint,
fontSize: fontSize,
tapToShowMenu: tapToShowMenu,
onTap: tapToShowMenu ? null : onTap,
onLongPress: () => _showMenu(builderContext),
onSelect: onSelect,
onMore: () => _showMenu(builderContext),
);
},
),
),
);
}
@@ -79,6 +83,7 @@ class FileGridItem extends StatelessWidget {
hasSelect: onSelect != null,
hasDownload: onDownload != null,
hasOpenInBrowser: onOpenInBrowser != null,
hasOpenInCloudreveApp: onOpenInCloudreveApp != null,
hasRename: onRename != null,
hasMove: onMove != null,
hasCopy: onCopy != null,
@@ -95,6 +100,8 @@ class FileGridItem extends StatelessWidget {
onDownload?.call();
case FileMenuAction.openInBrowser:
onOpenInBrowser?.call();
case FileMenuAction.openInCloudreveApp:
onOpenInCloudreveApp?.call();
case FileMenuAction.rename:
onRename?.call();
case FileMenuAction.move:
@@ -318,10 +325,8 @@ class _FileGridItemHoverState extends State<_FileGridItemHover> {
Widget _buildIconArea(BuildContext context) {
final file = widget.file;
final ext = FileUtils.getFileExtension(file.name);
final isThumbnailable = !file.isFolder
&& FileUtils.isImageFile(file.name)
&& ext != 'svg';
final isThumbnailable =
!file.isFolder && FileUtils.isThumbnailableFile(file.name);
if (!isThumbnailable) {
return Center(
@@ -335,10 +340,52 @@ class _FileGridItemHoverState extends State<_FileGridItemHover> {
);
}
return ThumbnailImage(
file: file,
contextHint: widget.contextHint,
borderRadius: 10,
return Stack(
fit: StackFit.expand,
children: [
ThumbnailImage(
file: file,
contextHint: widget.contextHint,
borderRadius: 10,
),
if (FileUtils.isVideoFile(file.name))
Center(
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(17),
),
child: const Icon(
LucideIcons.play,
color: Colors.white,
size: 18,
),
),
),
if (FileUtils.isPsdFile(file.name))
Positioned(
left: 6,
bottom: 6,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'PSD',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
),
),
],
);
}
}
+122 -177
View File
@@ -5,7 +5,6 @@ import '../../data/models/file_model.dart';
import '../../core/utils/date_utils.dart' as date_utils;
import '../../core/utils/file_icon_utils.dart';
import '../../core/utils/file_type_utils.dart';
import '../../core/utils/file_utils.dart';
import '../../services/file_service.dart';
import '../../router/app_router.dart';
import 'toast_helper.dart';
@@ -20,11 +19,58 @@ class FileInfoPanel extends StatefulWidget {
Scaffold.of(context).openEndDrawer();
}
/// 以 BottomSheet 方式展示文件详情
static void showAsBottomSheet(BuildContext context, FileModel file) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
showDragHandle: true,
builder: (_) => _FileInfoSheet(file: file),
);
}
@override
State<FileInfoPanel> createState() => _FileInfoPanelState();
}
class _FileInfoPanelState extends State<FileInfoPanel> {
@override
Widget build(BuildContext context) {
return Drawer(
child: FileInfoPanelContent(file: widget.file),
);
}
}
/// 以 BottomSheet 形式展示的文件详情
class _FileInfoSheet extends StatelessWidget {
final FileModel file;
const _FileInfoSheet({required this.file});
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.4,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return FileInfoPanelContent(file: file);
},
);
}
}
/// FileInfoPanel 的可复用内容区(不含 Drawer 壳)
class FileInfoPanelContent extends StatefulWidget {
final FileModel file;
const FileInfoPanelContent({super.key, required this.file});
@override
State<FileInfoPanelContent> createState() => _FileInfoPanelContentState();
}
class _FileInfoPanelContentState extends State<FileInfoPanelContent> {
FileInfoModel? _fileInfo;
bool _isLoading = true;
bool _isCalculatingFolder = false;
@@ -88,60 +134,53 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Drawer(
child: SafeArea(
right: false,
child: Column(
children: [
Container(
padding: const EdgeInsets.only(
left: 16,
right: 8,
top: 8,
bottom: 12,
return Column(
children: [
Container(
padding: const EdgeInsets.only(
left: 16,
right: 8,
top: 8,
bottom: 12,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: theme.dividerColor.withValues(alpha: 0.2)),
),
),
child: Row(
children: [
FileIconUtils.buildIconWidget(
context: context,
file: widget.file,
size: 32,
iconSize: 18,
borderRadius: 8,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withValues(alpha: 0.2),
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.file.name,
style: theme.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
child: Row(
children: [
FileIconUtils.buildIconWidget(
context: context,
file: widget.file,
size: 32,
iconSize: 18,
borderRadius: 8,
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.file.name,
style: theme.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.of(context).pop(),
),
],
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.of(context).pop(),
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
],
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? _buildError(theme)
: _buildContent(theme, colorScheme),
),
],
),
),
],
);
}
@@ -150,21 +189,13 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.alertCircle,
size: 48,
color: theme.colorScheme.error,
),
Icon(LucideIcons.alertCircle, size: 48, color: theme.colorScheme.error),
const SizedBox(height: 12),
Text('加载失败', style: theme.textTheme.titleSmall),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
_error ?? '',
style: TextStyle(color: theme.hintColor, fontSize: 12),
textAlign: TextAlign.center,
),
child: Text(_error ?? '', style: TextStyle(color: theme.hintColor, fontSize: 12), textAlign: TextAlign.center),
),
const SizedBox(height: 12),
FilledButton.tonal(onPressed: _loadFileInfo, child: const Text('重试')),
@@ -179,8 +210,10 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
? '文件夹'
: FileIconUtils.getFileTypeLabel(file.name);
final extendedInfo = _fileInfo?.extendedInfo;
final versionEntities =
extendedInfo?.entities?.where((e) => e.type == 0).toList() ?? [];
final versionEntities = extendedInfo?.entities
?.where((e) => e.type == 0)
.toList() ??
[];
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
@@ -188,7 +221,6 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 类型标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
@@ -205,43 +237,23 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
),
),
const SizedBox(height: 16),
// 基本信息
_buildInfoRow(
LucideIcons.folderOpen,
'位置',
FileUtils.safeDecodePathSegment(file.relativePath),
),
_buildInfoRow(LucideIcons.folderOpen, '位置', Uri.decodeComponent(file.relativePath)),
if (file.isFile)
_buildInfoRow(
LucideIcons.hardDrive,
'大小',
date_utils.DateUtils.formatFileSize(file.size),
),
_buildInfoRow(
LucideIcons.calendarPlus,
'创建时间',
date_utils.DateUtils.formatDateTime(file.createdAt),
),
_buildInfoRow(
LucideIcons.calendar,
'修改时间',
date_utils.DateUtils.formatDateTime(file.updatedAt),
),
_buildInfoRow(LucideIcons.calendarPlus, '创建时间', date_utils.DateUtils.formatDateTime(file.createdAt)),
_buildInfoRow(LucideIcons.calendar, '修改时间', date_utils.DateUtils.formatDateTime(file.updatedAt)),
if (file.owned != null)
_buildInfoRow(LucideIcons.shield, '所有者', file.owned! ? '' : ''),
// 文件扩展信息
if (file.isFile && extendedInfo != null) ...[
const SizedBox(height: 12),
Divider(color: theme.dividerColor.withValues(alpha: 0.3)),
const SizedBox(height: 8),
Text(
'扩展信息',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text('扩展信息', style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
_buildInfoRow(LucideIcons.fingerprint, '文件ID', file.id),
if (file.primaryEntity != null)
@@ -260,7 +272,6 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
),
],
// 版本历史
if (file.isFile && versionEntities.isNotEmpty) ...[
const SizedBox(height: 12),
Divider(color: theme.dividerColor.withValues(alpha: 0.3)),
@@ -268,17 +279,11 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
_buildVersionSection(theme, colorScheme, versionEntities),
],
// 文件夹信息
if (file.isFolder) ...[
const SizedBox(height: 12),
Divider(color: theme.dividerColor.withValues(alpha: 0.3)),
const SizedBox(height: 8),
Text(
'文件夹信息',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text('文件夹信息', style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
_buildFolderSummary(theme, colorScheme),
],
@@ -301,12 +306,7 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
children: [
Row(
children: [
Text(
'版本历史',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text('版本历史', style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
@@ -354,9 +354,7 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
required bool isPreviewable,
required FileModel file,
}) {
final shortId = entity.id.length > 6
? entity.id.substring(0, 6)
: entity.id;
final shortId = entity.id.length > 6 ? entity.id.substring(0, 6) : entity.id;
final createdBy = entity.createdBy?.nickname ?? '未知';
return Container(
@@ -378,32 +376,24 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
_buildVersionActionButton(
icon: LucideIcons.externalLink,
tooltip: '打开',
onPressed: _isVersionLoading
? null
: () => _openVersion(entity),
onPressed: _isVersionLoading ? null : () => _openVersion(entity),
),
_buildVersionActionButton(
icon: LucideIcons.download,
tooltip: '下载',
onPressed: _isVersionLoading
? null
: () => _downloadVersion(entity),
onPressed: _isVersionLoading ? null : () => _downloadVersion(entity),
),
if (!isCurrent) ...[
_buildVersionActionButton(
icon: LucideIcons.pin,
tooltip: '设为当前版本',
onPressed: _isVersionLoading
? null
: () => _setCurrentVersion(entity),
onPressed: _isVersionLoading ? null : () => _setCurrentVersion(entity),
),
_buildVersionActionButton(
icon: LucideIcons.trash2,
tooltip: '删除',
color: colorScheme.error,
onPressed: _isVersionLoading
? null
: () => _deleteVersion(entity),
onPressed: _isVersionLoading ? null : () => _deleteVersion(entity),
),
],
];
@@ -449,10 +439,8 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
const SizedBox(height: 1),
Text(
'${date_utils.DateUtils.formatDateTime(entity.createdAt)} · $createdBy',
style: const TextStyle(
fontSize: 11,
color: null,
).copyWith(color: theme.hintColor),
style: const TextStyle(fontSize: 11, color: null)
.copyWith(color: theme.hintColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -497,26 +485,14 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(LucideIcons.hash, 'ID', entity.id),
_buildInfoRow(
LucideIcons.hardDrive,
'大小',
date_utils.DateUtils.formatFileSize(entity.size),
),
_buildInfoRow(
LucideIcons.calendarPlus,
'创建时间',
date_utils.DateUtils.formatDateTime(entity.createdAt),
),
_buildInfoRow(LucideIcons.hardDrive, '大小', date_utils.DateUtils.formatFileSize(entity.size)),
_buildInfoRow(LucideIcons.calendarPlus, '创建时间', date_utils.DateUtils.formatDateTime(entity.createdAt)),
if (createdBy != null) ...[
_buildInfoRow(LucideIcons.user, '创建者', createdBy.nickname),
_buildInfoRow(LucideIcons.fingerprint, '创建者ID', createdBy.id),
],
if (entity.storagePolicy != null)
_buildInfoRow(
LucideIcons.server,
'存储策略',
'${entity.storagePolicy!.name} (${entity.storagePolicy!.type})',
),
_buildInfoRow(LucideIcons.server, '存储策略', '${entity.storagePolicy!.name} (${entity.storagePolicy!.type})'),
if (entity.encryptedWith != null)
_buildInfoRow(LucideIcons.lock, '加密', entity.encryptedWith!),
],
@@ -548,8 +524,6 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
);
}
// ─── 版本操作 ───
void _openVersion(EntityModel entity) {
final file = _fileInfo!.file;
if (!FileTypeUtils.isPreviewable(file.name)) {
@@ -568,13 +542,9 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
} else if (FileTypeUtils.isAudio(file.name)) {
Navigator.of(context).pushNamed(RouteNames.audioPreview, arguments: args);
} else if (FileTypeUtils.isMarkdown(file.name)) {
Navigator.of(
context,
).pushNamed(RouteNames.markdownPreview, arguments: args);
Navigator.of(context).pushNamed(RouteNames.markdownPreview, arguments: args);
} else if (FileTypeUtils.isTextCode(file.name)) {
Navigator.of(
context,
).pushNamed(RouteNames.documentPreview, arguments: args);
Navigator.of(context).pushNamed(RouteNames.documentPreview, arguments: args);
}
}
@@ -625,16 +595,12 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
Future<void> _deleteVersion(EntityModel entity) async {
final colorScheme = Theme.of(context).colorScheme;
final shortId = entity.id.length > 6
? entity.id.substring(0, 6)
: entity.id;
final shortId = entity.id.length > 6 ? entity.id.substring(0, 6) : entity.id;
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('删除版本'),
content: Text(
'确定要删除版本 "$shortId" (${date_utils.DateUtils.formatFileSize(entity.size)}) 吗?',
),
content: Text('确定要删除版本 "$shortId" (${date_utils.DateUtils.formatFileSize(entity.size)}) 吗?'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
@@ -669,8 +635,6 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
}
}
// ─── 文件夹摘要 ───
Widget _buildFolderSummary(ThemeData theme, ColorScheme colorScheme) {
final summary = _fileInfo?.folderSummary;
@@ -679,29 +643,15 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
children: [
_buildInfoRow(LucideIcons.file, '包含文件', '${summary.files}'),
_buildInfoRow(LucideIcons.folder, '包含文件夹', '${summary.folders}'),
_buildInfoRow(
LucideIcons.hardDrive,
'总大小',
date_utils.DateUtils.formatFileSize(summary.size),
),
_buildInfoRow(LucideIcons.hardDrive, '总大小', date_utils.DateUtils.formatFileSize(summary.size)),
if (!summary.completed)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
LucideIcons.alertCircle,
size: 14,
color: theme.colorScheme.error,
),
Icon(LucideIcons.alertCircle, size: 14, color: theme.colorScheme.error),
const SizedBox(width: 6),
Text(
'计算未完成,结果可能不完整',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.error,
),
),
Text('计算未完成,结果可能不完整', style: TextStyle(fontSize: 12, color: theme.colorScheme.error)),
],
),
),
@@ -715,12 +665,7 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
}
return _isCalculatingFolder
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
)
? const Center(child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()))
: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
@@ -742,13 +687,13 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
const SizedBox(width: 8),
SizedBox(
width: 72,
child: Text(
label,
style: TextStyle(fontSize: 13, color: theme.hintColor),
),
child: Text(label, style: TextStyle(fontSize: 13, color: theme.hintColor)),
),
Expanded(
child: SelectableText(value, style: const TextStyle(fontSize: 13)),
child: SelectableText(
value,
style: const TextStyle(fontSize: 13),
),
),
],
),
+18 -11
View File
@@ -18,6 +18,7 @@ class FileListItem extends StatelessWidget {
final VoidCallback? onSelect;
final VoidCallback? onDownload;
final VoidCallback? onOpenInBrowser;
final VoidCallback? onOpenInCloudreveApp;
final VoidCallback? onRename;
final VoidCallback? onMove;
final VoidCallback? onCopy;
@@ -40,6 +41,7 @@ class FileListItem extends StatelessWidget {
this.onSelect,
this.onDownload,
this.onOpenInBrowser,
this.onOpenInCloudreveApp,
this.onRename,
this.onMove,
this.onCopy,
@@ -51,17 +53,19 @@ class FileListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _FileListItemHover(
file: file,
isSelected: isSelected,
isHighlighted: isHighlighted,
index: index,
isDesktop: isDesktop,
showCheckbox: showCheckbox,
tapToShowMenu: tapToShowMenu,
onTap: tapToShowMenu ? null : onTap,
onLongPress: () => _showMenu(context),
onSelect: onSelect,
return RepaintBoundary(
child: _FileListItemHover(
file: file,
isSelected: isSelected,
isHighlighted: isHighlighted,
index: index,
isDesktop: isDesktop,
showCheckbox: showCheckbox,
tapToShowMenu: tapToShowMenu,
onTap: tapToShowMenu ? null : onTap,
onLongPress: () => _showMenu(context),
onSelect: onSelect,
),
);
}
@@ -71,6 +75,7 @@ class FileListItem extends StatelessWidget {
hasSelect: onSelect != null,
hasDownload: onDownload != null,
hasOpenInBrowser: onOpenInBrowser != null,
hasOpenInCloudreveApp: onOpenInCloudreveApp != null,
hasRename: onRename != null,
hasMove: onMove != null,
hasCopy: onCopy != null,
@@ -87,6 +92,8 @@ class FileListItem extends StatelessWidget {
onDownload?.call();
case FileMenuAction.openInBrowser:
onOpenInBrowser?.call();
case FileMenuAction.openInCloudreveApp:
onOpenInCloudreveApp?.call();
case FileMenuAction.rename:
onRename?.call();
case FileMenuAction.move:
@@ -7,6 +7,7 @@ enum FileMenuAction {
select,
download,
openInBrowser,
openInCloudreveApp,
rename,
move,
copy,
@@ -22,6 +23,7 @@ Future<FileMenuAction?> showFileMenu({
required bool hasSelect,
required bool hasDownload,
required bool hasOpenInBrowser,
bool hasOpenInCloudreveApp = false,
required bool hasRename,
required bool hasMove,
required bool hasCopy,
@@ -87,6 +89,17 @@ Future<FileMenuAction?> showFileMenu({
],
),
),
if (hasOpenInCloudreveApp)
const PopupMenuItem(
value: FileMenuAction.openInCloudreveApp,
child: Row(
children: [
Icon(Icons.web_asset, size: 20),
SizedBox(width: 12),
Text('在 Cloudreve 中打开'),
],
),
),
if (hasRename)
const PopupMenuItem(
value: FileMenuAction.rename,
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -132,7 +132,7 @@ class _FolderPickerState extends State<FolderPicker> {
return InkWell(
onTap: () => _enterFolder(folder),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Row(
children: [
Container(
@@ -163,7 +163,7 @@ class _FolderPickerState extends State<FolderPicker> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Row(
children: [
Expanded(
@@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
/// 选择工具栏组件
class SelectionToolbar extends StatelessWidget {
final int selectionCount;
final int totalCount;
final VoidCallback onCancel;
final VoidCallback? onRename;
final VoidCallback? onSelectAll;
final VoidCallback? onMore;
final VoidCallback? onMove;
final VoidCallback? onCopy;
final VoidCallback onDelete;
@@ -12,8 +14,10 @@ class SelectionToolbar extends StatelessWidget {
const SelectionToolbar({
super.key,
required this.selectionCount,
this.totalCount = 0,
required this.onCancel,
this.onRename,
this.onSelectAll,
this.onMore,
this.onMove,
this.onCopy,
required this.onDelete,
@@ -45,11 +49,19 @@ class SelectionToolbar extends StatelessWidget {
onPressed: onCancel,
tooltip: '取消选择',
),
if (selectionCount == 1 && onRename != null)
if (onSelectAll != null && selectionCount < totalCount)
IconButton(
icon: const Icon(Icons.edit),
onPressed: onRename,
tooltip: '重命名',
icon: const Icon(Icons.select_all),
onPressed: onSelectAll,
tooltip: '全选',
),
if (selectionCount == 1 && onMore != null)
IconButton(
icon: Icon(Theme.of(context).platform == TargetPlatform.iOS
? Icons.more_horiz
: Icons.more_vert),
onPressed: onMore,
tooltip: '更多',
),
if (onMove != null)
IconButton(