二次开发源码提交

This commit is contained in:
2026-05-15 08:52:48 +08:00
parent 4131f7321a
commit eb35a1d067
191 changed files with 34566 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
import 'package:animations/animations.dart';
import 'package:cloudreve4_flutter/presentation/providers/auth_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/download_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/file_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/navigation_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/upload_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/widgets/gesture_handler_mixin.dart';
import 'package:cloudreve4_flutter/presentation/widgets/glassmorphism_container.dart';
import 'package:cloudreve4_flutter/presentation/widgets/user_avatar.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import '../../../router/app_router.dart';
import '../files/files_page.dart';
import '../overview/overview_page.dart';
import '../tasks/tasks_page.dart';
import '../profile/profile_page.dart';
class AppShell extends StatefulWidget {
const AppShell({super.key});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> with GestureHandlerMixin {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth >= 1000;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) {
final navProvider = Provider.of<NavigationProvider>(context, listen: false);
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
if (navProvider.currentIndex == 1 && fileManager.currentPath != '/') {
await fileManager.goBack();
} else if (navProvider.currentIndex != 0 && navProvider.currentIndex != 1) {
navProvider.setIndex(0);
} else {
await checkExitApp(context);
}
}
},
child: Consumer<NavigationProvider>(
builder: (context, navProvider, _) {
if (isDesktop) {
return _buildDesktopLayout(context, navProvider);
}
return _buildMobileLayout(context, navProvider);
},
),
);
}
Widget _buildPageContent(BuildContext context, int currentIndex) {
const pages = [
OverviewPage(),
FilesPage(),
TasksPage(),
ProfilePage(),
];
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation, secondaryAnimation) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: KeyedSubtree(
key: ValueKey(currentIndex),
child: pages[currentIndex],
),
);
}
Widget _buildMobileLayout(BuildContext context, NavigationProvider navProvider) {
return Scaffold(
body: _buildPageContent(context, navProvider.currentIndex),
bottomNavigationBar: GlassmorphismContainer(
borderRadius: 0,
child: Consumer2<UploadManagerProvider, DownloadManagerProvider>(
builder: (context, uploadManager, downloadManager, _) {
final activeCount = uploadManager.activeTasks.length + downloadManager.downloadingCount;
return NavigationBar(
height: 64,
selectedIndex: navProvider.currentIndex,
onDestinationSelected: (i) => navProvider.setIndex(i),
destinations: [
const NavigationDestination(
icon: Icon(LucideIcons.layoutDashboard),
selectedIcon: Icon(LucideIcons.layoutDashboard, weight: 700),
label: '概览',
),
const NavigationDestination(
icon: Icon(LucideIcons.folder),
selectedIcon: Icon(LucideIcons.folder, weight: 700),
label: '文件',
),
NavigationDestination(
icon: Badge(
isLabelVisible: activeCount > 0,
label: Text('$activeCount'),
child: const Icon(LucideIcons.listChecks),
),
selectedIcon: Badge(
isLabelVisible: activeCount > 0,
label: Text('$activeCount'),
child: const Icon(LucideIcons.listChecks, weight: 700),
),
label: '任务',
),
const NavigationDestination(
icon: Icon(LucideIcons.user),
selectedIcon: Icon(LucideIcons.user, weight: 700),
label: '我的',
),
],
);
},
),
),
);
}
Widget _buildDesktopLayout(BuildContext context, NavigationProvider navProvider) {
final theme = Theme.of(context);
final authProvider = context.watch<AuthProvider>();
final user = authProvider.user;
final displayName = user?.nickname ?? '用户';
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: navProvider.currentIndex,
onDestinationSelected: (i) => navProvider.setIndex(i),
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: GestureDetector(
onTap: () => navProvider.setIndex(3),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: navProvider.currentIndex == 3
? Border.all(
color: theme.colorScheme.primary,
width: 2.5,
)
: null,
),
child: UserAvatar(
userId: user?.id ?? '',
email: user?.email,
displayName: displayName,
radius: 20,
),
),
),
),
destinations: [
const NavigationRailDestination(
icon: Icon(LucideIcons.layoutDashboard),
selectedIcon: Icon(LucideIcons.layoutDashboard, weight: 700),
label: Text('概览'),
),
const NavigationRailDestination(
icon: Icon(LucideIcons.folder),
selectedIcon: Icon(LucideIcons.folder, weight: 700),
label: Text('文件'),
),
NavigationRailDestination(
icon: Consumer2<UploadManagerProvider, DownloadManagerProvider>(
builder: (context, uploadManager, downloadManager, _) {
final activeCount = uploadManager.activeTasks.length + downloadManager.downloadingCount;
return Badge(
isLabelVisible: activeCount > 0,
label: Text('$activeCount'),
child: const Icon(LucideIcons.listChecks),
);
},
),
selectedIcon: const Icon(LucideIcons.listChecks, weight: 700),
label: const Text('任务'),
),
const NavigationRailDestination(
icon: Icon(LucideIcons.user),
selectedIcon: Icon(LucideIcons.user, weight: 700),
label: Text('我的'),
),
],
trailing: Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Divider(indent: 12, endIndent: 12),
_buildSecondaryNavItem(
context,
icon: LucideIcons.share2,
label: '我的分享',
onTap: () => Navigator.of(context).pushNamed(RouteNames.share),
),
_buildSecondaryNavItem(
context,
icon: LucideIcons.cloud,
label: 'WebDAV',
onTap: () => Navigator.of(context).pushNamed(RouteNames.webdav),
),
_buildSecondaryNavItem(
context,
icon: LucideIcons.download,
label: '离线下载',
onTap: () => Navigator.of(context).pushNamed(RouteNames.remoteDownload),
),
_buildSecondaryNavItem(
context,
icon: LucideIcons.trash2,
label: '回收站',
onTap: () => Navigator.of(context).pushNamed(RouteNames.recycleBin),
),
const Divider(indent: 12, endIndent: 12),
_buildSecondaryNavItem(
context,
icon: LucideIcons.settings,
label: '设置',
onTap: () => Navigator.of(context).pushNamed(RouteNames.settings),
),
_buildSecondaryNavItem(
context,
icon: LucideIcons.logOut,
label: '退出登录',
onTap: () => _handleLogout(context),
),
const SizedBox(height: 12),
],
),
),
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: _buildPageContent(context, navProvider.currentIndex),
),
],
),
);
}
Widget _buildSecondaryNavItem(
BuildContext context, {
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap,
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Tooltip(
message: label,
preferBelow: false,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Icon(icon, size: 22, color: theme.hintColor),
),
),
);
}
Future<void> _handleLogout(BuildContext context) async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('退出'),
),
],
),
);
if (confirmed == true) {
await authProvider.logout();
fileManager.clearFiles();
if (context.mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(RouteNames.login, (route) => false);
}
}
}
}