Files
iiEsaywebUIapp/lib/shared/widgets/empty_states.dart

449 lines
14 KiB
Dart
Raw Normal View History

2025-08-10 01:20:45 +05:30
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'dart:io' show Platform;
import '../theme/theme_extensions.dart';
import '../services/brand_service.dart';
/// Enhanced empty state widgets with illustrations and actions
class ConduitEmptyState extends StatelessWidget {
final String title;
final String? subtitle;
final IconData? icon;
final Widget? illustration;
final List<EmptyStateAction>? actions;
final bool isLoading;
const ConduitEmptyState({
super.key,
required this.title,
this.subtitle,
this.icon,
this.illustration,
this.actions,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
final conduitTheme = context.conduitTheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(Spacing.xl),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Illustration or icon
if (illustration != null)
illustration!
else if (icon != null)
Container(
width: IconSize.xxl * 2.5, // 120px equivalent
height: IconSize.xxl * 2.5, // 120px equivalent
decoration: BoxDecoration(
color: conduitTheme.cardBackground,
shape: BoxShape.circle,
border: Border.all(color: conduitTheme.cardBorder, width: 2),
),
child: Icon(
icon!,
size: IconSize.xxl,
color: context.conduitTheme.iconSecondary,
),
)
else
// Default to brand icon when no specific icon or illustration provided
BrandService.createBrandEmptyStateIcon(
size: IconSize.xxl * 2.5, // 120px equivalent
showBackground: true,
),
const SizedBox(height: Spacing.xl),
// Title
Text(
title,
style: conduitTheme.headingMedium,
textAlign: TextAlign.center,
),
// Subtitle
if (subtitle != null) ...[
const SizedBox(height: Spacing.xs),
Text(
subtitle!,
style: conduitTheme.bodyMedium?.copyWith(
color: context.conduitTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
// Actions
if (actions != null && actions!.isNotEmpty) ...[
const SizedBox(height: Spacing.xl),
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(bottom: Spacing.xs),
child: _buildActionButton(context, action),
),
),
],
],
),
),
).animate().fadeIn(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
Widget _buildActionButton(BuildContext context, EmptyStateAction action) {
return SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: action.onPressed,
style: action.isPrimary
? FilledButton.styleFrom(
backgroundColor: context.conduitTheme.buttonPrimary,
foregroundColor: context.conduitTheme.buttonPrimaryText,
)
: FilledButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: context.conduitTheme.textSecondary,
side: BorderSide(
color: context.conduitTheme.dividerColor,
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (action.icon != null) ...[
Icon(action.icon, size: IconSize.md),
const SizedBox(width: Spacing.sm),
],
Text(action.label),
],
),
),
);
}
}
/// Action for empty states
class EmptyStateAction {
final String label;
final VoidCallback onPressed;
final IconData? icon;
final bool isPrimary;
const EmptyStateAction({
required this.label,
required this.onPressed,
this.icon,
this.isPrimary = true,
});
}
/// Chat-specific empty state
class ChatEmptyState extends StatelessWidget {
final VoidCallback? onStartChat;
const ChatEmptyState({super.key, this.onStartChat});
@override
Widget build(BuildContext context) {
return ConduitEmptyState(
title: 'Start a conversation',
subtitle:
'Ask me anything! I\'m here to help with questions, creative tasks, analysis, and more.',
// Remove custom illustration to use default brand icon
icon: BrandService.primaryIcon,
actions: onStartChat != null
? [
EmptyStateAction(
label: 'Start chatting',
icon: BrandService.primaryIcon,
onPressed: onStartChat!,
),
]
: null,
);
}
}
/// Files empty state
class FilesEmptyState extends StatelessWidget {
final VoidCallback? onUploadFile;
const FilesEmptyState({super.key, this.onUploadFile});
@override
Widget build(BuildContext context) {
return ConduitEmptyState(
title: 'No files yet',
subtitle:
'Upload documents, images, or other files to get started with your knowledge base.',
illustration: Builder(
builder: (context) => _buildFilesIllustration(context),
),
actions: onUploadFile != null
? [
EmptyStateAction(
label: 'Upload files',
icon: Platform.isIOS
? CupertinoIcons.doc_on_doc
: Icons.upload_file,
onPressed: onUploadFile!,
),
]
: null,
);
}
Widget _buildFilesIllustration(BuildContext context) {
return SizedBox(
width: 120,
height: 120,
child: Stack(
alignment: Alignment.center,
children: [
// Background circle
Container(
width: IconSize.xxl * 2.5, // 120px equivalent
height: IconSize.xxl * 2.5, // 120px equivalent
decoration: BoxDecoration(
color: context.conduitTheme.info.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
),
// File stack
...List.generate(3, (index) {
return Positioned(
top: 30 + (index * 8.0),
left: 30 + (index * 4.0),
child:
Container(
width: TouchTarget.minimum,
height: 50,
decoration: BoxDecoration(
color: [
context.conduitTheme.info,
context.conduitTheme.success,
context.conduitTheme.warning,
][index],
borderRadius: BorderRadius.circular(
AppBorderRadius.xs,
),
),
child: Icon(
[Icons.description, Icons.image, Icons.folder][index],
color: context.conduitTheme.textInverse,
size: IconSize.md,
),
)
.animate(delay: Duration(milliseconds: index * 200))
.fadeIn()
.slideY(begin: 0.3, end: 0),
);
}),
],
),
);
}
}
/// Tools empty state
class ToolsEmptyState extends StatelessWidget {
final VoidCallback? onExploreTools;
const ToolsEmptyState({super.key, this.onExploreTools});
@override
Widget build(BuildContext context) {
return ConduitEmptyState(
title: 'Powerful tools await',
subtitle: 'Discover tools to enhance your productivity and creativity.',
illustration: Builder(
builder: (context) => _buildToolsIllustration(context),
),
actions: onExploreTools != null
? [
EmptyStateAction(
label: 'Explore tools',
icon: Platform.isIOS
? CupertinoIcons.wand_stars
: Icons.auto_awesome,
onPressed: onExploreTools!,
),
]
: null,
);
}
Widget _buildToolsIllustration(BuildContext context) {
return SizedBox(
width: 120,
height: 120,
child: Stack(
alignment: Alignment.center,
children: [
// Background circle
Container(
width: IconSize.xxl * 2.5, // 120px equivalent
height: IconSize.xxl * 2.5, // 120px equivalent
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
),
// Tools arrangement
...List.generate(6, (index) {
final angle = (index * 60) * (3.14159 / 180);
final radius = 35.0;
return Positioned(
top: 60 + (radius * -cos(angle)) - 15,
left: 60 + (radius * sin(angle)) - 15,
child:
Container(
width: Spacing.xl - Spacing.xxs, // 30px equivalent
height: Spacing.xl - Spacing.xxs, // 30px equivalent
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary,
shape: BoxShape.circle,
),
child: Icon(
[
Icons.palette,
Icons.calculate,
Icons.code,
Icons.translate,
Icons.music_note,
Icons.analytics,
][index],
color: context.conduitTheme.textInverse,
size: IconSize.sm,
),
)
.animate(delay: Duration(milliseconds: index * 100))
.fadeIn()
.scale(
begin: const Offset(0.5, 0.5),
end: const Offset(1.0, 1.0),
),
);
}),
],
),
);
}
}
/// Search results empty state
class SearchEmptyState extends StatelessWidget {
final String query;
final VoidCallback? onClearSearch;
const SearchEmptyState({super.key, required this.query, this.onClearSearch});
@override
Widget build(BuildContext context) {
return ConduitEmptyState(
title: 'No results found',
subtitle: 'No results for "$query". Try adjusting your search terms.',
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search_off,
actions: onClearSearch != null
? [
EmptyStateAction(
label: 'Clear search',
icon: Platform.isIOS ? CupertinoIcons.clear : Icons.clear,
onPressed: onClearSearch!,
isPrimary: false,
),
]
: null,
);
}
}
/// Connection error empty state
class ConnectionEmptyState extends StatelessWidget {
final VoidCallback? onRetry;
const ConnectionEmptyState({super.key, this.onRetry});
@override
Widget build(BuildContext context) {
return ConduitEmptyState(
title: 'Connection problem',
subtitle:
'Unable to load content. Please check your connection and try again.',
icon: Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off,
actions: onRetry != null
? [
EmptyStateAction(
label: 'Try again',
icon: Platform.isIOS ? CupertinoIcons.refresh : Icons.refresh,
onPressed: onRetry!,
),
]
: null,
);
}
}
/// Generic empty state with custom illustration
class CustomEmptyState extends StatelessWidget {
final String title;
final String subtitle;
final Widget illustration;
final List<EmptyStateAction>? actions;
const CustomEmptyState({
super.key,
required this.title,
required this.subtitle,
required this.illustration,
this.actions,
});
@override
Widget build(BuildContext context) {
return ConduitEmptyState(
title: title,
subtitle: subtitle,
illustration: illustration,
actions: actions,
);
}
}
// Helper function to get cosine
double cos(double radians) {
// Simple cosine approximation for illustration positioning
if (radians == 0) return 1.0;
if (radians == 1.5708) return 0.0; // π/2
if (radians == 3.14159) return -1.0; // π
if (radians == 4.71239) return 0.0; // 3π/2
// Taylor series approximation for other values
double x2 = radians * radians;
return 1 - x2 / 2 + x2 * x2 / 24 - x2 * x2 * x2 / 720;
}
// Helper function to get sine
double sin(double radians) {
// Simple sine approximation for illustration positioning
if (radians == 0) return 0.0;
if (radians == 1.5708) return 1.0; // π/2
if (radians == 3.14159) return 0.0; // π
if (radians == 4.71239) return -1.0; // 3π/2
// Taylor series approximation for other values
double x2 = radians * radians;
return radians - radians * x2 / 6 + radians * x2 * x2 / 120;
}