622 lines
26 KiB
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|