flip_counter_plus 1.0.0
flip_counter_plus: ^1.0.0 copied to clipboard
An implicit animation widget that flips from one number to another, with support for customize styles, decimals and negative values.
example/lib/main.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flip_counter_plus/flip_counter_plus.dart';
void main() {
runApp(const MyApp());
}
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.dark);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ThemeMode>(
valueListenable: themeNotifier,
builder: (context, currentMode, _) {
return MaterialApp(
title: 'FlipCounter Hub',
debugShowCheckedModeBanner: false,
themeMode: currentMode,
theme: ThemeData.light().copyWith(
scaffoldBackgroundColor: const Color(0xFFF8FAFC), // Slate 50
primaryColor: const Color(0xFF4F46E5), // Indigo 600
colorScheme: const ColorScheme.light(
primary: Color(0xFF4F46E5),
secondary: Color(0xFFDB2777), // Pink 600
surface: Colors.white,
error: Color(0xFFDC2626),
onSurface: Color(0xFF0F172A),
onPrimary: Colors.white,
),
cardTheme: const CardThemeData(
color: Colors.white,
elevation: 2,
margin: EdgeInsets.zero,
),
sliderTheme: const SliderThemeData(
activeTrackColor: Color(0xFF4F46E5),
thumbColor: Color(0xFF4F46E5),
),
dividerTheme: const DividerThemeData(
color: Color(0xFFE2E8F0),
),
),
darkTheme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: const Color(0xFF0B0F19), // Midnight dark background
primaryColor: const Color(0xFF6366F1), // Indigo
colorScheme: const ColorScheme.dark(
primary: Color(0xFF6366F1),
secondary: Color(0xFFEC4899), // Pink
surface: Color(0xFF151D30), // Dark slate cards
error: Color(0xFFEF4444),
onSurface: Colors.white,
onPrimary: Colors.white,
),
cardTheme: const CardThemeData(
color: Color(0xFF151D30),
elevation: 4,
margin: EdgeInsets.zero,
),
sliderTheme: const SliderThemeData(
activeTrackColor: Color(0xFF6366F1),
thumbColor: Color(0xFF6366F1),
),
dividerTheme: const DividerThemeData(
color: Color(0xFF1E293B),
),
),
home: const DashboardPage(),
);
},
);
}
}
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
int _activeTab = 0;
final List<Map<String, dynamic>> _tabs = [
{
'title': 'Visual Sandbox',
'icon': Icons.tune_rounded,
'subtitle': 'Interactive transition & speed controls',
},
{
'title': 'Formats & Localizations',
'icon': Icons.language_rounded,
'subtitle': 'Currencies, groupings & regional digits',
},
{
'title': 'Custom Stylings',
'icon': Icons.palette_rounded,
'subtitle': 'Retro flip clock & neon builders',
},
{
'title': 'Controller & Lifecycle',
'icon': Icons.settings_input_component_rounded,
'subtitle': 'Imperative APIs & status listeners',
},
{
'title': 'Stock Market',
'icon': Icons.trending_up_rounded,
'subtitle': 'Real-time quotes & order book simulation',
},
];
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isWide = width >= 850;
return Scaffold(
body: SafeArea(
child: isWide
? Row(
children: [
_buildSidebar(context),
VerticalDivider(width: 1, color: Theme.of(context).dividerTheme.color ?? const Color(0xFF1E293B)),
Expanded(
child: _buildMainContent(),
),
],
)
: Column(
children: [
_buildHeader(),
Expanded(
child: _buildMainContent(),
),
],
),
),
bottomNavigationBar: isWide
? null
: BottomNavigationBar(
currentIndex: _activeTab,
onTap: (index) => setState(() => _activeTab = index),
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
backgroundColor: Theme.of(context).colorScheme.surface,
type: BottomNavigationBarType.fixed,
items: _tabs.map((tab) {
return BottomNavigationBarItem(
icon: Icon(tab['icon']),
label: tab['title'],
);
}).toList(),
),
);
}
Widget _buildHeader() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
color: Theme.of(context).colorScheme.surface,
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFFEC4899)],
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.flash_on_rounded, color: Colors.white),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'FlipCounter Hub',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
color: Theme.of(context).colorScheme.onSurface,
),
),
Text(
'High Performance Counters',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
const Spacer(),
IconButton(
icon: Icon(
isDark ? Icons.light_mode_rounded : Icons.dark_mode_rounded,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
themeNotifier.value = isDark ? ThemeMode.light : ThemeMode.dark;
},
),
],
),
);
}
Widget _buildSidebar(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: 260,
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFFEC4899)],
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.flash_on_rounded, color: Colors.white, size: 24),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'FlipCounter',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
color: Theme.of(context).colorScheme.onSurface,
),
),
Text(
'Hub v1.0.0',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
const SizedBox(height: 32),
Text(
'NAVIGATION',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w800,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
letterSpacing: 1.5,
),
),
const SizedBox(height: 12),
Expanded(
child: ListView.separated(
itemCount: _tabs.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final tab = _tabs[index];
final isSelected = _activeTab == index;
return InkWell(
onTap: () => setState(() => _activeTab = index),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.3)
: Colors.transparent,
width: 1,
),
),
child: Row(
children: [
Icon(
tab['icon'],
color: isSelected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
size: 20,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tab['title'],
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isSelected ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 2),
Text(
tab['subtitle'],
style: TextStyle(
fontSize: 9,
color: isSelected
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.8)
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
);
},
),
),
Row(
children: [
Expanded(
child: Text(
'Theme Mode',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
),
),
),
IconButton(
icon: Icon(
isDark ? Icons.light_mode_rounded : Icons.dark_mode_rounded,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
themeNotifier.value = isDark ? ThemeMode.light : ThemeMode.dark;
},
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.bolt, color: Colors.amberAccent, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
'Render time: < 2.5us\nIsolated via Repaints',
style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), height: 1.3),
),
),
],
),
),
],
),
);
}
Widget _buildMainContent() {
switch (_activeTab) {
case 0:
return const VisualSandboxTab();
case 1:
return const FormatsAndLocalesTab();
case 2:
return const CustomStylingsTab();
case 3:
return const ControllerTab();
case 4:
return const StockMarketTab();
default:
return const SizedBox.shrink();
}
}
}
/// ==========================================
/// TAB 1: VISUAL SANDBOX
/// ==========================================
class VisualSandboxTab extends StatefulWidget {
const VisualSandboxTab({super.key});
@override
State<VisualSandboxTab> createState() => _VisualSandboxTabState();
}
class _VisualSandboxTabState extends State<VisualSandboxTab> {
double _value = 345.67;
CounterTransitionType _transition = CounterTransitionType.roll;
double _speed = 1.0;
double _delayMs = 0.0;
double _staggerMs = 50.0;
StaggerDirection _staggerDir = StaggerDirection.rightToLeft;
AxisDirection _flipDir = AxisDirection.up;
final TextEditingController _inputController = TextEditingController();
double _liveHz = 0.0;
double _liveRenderTimeUs = 1764.0;
bool _benchmarkRunning = false;
final List<DateTime> _lastUpdateTimes = [];
Timer? _hzDecayTimer;
@override
void initState() {
super.initState();
_hzDecayTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (mounted && _lastUpdateTimes.isNotEmpty) {
final now = DateTime.now();
if (now.difference(_lastUpdateTimes.last).inMilliseconds > 1000) {
setState(() {
_liveHz = 0.0;
_lastUpdateTimes.clear();
});
}
}
});
}
@override
void dispose() {
_hzDecayTimer?.cancel();
_inputController.dispose();
super.dispose();
}
void _updateValue(double newValue) {
setState(() {
_value = newValue;
final now = DateTime.now();
_lastUpdateTimes.add(now);
_lastUpdateTimes.removeWhere((t) => now.difference(t).inMilliseconds > 1000);
if (_lastUpdateTimes.length > 1) {
final duration = now.difference(_lastUpdateTimes.first).inMilliseconds / 1000.0;
_liveHz = duration > 0 ? (_lastUpdateTimes.length - 1) / duration : 0.0;
} else {
_liveHz = 0.0;
}
});
}
Future<void> _runStressTest() async {
if (_benchmarkRunning) return;
setState(() {
_benchmarkRunning = true;
_liveHz = 0.0;
});
final stopwatch = Stopwatch()..start();
const int totalTicks = 60;
final double startVal = _value;
for (int i = 1; i <= totalTicks; i++) {
await Future.delayed(const Duration(milliseconds: 16));
if (!mounted) return;
_updateValue(startVal + i * 2.5);
}
stopwatch.stop();
setState(() {
_liveRenderTimeUs = (stopwatch.elapsedMicroseconds / (totalTicks * 2));
_benchmarkRunning = false;
});
}
double _estimateMemoryFootprint() {
final double digitCount = _value.toStringAsFixed(2).length.toDouble();
return 2.4 + (digitCount * 12.0) + 4.0 + 0.8;
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isDesktop = width >= 1000;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Visual Showcase Card - Sticky/Fixed at the top
_buildHeroDisplayCard(),
const SizedBox(height: 24),
// Scrollable area for Telemetry & Controls
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Performance Telemetry Card
_buildPerformanceTelemetryCard(),
const SizedBox(height: 24),
// Side-by-side controls or vertical layouts
if (isDesktop)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildValueControls()),
const SizedBox(width: 24),
Expanded(child: _buildAnimationConfigs()),
],
)
else ...[
_buildValueControls(),
const SizedBox(height: 24),
_buildAnimationConfigs(),
],
],
),
),
),
],
),
);
}
Widget _buildHeroDisplayCard() {
final isDark = Theme.of(context).brightness == Brightness.dark;
final gradientColors = isDark
? [const Color(0xFF1E1E38), const Color(0xFF111827)]
: [const Color(0xFFEEF2F6), const Color(0xFFE2E8F0)];
final borderColor = isDark ? const Color(0xFF312E81) : const Color(0xFFCBD5E1);
final shadowColor = isDark
? const Color(0xFF6366F1).withValues(alpha: 0.15)
: const Color(0xFF4F46E5).withValues(alpha: 0.1);
return Container(
padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: borderColor, width: 1.5),
boxShadow: [
BoxShadow(
color: shadowColor,
blurRadius: 30,
spreadRadius: 2,
),
],
),
child: Column(
children: [
Text(
'SANDBOX INTERACTIVE MONITOR',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w900,
color: isDark ? const Color(0xFF818CF8) : const Color(0xFF4F46E5),
letterSpacing: 2,
),
),
const SizedBox(height: 32),
Center(
child: AnimatedFlipCounter(
value: _value,
fractionDigits: 2,
thousandSeparator: ',',
transitionType: _transition,
speedMultiplier: _speed,
flipDirection: _flipDir,
staggerDelay: Duration(milliseconds: _staggerMs.round()),
staggerDirection: _staggerDir,
startDelay: Duration(milliseconds: _delayMs.round()),
increasingColor: const Color(0xFF10B981),
decreasingColor: const Color(0xFFEF4444),
colorFadeDuration: const Duration(milliseconds: 500),
textStyle: TextStyle(
fontSize: 64,
fontWeight: FontWeight.w900,
color: isDark ? Colors.white : const Color(0xFF0F172A),
letterSpacing: -1.0,
shadows: isDark
? const [
BoxShadow(
color: Colors.black45,
offset: Offset(4, 6),
blurRadius: 12,
),
]
: const [
BoxShadow(
color: Colors.black12,
offset: Offset(2, 4),
blurRadius: 6,
),
],
),
),
),
const SizedBox(height: 20),
Text(
'Active configuration: ${_transition.name.toUpperCase()} / ${_flipDir.name.toUpperCase()}',
style: TextStyle(
fontSize: 11,
color: isDark ? const Color(0xFF64748B) : const Color(0xFF475569),
),
),
],
),
);
}
Widget _buildPerformanceTelemetryCard() {
final width = MediaQuery.sizeOf(context).width;
final isMobile = width < 680;
final telemetryWidgets = [
_buildTelemetryTile(
title: 'UPDATE FREQUENCY',
value: '${_liveHz.toStringAsFixed(1)} Hz',
subtitle: 'Live value updates per second',
color: _liveHz > 0 ? const Color(0xFF10B981) : const Color(0xFF64748B),
glow: _liveHz > 0,
),
_buildTelemetryTile(
title: 'FRAME RENDER TIME',
value: '${_liveRenderTimeUs.toStringAsFixed(1)} µs',
subtitle: 'Avg build time (~${(1000000 / _liveRenderTimeUs).toStringAsFixed(0)} FPS)',
color: const Color(0xFF06B6D4),
glow: _benchmarkRunning,
),
_buildTelemetryTile(
title: 'ESTIMATED RAM FOOTPRINT',
value: '${_estimateMemoryFootprint().toStringAsFixed(1)} KB',
subtitle: 'Subtree memory footprint',
color: const Color(0xFF8B5CF6),
glow: true,
),
];
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'PERFORMANCE & TELEMETRY MONITOR',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
letterSpacing: 1,
),
),
if (_benchmarkRunning)
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
const SizedBox(height: 16),
if (isMobile)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
telemetryWidgets[0],
const SizedBox(height: 12),
telemetryWidgets[1],
const SizedBox(height: 12),
telemetryWidgets[2],
],
)
else
Row(
children: [
Expanded(child: telemetryWidgets[0]),
const SizedBox(width: 16),
Expanded(child: telemetryWidgets[1]),
const SizedBox(width: 16),
Expanded(child: telemetryWidgets[2]),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: Text(
'Subtree utilizes RepaintBoundary to isolate canvas repaints and caches TextPainters to bypass font layouts.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
),
const SizedBox(width: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
onPressed: _benchmarkRunning ? null : _runStressTest,
icon: const Icon(Icons.speed_rounded, size: 16),
label: const Text('Run Live FPS Test', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
),
],
),
],
),
),
);
}
Widget _buildTelemetryTile({
required String title,
required String value,
required String subtitle,
required Color color,
required bool glow,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: glow ? color.withValues(alpha: 0.3) : (Theme.of(context).dividerTheme.color ?? const Color(0xFF1E293B)),
width: 1.5,
),
boxShadow: glow
? [
BoxShadow(
color: color.withValues(alpha: 0.15),
blurRadius: 8,
spreadRadius: 1,
)
]
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: color, letterSpacing: 0.5),
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(fontSize: 10, color: Color(0xFF64748B)),
),
],
),
);
}
Widget _buildValueControls() {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'VALUATION TRIGGER',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
const Text('Manual Increments', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildDeltaBtn(-100, isPositive: false),
_buildDeltaBtn(-10, isPositive: false),
_buildDeltaBtn(-0.5, isPositive: false),
_buildDeltaBtn(0.5, isPositive: true),
_buildDeltaBtn(10, isPositive: true),
_buildDeltaBtn(100, isPositive: true),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Divider(),
),
const Text('Set Custom Value', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: SizedBox(
height: 48,
child: TextField(
controller: _inputController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: 'e.g. 1045.50',
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
),
),
const SizedBox(width: 12),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
),
onPressed: () {
final val = double.tryParse(_inputController.text);
if (val != null) {
_updateValue(val);
FocusScope.of(context).unfocus();
}
},
child: const Text('Apply'),
),
],
),
],
),
),
);
}
Widget _buildDeltaBtn(double delta, {required bool isPositive}) {
final text = isPositive ? '+$delta' : '$delta';
final color = isPositive ? const Color(0xFF10B981) : const Color(0xFFEF4444);
return InkWell(
onTap: () => _updateValue(_value + delta),
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
border: Border.all(color: color.withValues(alpha: 0.25)),
borderRadius: BorderRadius.circular(10),
),
child: Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.w900,
fontSize: 12,
),
),
),
);
}
Widget _buildAnimationConfigs() {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ANIMATION PARAMETERS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
const Text('Transition Effect', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: CounterTransitionType.values.map((t) {
final selected = _transition == t;
return ChoiceChip(
label: Text(t.name.toUpperCase(), style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
selected: selected,
selectedColor: Theme.of(context).colorScheme.primary,
checkmarkColor: Colors.white,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
labelStyle: TextStyle(
color: selected ? Colors.white : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
onSelected: (_) => setState(() => _transition = t),
);
}).toList(),
),
const SizedBox(height: 16),
const Text('Flip Scroll Direction', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [AxisDirection.up, AxisDirection.down, AxisDirection.left, AxisDirection.right].map((dir) {
final selected = _flipDir == dir;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(dir.name.toUpperCase(), style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
selected: selected,
selectedColor: Theme.of(context).colorScheme.secondary,
checkmarkColor: Colors.white,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
labelStyle: TextStyle(
color: selected ? Colors.white : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
onSelected: (_) => setState(() => _flipDir = dir),
),
);
}).toList(),
),
const Divider(height: 32),
_buildSliderItem(
title: 'Speed Multiplier',
description: 'Scale animation play durations.',
value: _speed,
min: 0.2,
max: 3.0,
display: '${_speed.toStringAsFixed(1)}x',
onChanged: (val) => setState(() => _speed = val),
),
const SizedBox(height: 12),
_buildSliderItem(
title: 'Start Delay',
description: 'Initial wait interval before scroll.',
value: _delayMs,
min: 0.0,
max: 1500.0,
display: '${_delayMs.round()}ms',
onChanged: (val) => setState(() => _delayMs = val),
),
const SizedBox(height: 12),
_buildSliderItem(
title: 'Digit Stagger Delay',
description: 'Delay between contiguous columns.',
value: _staggerMs,
min: 0.0,
max: 300.0,
display: '${_staggerMs.round()}ms',
onChanged: (val) => setState(() => _staggerMs = val),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Stagger direction', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
Text(
'Sequence of staggered digits.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
ChoiceChip(
label: const Text('L ➔ R', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
selected: _staggerDir == StaggerDirection.leftToRight,
selectedColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
checkmarkColor: Colors.white,
labelStyle: TextStyle(
color: _staggerDir == StaggerDirection.leftToRight ? Colors.white : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
onSelected: (selected) => setState(() => _staggerDir = StaggerDirection.leftToRight),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('R ➔ L', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
selected: _staggerDir == StaggerDirection.rightToLeft,
selectedColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
checkmarkColor: Colors.white,
labelStyle: TextStyle(
color: _staggerDir == StaggerDirection.rightToLeft ? Colors.white : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
onSelected: (selected) => setState(() => _staggerDir = StaggerDirection.rightToLeft),
),
],
),
],
),
),
);
}
Widget _buildSliderItem({
required String title,
required String description,
required double value,
required double min,
required double max,
required String display,
required ValueChanged<double> onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
Text(
description,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
Text(
display,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
Slider(
value: value,
min: min,
max: max,
onChanged: onChanged,
),
],
);
}
}
/// ==========================================
/// TAB 2: FORMATS & LOCALIZATIONS
/// ==========================================
class FormatsAndLocalesTab extends StatefulWidget {
const FormatsAndLocalesTab({super.key});
@override
State<FormatsAndLocalesTab> createState() => _FormatsAndLocalesTabState();
}
class _FormatsAndLocalesTabState extends State<FormatsAndLocalesTab> {
double _value = 1452589.67;
bool _customAbbr = false;
bool _hideLeading = false;
int _fractionDigits = 2;
int _compactDecimals = 1;
List<int> _grouping = [3];
NumeralSystem _numeralSystem = NumeralSystem.latin;
bool _useCustomMapper = false;
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
int crossAxisCount = 1;
if (width >= 1100) {
crossAxisCount = 3;
} else if (width >= 700) {
crossAxisCount = 2;
}
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Live display card
_buildLiveDisplayCard(),
const SizedBox(height: 24),
// Control configurations
_buildConfigurationSection(),
const SizedBox(height: 24),
// Grid display
const Text(
'REGIONAL PRESET GRID',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF94A3B8), letterSpacing: 1),
),
const SizedBox(height: 12),
GridView.count(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildGridItem('British Pound (GBP Preset)', _buildGBPWidget()),
_buildGridItem('Japanese Yen (JPY Preset)', _buildJPYWidget()),
_buildGridItem('Percentage Formatter', _buildPercentWidget()),
_buildGridItem('Compact Presets', _buildCompactWidget()),
_buildGridItem('Regional Numeral System', _buildNumeralSystemWidget()),
_buildGridItem('Custom Symbol Mapping', _buildMapperWidget()),
],
),
],
),
);
}
Widget _buildLiveDisplayCard() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerTheme.color ?? const Color(0xFF334155), width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'FORMATTER MONITOR',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
Text(
'Real-time raw value: ${_value.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Color(0xFFEF4444)),
onPressed: () => setState(() => _value = (_value - 250000.5).clamp(0, double.infinity)),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.add_circle_outline, color: Color(0xFF10B981)),
onPressed: () => setState(() => _value += 250000.5),
),
],
),
],
),
);
}
Widget _buildConfigurationSection() {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DYNAMIC FORMAT CONFIGURATIONS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildToggleControl(
'Hide leading zeros',
'Removes padding display.',
_hideLeading,
(val) => setState(() => _hideLeading = val),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildToggleControl(
'Custom Abbreviations',
'replaces M, B, T with custom words.',
_customAbbr,
(val) => setState(() => _customAbbr = val),
),
),
],
),
const Divider(height: 24),
Row(
children: [
Expanded(
child: _buildNumberInputControl(
'Fraction Digits',
'Adjust decimal precision.',
_fractionDigits,
0,
4,
(val) => setState(() => _fractionDigits = val),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildNumberInputControl(
'Compact Decimals',
'Fraction values for compact notation.',
_compactDecimals,
0,
3,
(val) => setState(() => _compactDecimals = val),
),
),
],
),
const Divider(height: 24),
const Text('Digit Spacing Patterns', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [
_buildGroupingChip('Western [3]', [3]),
const SizedBox(width: 8),
_buildGroupingChip('Indian Lakh [3, 2]', [3, 2]),
const SizedBox(width: 8),
_buildGroupingChip('East Asian [4]', [4]),
],
),
],
),
),
);
}
Widget _buildToggleControl(String title, String desc, bool value, ValueChanged<bool> onChanged) {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
Text(
desc,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
Switch(value: value, onChanged: onChanged),
],
);
}
Widget _buildNumberInputControl(String title, String desc, int value, int min, int max, ValueChanged<int> onChanged) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
Text(
desc,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove, size: 16),
onPressed: value > min ? () => onChanged(value - 1) : null,
),
Text('$value', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
IconButton(
icon: const Icon(Icons.add, size: 16),
onPressed: value < max ? () => onChanged(value + 1) : null,
),
],
),
],
);
}
Widget _buildGroupingChip(String label, List<int> pattern) {
final selected = _listsEqual(_grouping, pattern);
return ChoiceChip(
label: Text(label, style: const TextStyle(fontSize: 11)),
selected: selected,
selectedColor: Theme.of(context).colorScheme.primary,
checkmarkColor: Colors.white,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
labelStyle: TextStyle(
color: selected ? Colors.white : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
onSelected: (sel) {
if (sel) setState(() => _grouping = pattern);
},
);
}
bool _listsEqual(List<int> a, List<int> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
Widget _buildGridItem(String title, Widget counter) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerTheme.color ?? const Color(0xFF334155), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary)),
Center(child: counter),
const SizedBox(height: 2),
],
),
);
}
Widget _buildGBPWidget() {
return AnimatedFlipCounter.gbp(
value: _value,
hideLeadingZeroes: _hideLeading,
textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Color(0xFF10B981)),
);
}
Widget _buildJPYWidget() {
return AnimatedFlipCounter.jpy(
value: _value,
hideLeadingZeroes: _hideLeading,
textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Color(0xFF06B6D4)),
);
}
Widget _buildPercentWidget() {
return AnimatedFlipCounter.percentage(
value: (_value / 100000) % 100,
fractionDigits: _fractionDigits,
hideLeadingZeroes: _hideLeading,
textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Color(0xFFF59E0B)),
);
}
Widget _buildCompactWidget() {
final customMap = {
1e3: ' kilo',
1e6: ' millions',
1e9: ' billions',
};
return AnimatedFlipCounter.compact(
value: _value,
compactFractionDigits: _compactDecimals,
compactAbbreviations: _customAbbr ? customMap : null,
hideLeadingZeroes: _hideLeading,
textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Color(0xFFEC4899)),
);
}
Widget _buildNumeralSystemWidget() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedFlipCounter(
value: _value,
fractionDigits: _fractionDigits,
groupingPattern: _grouping,
thousandSeparator: ',',
numeralSystem: _numeralSystem,
hideLeadingZeroes: _hideLeading,
textStyle: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
DropdownButton<NumeralSystem>(
value: _numeralSystem,
isDense: true,
dropdownColor: Theme.of(context).colorScheme.surface,
underline: const SizedBox.shrink(),
onChanged: (sys) {
if (sys != null) setState(() => _numeralSystem = sys);
},
items: NumeralSystem.values.map((sys) {
return DropdownMenuItem(
value: sys,
child: Text(sys.name.toUpperCase(), style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
);
}).toList(),
),
],
);
}
Widget _buildMapperWidget() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedFlipCounter(
value: _value % 100000,
fractionDigits: 0,
numeralMapper: _useCustomMapper ? _emojiMapper : null,
hideLeadingZeroes: _hideLeading,
textStyle: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber),
),
const SizedBox(height: 4),
ChoiceChip(
label: const Text('Emoji Mapper', style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold)),
selected: _useCustomMapper,
selectedColor: Colors.amber,
checkmarkColor: Colors.black,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
labelStyle: TextStyle(
color: _useCustomMapper ? Colors.black : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
onSelected: (sel) => setState(() => _useCustomMapper = sel),
),
],
);
}
String _emojiMapper(int val) {
const emojis = ['⓪', '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨'];
return emojis[val];
}
}
/// ==========================================
/// TAB 3: CUSTOM STYLINGS
/// ==========================================
class CustomStylingsTab extends StatelessWidget {
const CustomStylingsTab({super.key});
@override
Widget build(BuildContext context) {
return const _CustomStylingsTabContent();
}
}
class _CustomStylingsTabContent extends StatefulWidget {
const _CustomStylingsTabContent();
@override
State<_CustomStylingsTabContent> createState() => _CustomStylingsTabContentState();
}
class _CustomStylingsTabContentState extends State<_CustomStylingsTabContent> {
double _value = 45.0;
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (mounted) {
setState(() {
_value = _value == 45.0 ? 82.0 : 45.0;
});
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
int crossAxisCount = 1;
if (width >= 1100) {
crossAxisCount = 3;
} else if (width >= 700) {
crossAxisCount = 2;
}
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'CUSTOM WRAPPERS & TRANSITION BUILDERS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.4,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildFlipClockCard(),
_buildNeonGlowCard(),
_buildNixieTubeCard(),
_buildWoodTileCard(),
_buildPillBadgeCard(),
_buildCustomTransitionCard(),
],
),
],
),
);
}
Widget _buildStyleCard({
required String title,
required String description,
required Widget counter,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerTheme.color ?? const Color(0xFF334155), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8),
Center(child: counter),
const SizedBox(height: 4),
],
),
);
}
Widget _buildFlipClockCard() {
return _buildStyleCard(
title: 'Retro Flip Clock',
description: 'Digits inside dark cards with split line.',
counter: AnimatedFlipCounter(
value: _value,
wholeDigits: 3,
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutBack,
digitWrapperBuilder: (context, index, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF475569), width: 1),
),
child: Stack(
alignment: Alignment.center,
children: [
child,
Positioned(
left: 0,
right: 0,
child: Container(
height: 1,
color: const Color(0xFF334155),
),
),
],
),
);
},
textStyle: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Color(0xFFFFB000),
),
),
);
}
Widget _buildNeonGlowCard() {
return _buildStyleCard(
title: 'Cyberpunk Neon',
description: 'Deep visual glowing shadow drop effect.',
counter: AnimatedFlipCounter(
value: _value,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
textStyle: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w900,
color: Color(0xFF06B6D4),
shadows: [
Shadow(color: Color(0xFF06B6D4), blurRadius: 10),
Shadow(color: Color(0xFFEC4899), blurRadius: 20),
],
),
),
);
}
Widget _buildNixieTubeCard() {
return _buildStyleCard(
title: 'Glass Nixie Tube',
description: 'Filament indicators with warm orange hue.',
counter: AnimatedFlipCounter(
value: _value,
wholeDigits: 2,
textStyle: const TextStyle(
fontSize: 26,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
color: Color(0xFFFF5500),
shadows: [
Shadow(color: Color(0xFFFF5500), blurRadius: 8),
Shadow(color: Color(0xFFFF0000), blurRadius: 16),
],
),
digitWrapperBuilder: (context, index, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1E0A03),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFFF5500).withValues(alpha: 0.5), width: 1.2),
),
child: child,
);
},
),
);
}
Widget _buildWoodTileCard() {
return _buildStyleCard(
title: 'Wood Game Tile',
description: 'Scrabble-like tiles with point score subscript.',
counter: AnimatedFlipCounter(
value: _value,
wholeDigits: 2,
textStyle: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF5D4037),
),
digitWrapperBuilder: (context, index, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFFFECB3),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFFD7CCC8), width: 1.2),
boxShadow: const [
BoxShadow(
color: Colors.black12,
offset: Offset(1, 1),
blurRadius: 1.5,
),
],
),
child: Stack(
clipBehavior: Clip.none,
children: [
child,
Positioned(
right: -4,
bottom: -4,
child: Text(
'${(index + 1) * 2}',
style: const TextStyle(fontSize: 7, fontWeight: FontWeight.bold, color: Color(0xFF8D6E63)),
),
),
],
),
);
},
),
);
}
Widget _buildPillBadgeCard() {
final color = Theme.of(context).colorScheme.primary;
return _buildStyleCard(
title: 'Pill Capsule Badge',
description: 'Curved gradient borders and translucent backdrop.',
counter: AnimatedFlipCounter(
value: _value,
wholeDigits: 2,
textStyle: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: color,
),
digitWrapperBuilder: (context, index, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withValues(alpha: 0.4), width: 1.2),
),
child: child,
);
},
),
);
}
Widget _buildCustomTransitionCard() {
return _buildStyleCard(
title: 'Morph Transition',
description: 'Opacity/Translation sliding layouts.',
counter: AnimatedFlipCounter(
value: _value,
duration: const Duration(milliseconds: 700),
digitTransitionBuilder: (context, currentDigit, nextDigit, progress, size) {
return Stack(
alignment: Alignment.center,
children: [
Transform.translate(
offset: Offset(0, -progress * size.height),
child: Opacity(
opacity: (1 - progress).clamp(0.0, 1.0),
child: currentDigit,
),
),
Transform.translate(
offset: Offset(0, (1 - progress) * size.height),
child: Opacity(
opacity: progress.clamp(0.0, 1.0),
child: nextDigit,
),
),
],
);
},
textStyle: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w900,
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
}
/// ==========================================
/// TAB 4: CONTROLLER & LIFECYCLE
/// ==========================================
class ControllerTab extends StatefulWidget {
const ControllerTab({super.key});
@override
State<ControllerTab> createState() => _ControllerTabState();
}
class _ControllerTabState extends State<ControllerTab> {
late CounterController _controller;
bool _haptics = true;
String _status = 'Dismissed';
final List<String> _logs = [];
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_controller = CounterController(initialValue: 10.0);
_controller.addStatusListener(_onStatusChange);
}
void _onStatusChange(AnimationStatus status) {
if (mounted) {
setState(() {
_status = status.name.toUpperCase();
_addLog('Status changed to: $_status');
});
}
}
void _addLog(String text) {
final now = DateTime.now();
final timestamp = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.${(now.millisecond).toString().padLeft(3, '0')}';
_logs.add('[$timestamp] $text');
if (_logs.length > 50) {
_logs.removeAt(0);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
@override
void dispose() {
_controller.removeStatusListener(_onStatusChange);
_controller.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isDesktop = width >= 800;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildMonitorCard(),
const SizedBox(height: 24),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isDesktop)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildControlsCard()),
const SizedBox(width: 24),
Expanded(child: _buildTerminalCard()),
],
)
else ...[
_buildControlsCard(),
const SizedBox(height: 24),
_buildTerminalCard(),
],
],
),
),
),
],
),
);
}
Widget _buildMonitorCard() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Theme.of(context).dividerTheme.color ?? const Color(0xFF334155)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: Theme.of(context).brightness == Brightness.dark ? 0.3 : 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'CONTROLLER VALUE DISPLAY',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _status == 'FORWARD' || _status == 'REVERSE'
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.2)
: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_status,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: _status == 'FORWARD' || _status == 'REVERSE'
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
),
],
),
const SizedBox(height: 32),
AnimatedFlipCounter(
controller: _controller,
fractionDigits: 1,
triggerHaptics: _haptics,
duration: const Duration(milliseconds: 1000),
onAnimationStart: () => _addLog('Animation event: onAnimationStart'),
onAnimationEnd: () => _addLog('Animation event: onAnimationEnd'),
onPause: () => _addLog('Animation event: onPause'),
onResume: () => _addLog('Animation event: onResume'),
onRepeat: () => _addLog('Animation event: onRepeat'),
onReverse: () => _addLog('Animation event: onReverse'),
onReset: () => _addLog('Animation event: onReset'),
textStyle: TextStyle(
fontSize: 60,
fontWeight: FontWeight.w900,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Text(
'Target logic: ${_controller.value.toStringAsFixed(1)}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
);
}
Widget _buildControlsCard() {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'LIFECYCLE TRIGGERS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
const Text('Value Jumps / Animations', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
onPressed: () {
final target = math.Random().nextDouble() * 100;
_addLog('Trigger: animateTo(${target.toStringAsFixed(1)})');
_controller.animateTo(target);
},
child: const Text('Animate To Random', style: TextStyle(fontSize: 12)),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: () {
final target = math.Random().nextDouble() * 100;
_addLog('Trigger: jumpTo(${target.toStringAsFixed(1)})');
_controller.jumpTo(target);
},
child: const Text('Jump To Random', style: TextStyle(fontSize: 12)),
),
),
],
),
const Divider(height: 24),
const Text('Animation Playback Controls', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildPlaybackIconBtn(Icons.pause_rounded, 'Pause', () {
_addLog('Action: pause()');
_controller.pause();
}),
_buildPlaybackIconBtn(Icons.play_arrow_rounded, 'Resume', () {
_addLog('Action: resume()');
_controller.resume();
}),
_buildPlaybackIconBtn(Icons.compare_arrows_rounded, 'Reverse', () {
_addLog('Action: reverse()');
_controller.reverse();
}),
_buildPlaybackIconBtn(Icons.loop_rounded, 'Repeat', () {
_addLog('Action: repeat(reverse: true)');
_controller.repeat(reverse: true);
}),
_buildPlaybackIconBtn(Icons.replay_rounded, 'Reset', () {
_addLog('Action: jumpTo(10.0)');
_controller.jumpTo(10.0);
}),
],
),
const Divider(height: 24),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Digit selection haptics', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
Text(
'Triggers haptic ticks during rolls.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
Switch(
value: _haptics,
onChanged: (val) => setState(() => _haptics = val),
),
],
),
],
),
),
);
}
Widget _buildPlaybackIconBtn(IconData icon, String tooltip, VoidCallback onPressed) {
return Tooltip(
message: tooltip,
child: IconButton(
icon: Icon(icon),
onPressed: onPressed,
style: IconButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.all(12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
);
}
Widget _buildTerminalCard() {
return Container(
height: 310,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerTheme.color ?? const Color(0xFF1E293B)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'LIFECYCLE EVENTS CONSOLE',
style: TextStyle(fontFamily: 'Courier', fontSize: 11, fontWeight: FontWeight.bold, color: Colors.greenAccent),
),
GestureDetector(
onTap: () => setState(() => _logs.clear()),
child: const Text(
'CLEAR',
style: TextStyle(fontFamily: 'Courier', fontSize: 10, color: Colors.grey, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 12),
Expanded(
child: _logs.isEmpty
? const Center(
child: Text(
'No events logged yet.\nInteract with the controls above.',
textAlign: TextAlign.center,
style: TextStyle(fontFamily: 'Courier', fontSize: 11, color: Colors.grey),
),
)
: ListView.builder(
controller: _scrollController,
itemCount: _logs.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
_logs[index],
style: const TextStyle(
fontFamily: 'Courier',
fontSize: 11,
color: Colors.greenAccent,
),
),
);
},
),
),
],
),
);
}
}
/// ==========================================
/// TAB 5: STOCK MARKET TICKER SIMULATOR
/// ==========================================
class StockMarketTab extends StatefulWidget {
const StockMarketTab({super.key});
@override
State<StockMarketTab> createState() => _StockMarketTabState();
}
class _StockMarketTabState extends State<StockMarketTab> {
final List<Map<String, dynamic>> _stocks = [
{
'symbol': 'AAPL',
'name': 'Apple Inc.',
'price': 182.41,
'change': 1.25,
'changePercent': 0.69,
'volume': '52.4M',
'high': 183.50,
'low': 180.80,
},
{
'symbol': 'GOOGL',
'name': 'Alphabet Inc.',
'price': 142.10,
'change': -0.85,
'changePercent': -0.60,
'volume': '28.1M',
'high': 143.90,
'low': 141.20,
},
{
'symbol': 'TSLA',
'name': 'Tesla Inc.',
'price': 175.50,
'change': 4.15,
'changePercent': 2.42,
'volume': '91.8M',
'high': 178.00,
'low': 170.20,
},
{
'symbol': 'NVDA',
'name': 'NVIDIA Corp.',
'price': 875.20,
'change': 15.30,
'changePercent': 1.78,
'volume': '44.2M',
'high': 885.00,
'low': 852.10,
},
{
'symbol': 'MSFT',
'name': 'Microsoft Corp.',
'price': 420.35,
'change': -2.15,
'changePercent': -0.51,
'volume': '22.9M',
'high': 425.00,
'low': 418.50,
},
];
int _selectedStockIndex = 0;
String _marketTrend = 'volatile'; // bullish, bearish, volatile
double _tickSpeedSeconds = 1.0;
bool _isLive = true;
Timer? _tickerTimer;
// Trading variables
String _tradingType = 'BUY'; // BUY, SELL
int _sharesCount = 10;
final List<String> _tradeHistory = [];
final ScrollController _tradeScrollController = ScrollController();
@override
void initState() {
super.initState();
_startTicker();
}
@override
void dispose() {
_tickerTimer?.cancel();
_tradeScrollController.dispose();
super.dispose();
}
void _startTicker() {
_tickerTimer?.cancel();
if (!_isLive) return;
_tickerTimer = Timer.periodic(
Duration(milliseconds: (_tickSpeedSeconds * 1000).round()),
(timer) {
if (!mounted) return;
_simulateTicks();
},
);
}
void _simulateTicks() {
setState(() {
final random = math.Random();
for (var i = 0; i < _stocks.length; i++) {
final stock = _stocks[i];
final currentPrice = stock['price'] as double;
double changeDirection;
if (_marketTrend == 'bullish') {
// 75% chance up
changeDirection = random.nextDouble() < 0.75 ? 1.0 : -1.0;
} else if (_marketTrend == 'bearish') {
// 75% chance down
changeDirection = random.nextDouble() < 0.75 ? -1.0 : 1.0;
} else {
// 50/50
changeDirection = random.nextBool() ? 1.0 : -1.0;
}
final volatility = currentPrice > 500 ? 4.5 : 1.2;
final delta = changeDirection * (random.nextDouble() * volatility);
final newPrice = math.max(1.0, double.parse((currentPrice + delta).toStringAsFixed(2)));
final difference = newPrice - currentPrice;
stock['price'] = newPrice;
stock['change'] = difference;
stock['changePercent'] = (difference / currentPrice) * 100;
if (newPrice > stock['high']) {
stock['high'] = newPrice;
}
if (newPrice < stock['low']) {
stock['low'] = newPrice;
}
}
});
}
void _executeTrade() {
final stock = _stocks[_selectedStockIndex];
final price = stock['price'] as double;
final symbol = stock['symbol'] as String;
final totalCost = price * _sharesCount;
final timeStr = DateTime.now().toIso8601String().substring(11, 19);
setState(() {
_tradeHistory.insert(
0,
'[$timeStr] $_tradingType $_sharesCount sh of $symbol @ \$${price.toStringAsFixed(2)} | Vol: \$${totalCost.toStringAsFixed(2)}',
);
});
// Auto-scroll trade console to top
if (_tradeScrollController.hasClients) {
_tradeScrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final width = MediaQuery.sizeOf(context).width;
final isDesktop = width >= 950;
final activeStock = _stocks[_selectedStockIndex];
final priceChange = activeStock['change'] as double;
final percentChange = activeStock['changePercent'] as double;
final isPositive = priceChange >= 0;
final containerBg = isDark ? const Color(0xFF151D30) : Colors.white;
final borderCol = isDark ? const Color(0xFF1E293B) : const Color(0xFFE2E8F0);
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'LIVE TRADING BOARD',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w900,
color: Theme.of(context).colorScheme.primary,
letterSpacing: 2.0,
),
),
const SizedBox(height: 4),
Text(
'High Frequency Stock Simulation',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
Row(
children: [
Text(
'Simulation Live',
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)),
),
const SizedBox(width: 8),
Switch.adaptive(
value: _isLive,
activeTrackColor: Theme.of(context).colorScheme.primary,
onChanged: (val) {
setState(() {
_isLive = val;
_startTicker();
});
},
),
],
),
],
),
const SizedBox(height: 24),
// Main Layout
if (isDesktop)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Stock List & Simulator Config
Expanded(
flex: 2,
child: Column(
children: [
_buildStockSelectorList(),
const SizedBox(height: 20),
_buildSimulatorConfig(),
],
),
),
const SizedBox(width: 24),
// Right Column: Large Ticker & Trading Box
Expanded(
flex: 3,
child: Column(
children: [
_buildBigTickerPanel(activeStock, isPositive, priceChange, percentChange, isDark),
const SizedBox(height: 20),
_buildOrderTradingPanel(activeStock, isDark, containerBg, borderCol),
],
),
),
],
)
else
Column(
children: [
_buildStockSelectorList(),
const SizedBox(height: 20),
_buildBigTickerPanel(activeStock, isPositive, priceChange, percentChange, isDark),
const SizedBox(height: 20),
_buildOrderTradingPanel(activeStock, isDark, containerBg, borderCol),
const SizedBox(height: 20),
_buildSimulatorConfig(),
],
),
],
),
);
}
Widget _buildStockSelectorList() {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'WATCHLIST',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _stocks.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final stock = _stocks[index];
final isSelected = _selectedStockIndex == index;
final isStockPositive = (stock['change'] as double) >= 0;
final activeBorderColor = isStockPositive ? Colors.green.withValues(alpha: 0.5) : Colors.red.withValues(alpha: 0.5);
return InkWell(
onTap: () => setState(() => _selectedStockIndex = index),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected
? (isStockPositive
? Colors.green.withValues(alpha: 0.1)
: Colors.red.withValues(alpha: 0.1))
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? activeBorderColor
: (Theme.of(context).dividerTheme.color ?? const Color(0xFF1E293B)),
width: 1.5,
),
),
child: Row(
children: [
// Ticker Symbol
Container(
width: 65,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
stock['symbol'],
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 13),
),
),
),
const SizedBox(width: 12),
// Company Name
Expanded(
child: Text(
stock['name'],
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
// Mini Animated Counter
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
AnimatedFlipCounter.usd(
value: stock['price'],
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w900,
),
increasingColor: Colors.greenAccent,
decreasingColor: Colors.redAccent,
),
const SizedBox(height: 2),
Row(
children: [
Icon(
isStockPositive ? Icons.arrow_drop_up_rounded : Icons.arrow_drop_down_rounded,
color: isStockPositive ? Colors.green : Colors.red,
size: 16,
),
Text(
'${isStockPositive ? "+" : ""}${((stock['changePercent'] as double)).toStringAsFixed(2)}%',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: isStockPositive ? Colors.green : Colors.red,
),
),
],
),
],
),
],
),
),
);
},
),
],
),
),
);
}
Widget _buildSimulatorConfig() {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'SIMULATION PARAMETERS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
// Market Trend
const Text('Market Trend Bias', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [
_buildTrendButton('Bullish (↗)', 'bullish', Colors.green),
const SizedBox(width: 8),
_buildTrendButton('Bearish (↘)', 'bearish', Colors.red),
const SizedBox(width: 8),
_buildTrendButton('Volatile (⇄)', 'volatile', Colors.amber),
],
),
const SizedBox(height: 20),
// Ticking Speed Slider
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Tick Frequency', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
Text(
'Every ${_tickSpeedSeconds.toStringAsFixed(1)}s',
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold),
),
],
),
Slider(
min: 0.2,
max: 3.0,
divisions: 28,
value: _tickSpeedSeconds,
onChanged: (val) {
setState(() {
_tickSpeedSeconds = val;
_startTicker();
});
},
),
],
),
),
);
}
Widget _buildTrendButton(String label, String value, Color activeColor) {
final isSelected = _marketTrend == value;
return Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: isSelected ? 2 : 0,
backgroundColor: isSelected ? activeColor : Theme.of(context).scaffoldBackgroundColor,
foregroundColor: isSelected ? Colors.white : Theme.of(context).colorScheme.onSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isSelected ? Colors.transparent : (Theme.of(context).dividerTheme.color ?? const Color(0xFF1E293B)),
),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () {
setState(() {
_marketTrend = value;
});
},
child: Text(label, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
),
);
}
Widget _buildBigTickerPanel(
Map<String, dynamic> stock,
bool isPositive,
double priceChange,
double percentChange,
bool isDark,
) {
final gradientColors = isDark
? [const Color(0xFF161C2C), const Color(0xFF0F1424)]
: [const Color(0xFFF1F5F9), const Color(0xFFE2E8F0)];
final borderCol = isDark ? const Color(0xFF1E293B) : const Color(0xFFE2E8F0);
return Container(
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isPositive ? Colors.green.withValues(alpha: 0.3) : Colors.red.withValues(alpha: 0.3),
width: 2.0,
),
boxShadow: [
BoxShadow(
color: (isPositive ? Colors.green : Colors.red).withValues(alpha: 0.08),
blurRadius: 20,
spreadRadius: 1,
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Ticker & Name
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stock['name'].toUpperCase(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
stock['symbol'],
style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w900, letterSpacing: -0.5),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'NASDAQ',
style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 0.5),
),
),
],
),
],
),
Icon(
isPositive ? Icons.trending_up_rounded : Icons.trending_down_rounded,
color: isPositive ? Colors.greenAccent : Colors.redAccent,
size: 36,
),
],
),
const SizedBox(height: 28),
// LARGE FLIP COUNTER
Center(
child: AnimatedFlipCounter.usd(
value: stock['price'],
textStyle: TextStyle(
fontSize: 56,
fontWeight: FontWeight.w900,
color: Theme.of(context).colorScheme.onSurface,
letterSpacing: -1.0,
),
increasingColor: Colors.greenAccent,
decreasingColor: Colors.redAccent,
),
),
const SizedBox(height: 12),
// Price change badges
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isPositive ? Colors.green.withValues(alpha: 0.15) : Colors.red.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isPositive ? Colors.green.withValues(alpha: 0.3) : Colors.red.withValues(alpha: 0.3),
width: 1.0,
),
),
child: Row(
children: [
Icon(
isPositive ? Icons.arrow_upward_rounded : Icons.arrow_downward_rounded,
color: isPositive ? Colors.green : Colors.red,
size: 14,
),
const SizedBox(width: 4),
Text(
'${isPositive ? "+" : ""}${priceChange.toStringAsFixed(2)} (${isPositive ? "+" : ""}${percentChange.toStringAsFixed(2)}%)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isPositive ? Colors.green : Colors.red,
),
),
],
),
),
],
),
const SizedBox(height: 24),
// Ticker stats row
Divider(color: borderCol),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStockStat('Daily Vol', stock['volume']),
_buildStockStat('24h High', '\$${(stock['high'] as double).toStringAsFixed(2)}'),
_buildStockStat('24h Low', '\$${(stock['low'] as double).toStringAsFixed(2)}'),
],
),
],
),
);
}
Widget _buildStockStat(String title, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4)),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildOrderTradingPanel(
Map<String, dynamic> stock,
bool isDark,
Color bg,
Color border,
) {
final symbol = stock['symbol'] as String;
final price = stock['price'] as double;
final totalCost = price * _sharesCount;
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'ORDER EXECUTION TERMINAL',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1,
),
),
const SizedBox(height: 16),
// Trade Type selector
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _tradingType = 'BUY'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _tradingType == 'BUY' ? Colors.green : bg,
borderRadius: const BorderRadius.horizontal(left: Radius.circular(10)),
border: Border.all(color: _tradingType == 'BUY' ? Colors.green : border),
),
child: Center(
child: Text(
'BUY (LONG)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _tradingType == 'BUY' ? Colors.white : Theme.of(context).colorScheme.onSurface,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _tradingType = 'SELL'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _tradingType == 'SELL' ? Colors.red : bg,
borderRadius: const BorderRadius.horizontal(right: Radius.circular(10)),
border: Border.all(color: _tradingType == 'SELL' ? Colors.red : border),
),
child: Center(
child: Text(
'SELL (SHORT)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _tradingType == 'SELL' ? Colors.white : Theme.of(context).colorScheme.onSurface,
),
),
),
),
),
),
],
),
const SizedBox(height: 20),
// Stepper and Total Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Quantity (Shares)', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Row(
children: [
IconButton.filledTonal(
icon: const Icon(Icons.remove, size: 14),
onPressed: _sharesCount > 1
? () => setState(() => _sharesCount--)
: null,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'$_sharesCount',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
IconButton.filledTonal(
icon: const Icon(Icons.add, size: 14),
onPressed: () => setState(() => _sharesCount++),
),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text('Estimated Cost', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
// Flip Counter for Total Cost
AnimatedFlipCounter.usd(
value: totalCost,
textStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
],
),
const SizedBox(height: 24),
// Submit Button
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _tradingType == 'BUY' ? Colors.green : Colors.red,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 16),
),
onPressed: _executeTrade,
child: Text(
'EXECUTE $_tradingType ORDER FOR $symbol',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 0.5),
),
),
const SizedBox(height: 20),
// Trade Console Log
const Divider(),
const SizedBox(height: 10),
const Text(
'ORDER CONFIRMATION JOURNAL',
style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 0.5, color: Colors.grey),
),
const SizedBox(height: 10),
Container(
height: 110,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(10),
),
child: _tradeHistory.isEmpty
? const Center(
child: Text(
'No order transactions executed.',
style: TextStyle(fontFamily: 'Courier', fontSize: 10, color: Colors.grey),
),
)
: ListView.builder(
controller: _tradeScrollController,
itemCount: _tradeHistory.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
_tradeHistory[index],
style: const TextStyle(
fontFamily: 'Courier',
fontSize: 10,
color: Colors.greenAccent,
),
),
);
},
),
),
],
),
),
);
}
}