Files
b0esche_cloud/b0esche_cloud/lib/pages/home_page.dart

622 lines
26 KiB
Dart

import 'dart:ui' as ui;
import 'package:b0esche_cloud/blocs/organization/organization_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/auth/auth_bloc.dart';
import '../blocs/auth/auth_state.dart';
import '../blocs/organization/organization_bloc.dart';
import '../blocs/organization/organization_event.dart';
import '../blocs/file_browser/file_browser_bloc.dart';
import '../blocs/file_browser/file_browser_event.dart';
import '../blocs/permission/permission_bloc.dart';
import '../blocs/upload/upload_bloc.dart';
import '../repositories/file_repository.dart';
import '../services/file_service.dart';
import '../services/org_api.dart';
import '../theme/app_theme.dart';
import '../theme/modern_glass_button.dart';
import 'login_form.dart' show LoginForm;
import 'file_explorer.dart';
import '../injection.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
late String _selectedTab = 'Drive';
late AnimationController _animationController;
bool _isSignupMode = false;
bool _usePasswordMode = false;
// Shared blocs for the page lifecycle
late final PermissionBloc _permissionBloc;
late final FileBrowserBloc _fileBrowserBloc;
late final UploadBloc _uploadBloc;
late final OrganizationBloc _organizationBloc;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_permissionBloc = PermissionBloc();
_fileBrowserBloc = FileBrowserBloc(getIt<FileService>());
_uploadBloc = UploadBloc(getIt<FileRepository>());
_organizationBloc = OrganizationBloc(
_permissionBloc,
_fileBrowserBloc,
_uploadBloc,
getIt<OrgApi>(),
);
}
@override
void dispose() {
_animationController.dispose();
_organizationBloc.close();
_uploadBloc.close();
_fileBrowserBloc.close();
_permissionBloc.close();
super.dispose();
}
void _setSignupMode(bool isSignup) {
if (_isSignupMode && !isSignup) {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) setState(() => _isSignupMode = isSignup);
});
} else {
setState(() => _isSignupMode = isSignup);
}
}
void _setPasswordMode(bool usePassword) {
setState(() => _usePasswordMode = usePassword);
}
void _showCreateOrgDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
child: SizedBox(
width: 400,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: const EdgeInsets.all(24),
decoration: AppTheme.glassDecoration,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Create New Organization',
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
cursorColor: AppTheme.accentColor,
controller: controller,
style: TextStyle(color: AppTheme.primaryText),
decoration: InputDecoration(
labelText: 'Organization Name',
labelStyle: TextStyle(color: AppTheme.secondaryText),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppTheme.accentColor.withValues(alpha: 0.5),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppTheme.accentColor),
),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ModernGlassButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
const SizedBox(width: 16),
ModernGlassButton(
onPressed: () {
final name = controller.text.trim();
if (name.isNotEmpty) {
context.read<OrganizationBloc>().add(
CreateOrganization(name),
);
Navigator.of(context).pop();
}
},
child: const Text('Create'),
),
],
),
],
),
),
),
),
),
),
);
}
Widget _buildOrgRow(BuildContext context) {
return BlocBuilder<OrganizationBloc, OrganizationState>(
builder: (context, state) {
List<Organization> orgs = [];
Organization? selectedOrg;
bool isLoading = false;
if (state is OrganizationLoaded) {
orgs = state.organizations;
selectedOrg = state.selectedOrg;
isLoading = state.isLoading;
} else if (state is OrganizationLoading) {
isLoading = true;
}
return Column(
children: [
Row(
children: [
...orgs.map(
(org) => Row(
children: [
_buildOrgButton(org, org.id == selectedOrg?.id, () {
context.read<OrganizationBloc>().add(
SelectOrganization(org.id),
);
}),
const SizedBox(width: 16),
],
),
),
if (isLoading)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppTheme.accentColor,
),
),
)
else
_buildAddButton(() => _showCreateOrgDialog(context)),
],
),
const Divider(height: 1),
],
);
},
);
}
Widget _buildOrgButton(Organization org, bool selected, VoidCallback onTap) {
final highlightColor = const Color.fromARGB(255, 100, 200, 255);
final defaultColor = AppTheme.secondaryText;
return TextButton(
onPressed: onTap,
child: Text(
org.name,
style: TextStyle(
color: selected ? highlightColor : defaultColor,
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
),
),
);
}
Widget _buildAddButton(VoidCallback onTap) {
final defaultColor = AppTheme.secondaryText;
return TextButton(
onPressed: onTap,
child: Text('+ Add Organization', style: TextStyle(color: defaultColor)),
);
}
Widget _buildDrive(OrganizationState state, AuthState authState) {
String orgId;
if (state is OrganizationLoaded && state.selectedOrg != null) {
// Show selected organization's files
orgId = state.selectedOrg!.id;
} else if (authState is AuthAuthenticated) {
// Show personal workspace - use empty string to trigger /user/files endpoint
orgId = '';
} else {
orgId = '';
}
return FileExplorer(orgId: orgId);
}
Widget _buildNavButton(String label, IconData icon, {bool isAvatar = false}) {
final isSelected = _selectedTab == label;
final highlightColor = const Color.fromARGB(255, 100, 200, 255);
final defaultColor = AppTheme.secondaryText;
return GestureDetector(
onTap: () {
setState(() {
_selectedTab = label;
});
},
child: isAvatar
? CircleAvatar(
backgroundColor: isSelected ? highlightColor : defaultColor,
child: Icon(icon, color: AppTheme.primaryBackground),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? highlightColor : defaultColor,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isSelected ? highlightColor : defaultColor,
fontSize: 12,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<PermissionBloc>.value(value: _permissionBloc),
BlocProvider<FileBrowserBloc>.value(value: _fileBrowserBloc),
BlocProvider<UploadBloc>.value(value: _uploadBloc),
BlocProvider<OrganizationBloc>.value(value: _organizationBloc),
],
child: Scaffold(
backgroundColor: AppTheme.primaryBackground,
body: Stack(
children: [
Center(
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoggedIn = state is AuthAuthenticated;
if (isLoggedIn && !_animationController.isAnimating) {
_animationController.forward();
} else if (!isLoggedIn) {
_animationController.reverse();
}
return Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.width < 600
? 60.0
: 78.0,
),
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
width: isLoggedIn
? MediaQuery.of(context).size.width * 0.9
: 340,
height: isLoggedIn
? MediaQuery.of(context).size.height * 0.9
: (_isSignupMode
? 400
: (_usePasswordMode ? 350 : 280)),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Stack(
children: [
Container(
decoration: AppTheme.glassDecoration,
child: isLoggedIn
? BlocListener<
OrganizationBloc,
OrganizationState
>(
listener: (context, state) {
if (state is OrganizationLoaded) {
// Show errors if present
if (state.error != null &&
state.error!.isNotEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(state.error!),
),
);
}
final orgId =
state.selectedOrg?.id ?? '';
// Reload file browser when org changes (or when falling back to personal workspace)
context.read<FileBrowserBloc>().add(
LoadDirectory(
orgId: orgId,
path: '/',
),
);
}
},
child:
BlocBuilder<
OrganizationBloc,
OrganizationState
>(
builder: (context, orgState) {
if (orgState
is OrganizationInitial) {
WidgetsBinding.instance
.addPostFrameCallback((
_,
) {
// Kick off org fetch and immediately show personal workspace
// while org data loads.
context
.read<
OrganizationBloc
>()
.add(
LoadOrganizations(),
);
context
.read<
FileBrowserBloc
>()
.add(
const LoadDirectory(
orgId: '',
path: '/',
),
);
});
}
return Column(
children: [
const SizedBox(height: 16),
_buildOrgRow(context),
Expanded(
child: _buildDrive(
orgState,
state,
),
),
],
);
},
),
)
: LoginForm(
onSignupModeChanged: _setSignupMode,
onPasswordModeChanged: _setPasswordMode,
),
),
// Top-left radial glow - primary accent light
AnimatedPositioned(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
top: isLoggedIn ? -180 : -120,
left: isLoggedIn ? -180 : -120,
child: IgnorePointer(
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
width: isLoggedIn ? 550 : 400,
height: isLoggedIn ? 550 : 400,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppTheme.accentColor.withValues(
alpha: isLoggedIn ? 0.12 : 0.15,
),
AppTheme.accentColor.withValues(
alpha: 0.04,
),
Colors.transparent,
],
stops: const [0.0, 0.6, 1.0],
),
),
),
),
),
// Bottom-right warm glow - complementary lighting
AnimatedPositioned(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
bottom: isLoggedIn ? -200 : -140,
right: isLoggedIn ? -200 : -140,
child: IgnorePointer(
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
width: isLoggedIn ? 530 : 380,
height: isLoggedIn ? 530 : 380,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
Colors.cyan.withValues(
alpha: isLoggedIn ? 0.06 : 0.08,
),
Colors.transparent,
],
),
),
),
),
),
// Top edge subtle highlight
Positioned(
top: 0,
left: 0,
right: 0,
child: IgnorePointer(
child: Container(
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withValues(alpha: 0.05),
Colors.transparent,
],
),
),
),
),
),
// Left edge subtle side lighting
Positioned(
left: 0,
top: 0,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
AppTheme.accentColor.withValues(
alpha: 0.04,
),
Colors.transparent,
],
),
),
),
),
),
// Diagonal shimmer overlay
Positioned(
top: 0,
left: 0,
child: IgnorePointer(
child: Transform.rotate(
angle: 0.785,
child: Container(
width: 600,
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withValues(alpha: 0),
Colors.white.withValues(
alpha: 0.06,
),
Colors.white.withValues(alpha: 0),
],
),
),
),
),
),
),
],
),
),
),
),
);
},
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Center(
child: Builder(
builder: (context) {
final screenWidth = MediaQuery.of(context).size.width;
final fontSize = screenWidth < 600 ? 24.0 : 48.0;
return Text(
'b0esche.cloud',
style: TextStyle(
fontFamily: 'PixelatedElegance',
fontSize: fontSize,
color: AppTheme.primaryText,
decoration: TextDecoration.underline,
decorationColor: AppTheme.primaryText,
fontFeatures: const [FontFeature.slashedZero()],
),
);
},
),
),
),
Positioned(
top: MediaQuery.of(context).size.width < 600 ? 40 : 10,
right: 20,
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoggedIn = state is AuthAuthenticated;
if (!isLoggedIn) {
return const SizedBox.shrink();
}
return ScaleTransition(
scale: Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
),
),
child: Row(
children: [
_buildNavButton('Drive', Icons.cloud),
const SizedBox(width: 16),
_buildNavButton('Mail', Icons.mail),
const SizedBox(width: 16),
_buildNavButton('Add', Icons.add),
const SizedBox(width: 16),
_buildNavButton(
'Profile',
Icons.person,
isAvatar: true,
),
],
),
);
},
),
),
],
),
),
);
}
}