|
|
|
|
|
import 'dart:async';
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
class LoadingIndicator extends StatefulWidget {
|
|
|
|
|
|
|
|
|
final bool isCompleted;
|
|
|
|
|
|
|
|
|
final Duration spinnerDuration;
|
|
|
|
|
|
|
|
|
final Duration tickDuration;
|
|
|
|
|
|
const LoadingIndicator({
|
|
|
super.key,
|
|
|
required this.isCompleted,
|
|
|
this.spinnerDuration = const Duration(seconds: 5),
|
|
|
this.tickDuration = const Duration(milliseconds: 1500),
|
|
|
});
|
|
|
|
|
|
@override
|
|
|
State<LoadingIndicator> createState() => _LoadingIndicatorState();
|
|
|
}
|
|
|
|
|
|
class _LoadingIndicatorState extends State<LoadingIndicator> {
|
|
|
final List<String> _steps = const [
|
|
|
'Processing...',
|
|
|
'Preprocessing the query...',
|
|
|
'Geolocating...',
|
|
|
'Fetching news...',
|
|
|
'Analysing bias in news articles...',
|
|
|
];
|
|
|
|
|
|
int _currentStep = 0;
|
|
|
bool _showTick = false;
|
|
|
|
|
|
|
|
|
bool _running = false;
|
|
|
bool _stopRequested = false;
|
|
|
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
_startLoop();
|
|
|
}
|
|
|
|
|
|
void _startLoop() {
|
|
|
|
|
|
if (_running) return;
|
|
|
_stopRequested = false;
|
|
|
_runLoop();
|
|
|
}
|
|
|
|
|
|
Future<void> _runLoop() async {
|
|
|
_running = true;
|
|
|
final spinnerDelay = widget.spinnerDuration;
|
|
|
final tickDelay = widget.tickDuration;
|
|
|
|
|
|
while (mounted && !_stopRequested) {
|
|
|
|
|
|
for (int i = 0; i < _steps.length; i++) {
|
|
|
if (!mounted || _stopRequested) break;
|
|
|
|
|
|
|
|
|
setState(() {
|
|
|
_currentStep = i;
|
|
|
_showTick = false;
|
|
|
});
|
|
|
|
|
|
|
|
|
await Future.delayed(spinnerDelay);
|
|
|
if (!mounted) break;
|
|
|
|
|
|
|
|
|
if (widget.isCompleted) {
|
|
|
setState(() => _showTick = true);
|
|
|
_stopRequested = true;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
|
|
|
setState(() => _showTick = true);
|
|
|
|
|
|
|
|
|
await Future.delayed(tickDelay);
|
|
|
if (!mounted) break;
|
|
|
|
|
|
|
|
|
if (widget.isCompleted) {
|
|
|
_stopRequested = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (_stopRequested || !mounted) break;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
if (mounted) {
|
|
|
setState(() => _showTick = true);
|
|
|
}
|
|
|
_running = false;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void didUpdateWidget(covariant LoadingIndicator oldWidget) {
|
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
|
|
|
|
|
if (!oldWidget.isCompleted && widget.isCompleted) {
|
|
|
_stopRequested = true;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (oldWidget.isCompleted && !widget.isCompleted) {
|
|
|
_stopRequested = false;
|
|
|
if (!_running) _startLoop();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void dispose() {
|
|
|
_stopRequested = true;
|
|
|
super.dispose();
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final String text = _steps[_currentStep];
|
|
|
|
|
|
|
|
|
return Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
|
|
|
AnimatedSwitcher(
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
transitionBuilder: (child, animation) =>
|
|
|
FadeTransition(opacity: animation, child: child),
|
|
|
child: Text(
|
|
|
text,
|
|
|
key: ValueKey<int>(_currentStep),
|
|
|
style: const TextStyle(
|
|
|
color: Colors.white,
|
|
|
fontSize: 16,
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
|
AnimatedSwitcher(
|
|
|
duration: const Duration(milliseconds: 250),
|
|
|
transitionBuilder: (child, animation) =>
|
|
|
ScaleTransition(scale: animation, child: child),
|
|
|
child: _showTick
|
|
|
? const Icon(
|
|
|
Icons.check,
|
|
|
key: ValueKey('tick'),
|
|
|
color: Colors.green,
|
|
|
size: 20,
|
|
|
)
|
|
|
: const SizedBox(
|
|
|
key: ValueKey('spinner'),
|
|
|
width: 20,
|
|
|
height: 20,
|
|
|
child: CircularProgressIndicator(
|
|
|
strokeWidth: 2,
|
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|