chore: initialize Guangya Flutter project

This commit is contained in:
ngfchl
2026-07-18 10:23:34 +08:00
commit 6bcc3fdf18
143 changed files with 13939 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../models/cloud_file.dart';
class BreadcrumbBar extends StatelessWidget {
final List<CloudFile> path;
final ValueChanged<int> onNavigate;
const BreadcrumbBar({
super.key,
required this.path,
required this.onNavigate,
});
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final items = <Widget>[
// Home button
GestureDetector(
onTap: () => onNavigate(-1),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: path.isEmpty ? cs.primary.withAlpha(15) : null,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'云盘',
style: TextStyle(
fontSize: 13,
fontWeight: path.isEmpty ? FontWeight.w600 : FontWeight.normal,
color: path.isEmpty ? cs.primary : cs.mutedForeground,
),
),
),
),
),
];
for (int i = 0; i < path.length; i++) {
final isLast = i == path.length - 1;
items.add(
Icon(
Icons.chevron_right_rounded,
size: 16,
color: cs.mutedForeground.withAlpha(100),
),
);
items.add(
GestureDetector(
onTap: isLast ? null : () => onNavigate(i),
child: MouseRegion(
cursor: isLast ? SystemMouseCursors.basic : SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isLast ? cs.primary.withAlpha(15) : null,
borderRadius: BorderRadius.circular(6),
),
child: Text(
path[i].name,
style: TextStyle(
fontSize: 13,
fontWeight: isLast ? FontWeight.w600 : FontWeight.normal,
color: isLast ? cs.primary : cs.mutedForeground,
),
),
),
),
),
);
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: items),
);
}
}
+32
View File
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
Future<bool> showConfirmDialog(
BuildContext context, {
required String title,
required String content,
String confirmText = '确认',
String cancelText = '取消',
}) async {
final result = await showShadDialog<bool>(
context: context,
builder: (context) => ShadDialog(
title: Text(title),
description: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(content),
),
actions: [
ShadButton.outline(
child: Text(cancelText),
onPressed: () => Navigator.of(context).pop(false),
),
ShadButton(
child: Text(confirmText),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
);
return result ?? false;
}
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../models/cloud_file.dart';
class FileIcon extends StatelessWidget {
final CloudFile file;
final double size;
const FileIcon({
super.key,
required this.file,
this.size = 28,
});
@override
Widget build(BuildContext context) {
if (file.isDirectory) {
return Icon(
LucideIcons.folder,
size: size,
color: const Color(0xFFF59E0B),
);
}
final iconData = _getIconForFileType(file.fileType);
final color = _getColorForFileType(file.fileType);
return Icon(
iconData,
size: size,
color: color,
);
}
IconData _getIconForFileType(int fileType) {
switch (fileType) {
case 1:
return LucideIcons.image;
case 2:
return LucideIcons.film;
case 3:
return LucideIcons.music;
case 4:
return LucideIcons.fileText;
case 5:
case 9:
return LucideIcons.archive;
default:
return LucideIcons.file;
}
}
Color _getColorForFileType(int fileType) {
switch (fileType) {
case 1:
return const Color(0xFF10B981);
case 2:
return const Color(0xFF3B82F6);
case 3:
return const Color(0xFFF59E0B);
case 4:
return const Color(0xFF8B5CF6);
case 5:
case 9:
return const Color(0xFF6B7280);
default:
return const Color(0xFF6B7280);
}
}
}
+192
View File
@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../models/cloud_file.dart';
import 'file_icon.dart';
/// A single file row in the file list with context menu support.
class FileListTile extends StatelessWidget {
final CloudFile file;
final bool isSelected;
final VoidCallback? onSelect;
final VoidCallback? onOpen;
final VoidCallback? onRename;
final VoidCallback? onCopy;
final VoidCallback? onCut;
final VoidCallback? onDownload;
final VoidCallback? onShare;
final VoidCallback? onDelete;
const FileListTile({
super.key,
required this.file,
this.isSelected = false,
this.onSelect,
this.onOpen,
this.onRename,
this.onCopy,
this.onCut,
this.onDownload,
this.onShare,
this.onDelete,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final isDark = theme.brightness == Brightness.dark;
return ShadContextMenuRegion(
items: [
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.folderOpen, size: 16),
trailing: const Icon(LucideIcons.chevronRight),
onPressed: onOpen,
child: const Text('打开'),
),
const ShadContextMenuItem.inset(
enabled: false,
child: Text('重命名'),
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.copy, size: 16),
trailing: const Icon(LucideIcons.chevronRight),
onPressed: onCopy,
child: const Text('复制'),
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.scissors, size: 16),
trailing: const Icon(LucideIcons.chevronRight),
onPressed: onCut,
child: const Text('剪切'),
),
const Divider(height: 8),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.download, size: 16),
trailing: const Icon(LucideIcons.chevronRight),
onPressed: onDownload,
child: const Text('下载'),
),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.share2, size: 16),
trailing: const Icon(LucideIcons.chevronRight),
onPressed: onShare,
child: const Text('分享'),
),
const Divider(height: 8),
ShadContextMenuItem.inset(
leading: Icon(LucideIcons.trash2, size: 16, color: theme.colorScheme.destructive),
trailing: Icon(LucideIcons.chevronRight, color: theme.colorScheme.destructive),
onPressed: onDelete,
child: Text(
'删除',
style: TextStyle(color: theme.colorScheme.destructive),
),
),
],
child: GestureDetector(
onTap: () => onSelect?.call(),
onDoubleTap: onOpen,
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: isSelected
? (isDark
? theme.colorScheme.primary.withAlpha(30)
: theme.colorScheme.primary.withAlpha(15))
: (isDark
? Colors.white.withAlpha(3)
: Colors.black.withAlpha(2)),
border: Border(
bottom: BorderSide(
color: isDark
? Colors.white.withAlpha(10)
: Colors.black.withAlpha(8),
width: 0.5,
),
),
),
child: Row(
children: [
// Selection indicator
if (isSelected)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(
LucideIcons.checkCircle,
size: 18,
color: theme.colorScheme.primary,
),
),
// File icon
SizedBox(
width: 32,
height: 32,
child: FileIcon(file: file),
),
const SizedBox(width: 12),
// File name
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: theme.colorScheme.foreground,
),
overflow: TextOverflow.ellipsis,
),
if (file.isDirectory && file.subFileCount != null)
Text(
'${file.subFileCount} 个项目',
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.mutedForeground,
),
),
],
),
),
// File size
if (!file.isDirectory)
SizedBox(
width: 80,
child: Text(
file.formattedSize,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.mutedForeground,
),
),
),
const SizedBox(width: 12),
// Modified date
SizedBox(
width: 120,
child: Text(
file.modifiedAt,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.mutedForeground,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
}
+302
View File
@@ -0,0 +1,302 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../providers/file_provider.dart';
import '../providers/auth_provider.dart';
import '../models/cloud_file.dart';
class SidePanel extends ConsumerWidget {
const SidePanel({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ShadTheme.of(context);
return Container(
width: 280,
decoration: BoxDecoration(
color: theme.colorScheme.card,
border: Border(left: BorderSide(color: theme.colorScheme.border)),
),
child: Column(
children: [
Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(LucideIcons.info, size: 18, color: theme.colorScheme.foreground),
const SizedBox(width: 8),
Text(
'详情',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: theme.colorScheme.foreground,
),
),
],
),
),
const ShadSeparator.horizontal(),
Expanded(child: _buildContent(context, ref)),
],
),
);
}
Widget _buildContent(BuildContext context, WidgetRef ref) {
final fileState = ref.watch(fileProvider);
final selected = fileState.files
.where((f) => fileState.selectedIDs.contains(f.id))
.toList();
if (selected.isEmpty) {
return _buildDefaultContent(context, ref);
}
if (selected.length == 1) {
return _buildSingleFileDetail(context, selected.first, ref);
}
return _buildMultiSelectDetail(context, selected, ref);
}
Widget _buildDefaultContent(BuildContext context, WidgetRef ref) {
final auth = ref.watch(authProvider);
final fileState = ref.watch(fileProvider);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(context, '账号信息'),
const SizedBox(height: 12),
_infoRow(context, '用户名', auth.userName),
_infoRow(context, '会员等级', auth.memberLevel),
_infoRow(context, '存储空间', auth.capacityText),
const SizedBox(height: 24),
if (fileState.section == WorkspaceSection.recycle) ...[
_sectionTitle(context, '回收站'),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ShadButton.outline(
onPressed: () {
showShadDialog(
context: context,
builder: (context) => ShadDialog(
title: const Text('清空回收站'),
description: const Padding(
padding: EdgeInsets.only(bottom: 8),
child: Text('确定要永久删除回收站中的所有文件吗?此操作不可恢复。'),
),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.of(context).pop(),
),
ShadButton(
child: const Text('清空'),
onPressed: () {
ref.read(fileProvider.notifier).clearRecycleBin();
Navigator.of(context).pop();
},
),
],
),
);
},
leading: const Icon(LucideIcons.trash2, size: 16),
child: const Text('清空回收站'),
),
),
],
],
),
);
}
Widget _buildSingleFileDetail(
BuildContext context, CloudFile file, WidgetRef ref) {
final theme = ShadTheme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
Icon(
file.isDirectory ? LucideIcons.folder : LucideIcons.file,
size: 64,
color: file.isDirectory
? const Color(0xFFF59E0B)
: theme.colorScheme.primary,
),
const SizedBox(height: 12),
Text(
file.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: theme.colorScheme.foreground,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 24),
_sectionTitle(context, '文件信息'),
const SizedBox(height: 12),
_infoRow(context, '类型', file.typeName),
if (!file.isDirectory) _infoRow(context, '大小', file.formattedSize),
if (file.modifiedAt.isNotEmpty)
_infoRow(context, '修改时间', file.modifiedAt),
const SizedBox(height: 24),
_sectionTitle(context, '操作'),
const SizedBox(height: 12),
_buildActionButtons(context, file, ref),
],
),
);
}
Widget _buildMultiSelectDetail(
BuildContext context, List<CloudFile> files, WidgetRef ref) {
final theme = ShadTheme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
Icon(
LucideIcons.checkCircle,
size: 48,
color: theme.colorScheme.primary,
),
const SizedBox(height: 12),
Text(
'已选择 ${files.length} 个项目',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: theme.colorScheme.foreground,
),
),
],
),
),
const SizedBox(height: 24),
_sectionTitle(context, '批量操作'),
const SizedBox(height: 12),
_buildActionButtons(context, files.first, ref),
],
),
);
}
Widget _buildActionButtons(
BuildContext context, CloudFile file, WidgetRef ref) {
final theme = ShadTheme.of(context);
final fp = ref.read(fileProvider.notifier);
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (file.isDirectory)
_actionChip(
context,
icon: LucideIcons.folderOpen,
label: '打开',
onTap: () => fp.navigateToFolder(file),
),
_actionChip(
context,
icon: LucideIcons.copy,
label: '复制',
onTap: () => fp.copyToClipboard([file]),
),
_actionChip(
context,
icon: LucideIcons.scissors,
label: '剪切',
onTap: () => fp.cutToClipboard([file]),
),
_actionChip(
context,
icon: LucideIcons.download,
label: '下载',
onTap: () => fp.downloadFile(file),
),
_actionChip(
context,
icon: LucideIcons.share2,
label: '分享',
onTap: () {},
),
_actionChip(
context,
icon: LucideIcons.trash2,
label: '删除',
color: theme.colorScheme.destructive,
onTap: () => fp.deleteFiles([file]),
),
],
);
}
Widget _actionChip(
BuildContext context, {
required IconData icon,
required String label,
Color? color,
required VoidCallback onTap,
}) {
return ShadButton.outline(
onPressed: onTap,
leading: Icon(icon, size: 14, color: color),
child: Text(label, style: TextStyle(fontSize: 12, color: color)),
);
}
Widget _sectionTitle(BuildContext context, String title) {
final theme = ShadTheme.of(context);
return Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: theme.colorScheme.mutedForeground,
),
);
}
Widget _infoRow(BuildContext context, String label, String value) {
final theme = ShadTheme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(fontSize: 13, color: theme.colorScheme.mutedForeground),
),
Flexible(
child: Text(
value,
style: TextStyle(fontSize: 13, color: theme.colorScheme.foreground),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../providers/file_provider.dart';
class SortMenu extends StatelessWidget {
final FileSort currentSort;
final SortDirection currentDirection;
final ValueChanged<FileSort> onSortChanged;
const SortMenu({
super.key,
required this.currentSort,
required this.currentDirection,
required this.onSortChanged,
});
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return ShadButton.ghost(
onPressed: () => _showSortMenu(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.sort_rounded, size: 16, color: cs.mutedForeground),
const SizedBox(width: 4),
Text('排序', style: TextStyle(fontSize: 13, color: cs.mutedForeground)),
],
),
);
}
void _showSortMenu(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
showModalBottomSheet(
context: context,
builder: (ctx) => Container(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Text('排序方式',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.foreground)),
const Spacer(),
Text('当前: ${currentSort.title}',
style: TextStyle(fontSize: 12, color: cs.mutedForeground)),
],
),
),
const ShadSeparator.horizontal(),
for (final sort in FileSort.values)
ListTile(
dense: true,
leading: currentSort == sort
? Icon(Icons.check_rounded, size: 16, color: cs.primary)
: const SizedBox(width: 16),
title: Text(
sort.title,
style: TextStyle(
fontSize: 14,
fontWeight: currentSort == sort ? FontWeight.w600 : FontWeight.normal,
color: currentSort == sort ? cs.primary : cs.foreground,
),
),
trailing: currentSort == sort
? Icon(
currentDirection == SortDirection.ascending
? Icons.arrow_upward_rounded
: Icons.arrow_downward_rounded,
size: 14,
color: cs.primary,
)
: null,
onTap: () {
Navigator.of(ctx).pop();
onSortChanged(sort);
},
),
],
),
),
);
}
}