feat: Establish initial application structure, theming, responsive utilities, and core feature screens.

This commit is contained in:
Michael committed 2025-12-06 16:38:15 +01:00
1 parent 33239ca11e
commit d2c92215e9
10 files changed
+1754 -332

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
import '../api/api_client.dart';
import '../models/app_user.dart';
/// Service for user management via API (Admin only)
class UserApiService {
final ApiClient _api = ApiClient();
/// Get all users (Admin only)
Future<List<AppUser>> getAllUsers() async {
try {
_api.restoreToken();
final response = await _api.get('/api/users');
final data = response.data as Map<String, dynamic>;
if (data['success'] == true) {
final usersData = data['users'] as List;
return usersData.map((u) => AppUser(
id: u['id'] as String,
username: u['username'] as String,
passwordHash: '',
isAdmin: u['isAdmin'] as bool? ?? false,
createdAt: DateTime.tryParse(u['createdAt'] ?? '') ?? DateTime.now(),
)).toList();
}
return [];
} catch (e) {
throw Exception('Failed to fetch users: $e');
}
}
/// Create a new user (Admin only)
Future<UserResult> createUser({
required String username,
required String password,
bool isAdmin = false,
}) async {
try {
_api.restoreToken();
final response = await _api.post('/api/users', data: {
'username': username,
'password': password,
'isAdmin': isAdmin,
});
final data = response.data as Map<String, dynamic>;
if (data['success'] == true) {
final userData = data['user'] as Map<String, dynamic>;
return UserResult(
success: true,
user: AppUser(
id: userData['id'] as String,
username: userData['username'] as String,
passwordHash: '',
isAdmin: userData['isAdmin'] as bool? ?? false,
createdAt: DateTime.now(),
),
);
}
return UserResult(
success: false,
error: data['error'] as String? ?? 'Failed to create user',
);
} catch (e) {
return UserResult(success: false, error: 'Network error: $e');
}
}
/// Update user password (Admin only)
Future<UserResult> updatePassword({
required String userId,
required String newPassword,
}) async {
try {
_api.restoreToken();
final response = await _api.put('/api/users/$userId/password', data: {
'password': newPassword,
});
final data = response.data as Map<String, dynamic>;
if (data['success'] == true) {
return UserResult(success: true);
}
return UserResult(
success: false,
error: data['error'] as String? ?? 'Failed to update password',
);
} catch (e) {
return UserResult(success: false, error: 'Network error: $e');
}
}
/// Delete a user (Admin only)
Future<UserResult> deleteUser(String userId) async {
try {
_api.restoreToken();
final response = await _api.delete('/api/users/$userId');
final data = response.data as Map<String, dynamic>;
if (data['success'] == true) {
return UserResult(success: true);
}
return UserResult(
success: false,
error: data['error'] as String? ?? 'Failed to delete user',
);
} catch (e) {
return UserResult(success: false, error: 'Network error: $e');
}
}
/// Toggle admin status (Admin only)
Future<UserResult> toggleAdmin(String userId, bool isAdmin) async {
try {
_api.restoreToken();
final response = await _api.put('/api/users/$userId', data: {
'isAdmin': isAdmin,
});
final data = response.data as Map<String, dynamic>;
if (data['success'] == true) {
return UserResult(success: true);
}
return UserResult(
success: false,
error: data['error'] as String? ?? 'Failed to update user',
);
} catch (e) {
return UserResult(success: false, error: 'Network error: $e');
}
}
}
/// Result of user operation
class UserResult {
final bool success;
final AppUser? user;
final String? error;
UserResult({
required this.success,
this.user,
this.error,
});
}
+94
View File
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
/// XtremFlow Premium Color Palette
///
/// Modern dark theme with cyan/teal gradients
class AppColors {
AppColors._();
// ============ PRIMARY GRADIENT ============
/// Cyan primary - main accent color
static const Color primary = Color(0xFF00D9FF);
/// Teal secondary - gradient end
static const Color secondary = Color(0xFF00BFA5);
/// Primary gradient for buttons and highlights
static const LinearGradient primaryGradient = LinearGradient(
colors: [primary, secondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ============ SURFACE COLORS ============
/// Deep dark background
static const Color background = Color(0xFF0A0E14);
/// Card/elevated surface
static const Color surface = Color(0xFF141A22);
/// Lighter surface variant (hover states, inputs)
static const Color surfaceVariant = Color(0xFF1E2630);
/// Border/divider color
static const Color border = Color(0xFF2A3441);
/// Overlay color (modals, dialogs)
static const Color overlay = Color(0xCC0A0E14);
// ============ TEXT COLORS ============
/// Primary text - white with high opacity
static const Color textPrimary = Color(0xFFF5F5F5);
/// Secondary text - muted
static const Color textSecondary = Color(0xFFB0BEC5);
/// Disabled/hint text
static const Color textDisabled = Color(0xFF607D8B);
// ============ ACCENT COLORS ============
/// Live indicator red
static const Color live = Color(0xFFFF6B6B);
/// Success green
static const Color success = Color(0xFF4ADE80);
/// Warning amber
static const Color warning = Color(0xFFFBBF24);
/// Error red
static const Color error = Color(0xFFEF4444);
/// Info blue
static const Color info = Color(0xFF3B82F6);
// ============ CATEGORY COLORS ============
/// Colors for category chips/tags
static const List<Color> categoryColors = [
Color(0xFF6366F1), // Indigo
Color(0xFFEC4899), // Pink
Color(0xFF8B5CF6), // Purple
Color(0xFF14B8A6), // Teal
Color(0xFFF59E0B), // Amber
Color(0xFF10B981), // Emerald
];
// ============ GLASSMORPHISM ============
/// Glass effect background color
static const Color glassBackground = Color(0x1AFFFFFF);
/// Glass border color
static const Color glassBorder = Color(0x33FFFFFF);
// ============ DARK THEME COLOR SCHEME ============
static ColorScheme get darkColorScheme => const ColorScheme.dark(
primary: primary,
secondary: secondary,
surface: surface,
error: error,
onPrimary: Color(0xFF000000),
onSecondary: Color(0xFF000000),
onSurface: textPrimary,
onError: Color(0xFFFFFFFF),
);
}
+391
View File
@@ -0,0 +1,391 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
/// XtremFlow Modern Theme
///
/// Complete theme with typography, spacing, and component theming
class AppTheme {
AppTheme._();
// ============ SPACING SCALE ============
static const double spacing4 = 4.0;
static const double spacing8 = 8.0;
static const double spacing12 = 12.0;
static const double spacing16 = 16.0;
static const double spacing24 = 24.0;
static const double spacing32 = 32.0;
static const double spacing48 = 48.0;
// ============ BORDER RADIUS ============
static const double radiusSm = 8.0;
static const double radiusMd = 12.0;
static const double radiusLg = 16.0;
static const double radiusXl = 24.0;
static const double radiusFull = 999.0;
// ============ ANIMATION DURATIONS ============
static const Duration durationFast = Duration(milliseconds: 150);
static const Duration durationNormal = Duration(milliseconds: 250);
static const Duration durationSlow = Duration(milliseconds: 400);
// ============ ANIMATION CURVES ============
static const Curve curveDefault = Curves.easeOutCubic;
static const Curve curveEmphasized = Curves.easeInOutCubic;
// ============ DARK THEME ============
static ThemeData get darkTheme {
final baseTheme = ThemeData.dark(useMaterial3: true);
return baseTheme.copyWith(
colorScheme: AppColors.darkColorScheme,
scaffoldBackgroundColor: AppColors.background,
// AppBar
appBarTheme: AppBarTheme(
backgroundColor: AppColors.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: false,
titleTextStyle: GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
iconTheme: const IconThemeData(color: AppColors.textPrimary),
),
// Navigation Bar (bottom)
navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.surface,
surfaceTintColor: Colors.transparent,
indicatorColor: AppColors.primary.withOpacity(0.2),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primary,
);
}
return GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: AppColors.primary);
}
return const IconThemeData(color: AppColors.textSecondary);
}),
),
// Navigation Rail (desktop sidebar)
navigationRailTheme: NavigationRailThemeData(
backgroundColor: AppColors.surface,
indicatorColor: AppColors.primary.withOpacity(0.2),
selectedIconTheme: const IconThemeData(color: AppColors.primary),
unselectedIconTheme: const IconThemeData(color: AppColors.textSecondary),
selectedLabelTextStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
unselectedLabelTextStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
// Cards
cardTheme: CardThemeData(
color: AppColors.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
side: const BorderSide(color: AppColors.border, width: 1),
),
margin: EdgeInsets.zero,
),
// Inputs
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: AppColors.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: AppColors.error),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: spacing16,
vertical: spacing12,
),
hintStyle: GoogleFonts.inter(
color: AppColors.textDisabled,
fontSize: 14,
),
labelStyle: GoogleFonts.inter(
color: AppColors.textSecondary,
fontSize: 14,
),
),
// Buttons
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: spacing24,
vertical: spacing12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
textStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary),
padding: const EdgeInsets.symmetric(
horizontal: spacing24,
vertical: spacing12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
textStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primary,
textStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
// Chips
chipTheme: ChipThemeData(
backgroundColor: AppColors.surfaceVariant,
selectedColor: AppColors.primary.withOpacity(0.2),
labelStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
side: const BorderSide(color: AppColors.border),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusFull),
),
padding: const EdgeInsets.symmetric(horizontal: spacing8),
),
// Dialogs
dialogTheme: DialogThemeData(
backgroundColor: AppColors.surface,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusLg),
),
titleTextStyle: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
contentTextStyle: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textSecondary,
),
),
// Snackbars
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.surfaceVariant,
contentTextStyle: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textPrimary,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
behavior: SnackBarBehavior.floating,
),
// Tabs
tabBarTheme: TabBarThemeData(
labelColor: AppColors.primary,
unselectedLabelColor: AppColors.textSecondary,
labelStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
indicatorColor: AppColors.primary,
indicatorSize: TabBarIndicatorSize.tab,
dividerColor: AppColors.border,
),
// Dividers
dividerTheme: const DividerThemeData(
color: AppColors.border,
thickness: 1,
space: 1,
),
// List tiles
listTileTheme: ListTileThemeData(
tileColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
horizontal: spacing16,
vertical: spacing8,
),
titleTextStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
subtitleTextStyle: GoogleFonts.inter(
fontSize: 12,
color: AppColors.textSecondary,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
// Progress indicators
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.primary,
linearTrackColor: AppColors.surfaceVariant,
circularTrackColor: AppColors.surfaceVariant,
),
// Switch
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primary;
}
return AppColors.textSecondary;
}),
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primary.withOpacity(0.3);
}
return AppColors.surfaceVariant;
}),
),
// Text theme
textTheme: TextTheme(
displayLarge: GoogleFonts.inter(
fontSize: 57,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
),
displayMedium: GoogleFonts.inter(
fontSize: 45,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
),
displaySmall: GoogleFonts.inter(
fontSize: 36,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
),
headlineLarge: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
headlineMedium: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
headlineSmall: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
titleLarge: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
titleMedium: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
titleSmall: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
bodyLarge: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
),
bodyMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
),
bodySmall: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.textSecondary,
),
labelLarge: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
labelMedium: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
labelSmall: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
);
}
}
+122
View File
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
/// Responsive breakpoints and layout utilities
class ResponsiveLayout {
ResponsiveLayout._();
// ============ BREAKPOINTS ============
static const double mobileBreakpoint = 600;
static const double tabletBreakpoint = 900;
static const double desktopBreakpoint = 1200;
/// Check if current screen is mobile size
static bool isMobile(BuildContext context) {
return MediaQuery.of(context).size.width < mobileBreakpoint;
}
/// Check if current screen is tablet size
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= mobileBreakpoint && width < desktopBreakpoint;
}
/// Check if current screen is desktop size
static bool isDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= desktopBreakpoint;
}
/// Get current device type
static DeviceType getDeviceType(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < mobileBreakpoint) return DeviceType.mobile;
if (width < desktopBreakpoint) return DeviceType.tablet;
return DeviceType.desktop;
}
/// Get responsive value based on screen size
static T value<T>(
BuildContext context, {
required T mobile,
T? tablet,
required T desktop,
}) {
final type = getDeviceType(context);
switch (type) {
case DeviceType.mobile:
return mobile;
case DeviceType.tablet:
return tablet ?? desktop;
case DeviceType.desktop:
return desktop;
}
}
/// Get grid columns based on screen size
static int gridColumns(BuildContext context) {
return value(context, mobile: 2, tablet: 3, desktop: 4);
}
/// Get content max width
static double maxContentWidth(BuildContext context) {
return value(context, mobile: double.infinity, tablet: 900, desktop: 1200);
}
}
/// Device types for responsive layouts
enum DeviceType { mobile, tablet, desktop }
/// Widget that adapts layout based on screen size
class AdaptiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const AdaptiveLayout({
super.key,
required this.mobile,
this.tablet,
required this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < ResponsiveLayout.mobileBreakpoint) {
return mobile;
}
if (constraints.maxWidth < ResponsiveLayout.desktopBreakpoint) {
return tablet ?? desktop;
}
return desktop;
},
);
}
}
/// Wrapper that constrains content width on large screens
class ResponsiveContainer extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
const ResponsiveContainer({
super.key,
required this.child,
this.padding = const EdgeInsets.symmetric(horizontal: 16),
});
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: ResponsiveLayout.maxContentWidth(context),
),
child: Padding(
padding: padding,
child: child,
),
),
);
}
}
@@ -0,0 +1,308 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../../theme/app_colors.dart';
import '../../theme/app_theme.dart';
/// Glassmorphism card widget with blur effect
class GlassCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
final double borderRadius;
final VoidCallback? onTap;
final bool showBorder;
const GlassCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(16),
this.borderRadius = 12,
this.onTap,
this.showBorder = true,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Material(
color: AppColors.glassBackground,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(borderRadius),
child: Container(
padding: padding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
border: showBorder
? Border.all(color: AppColors.glassBorder, width: 1)
: null,
),
child: child,
),
),
),
),
);
}
}
/// Animated gradient button
class GradientButton extends StatefulWidget {
final String label;
final VoidCallback? onPressed;
final IconData? icon;
final bool isLoading;
final bool isExpanded;
const GradientButton({
super.key,
required this.label,
this.onPressed,
this.icon,
this.isLoading = false,
this.isExpanded = false,
});
@override
State<GradientButton> createState() => _GradientButtonState();
}
class _GradientButtonState extends State<GradientButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: AppTheme.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final button = AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: Container(
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.isLoading ? null : widget.onPressed,
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _controller.reverse(),
onTapCancel: () => _controller.reverse(),
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 14,
),
child: widget.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.black,
),
)
: Row(
mainAxisSize:
widget.isExpanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.icon != null) ...[
Icon(widget.icon, color: Colors.black, size: 20),
const SizedBox(width: 8),
],
Text(
widget.label,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
),
),
),
),
);
return widget.isExpanded
? SizedBox(width: double.infinity, child: button)
: button;
}
}
/// Shimmer loading effect widget
class ShimmerLoading extends StatefulWidget {
final double width;
final double height;
final double borderRadius;
const ShimmerLoading({
super.key,
this.width = double.infinity,
required this.height,
this.borderRadius = 8,
});
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(begin: -2, end: 2).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
gradient: LinearGradient(
begin: Alignment(_animation.value - 1, 0),
end: Alignment(_animation.value + 1, 0),
colors: const [
AppColors.surfaceVariant,
AppColors.surface,
AppColors.surfaceVariant,
],
),
),
);
},
);
}
}
/// Live indicator badge
class LiveBadge extends StatelessWidget {
const LiveBadge({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.live,
borderRadius: BorderRadius.circular(4),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, size: 6, color: Colors.white),
SizedBox(width: 4),
Text(
'LIVE',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
/// Category chip with color
class CategoryChip extends StatelessWidget {
final String label;
final bool isSelected;
final VoidCallback? onTap;
const CategoryChip({
super.key,
required this.label,
this.isSelected = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: isSelected
? AppColors.primary.withOpacity(0.2)
: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
border: Border.all(
color: isSelected ? AppColors.primary : AppColors.border,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? AppColors.primary : AppColors.textSecondary,
),
),
),
),
);
}
}