File size: 5,259 Bytes
5bda825
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// loading_indicator.dart
import 'dart:async';
import 'package:flutter/material.dart';

class LoadingIndicator extends StatefulWidget {
  /// When true, the indicator will finish the current step (show its tick)
  /// and then stop looping.
  final bool isCompleted;

  /// How long the spinner shows for each step.
  final Duration spinnerDuration;

  /// How long the tick remains visible before moving to next step.
  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;

  // Control flags for the async loop
  bool _running = false;
  bool _stopRequested = false;

  @override
  void initState() {
    super.initState();
    _startLoop();
  }

  void _startLoop() {
    // Ensure we don't spawn multiple loops
    if (_running) return;
    _stopRequested = false;
    _runLoop();
  }

  Future<void> _runLoop() async {
    _running = true;
    final spinnerDelay = widget.spinnerDuration;
    final tickDelay = widget.tickDuration;

    while (mounted && !_stopRequested) {
      // Go through all steps sequentially (this completes one full cycle)
      for (int i = 0; i < _steps.length; i++) {
        if (!mounted || _stopRequested) break;

        // show spinner for this step
        setState(() {
          _currentStep = i;
          _showTick = false;
        });

        // wait spinner time
        await Future.delayed(spinnerDelay);
        if (!mounted) break;

        // if parent requested completion while spinner ran, show tick then stop
        if (widget.isCompleted) {
          setState(() => _showTick = true);
          _stopRequested = true;
          break;
        }

        // show tick for this step
        setState(() => _showTick = true);

        // wait tick time
        await Future.delayed(tickDelay);
        if (!mounted) break;

        // if parent requested completion during tick, stop after this tick
        if (widget.isCompleted) {
          _stopRequested = true;
          break;
        }
      }

      // If stop requested, break out of the outer while
      if (_stopRequested || !mounted) break;

      // Completed all steps — loop again from step 0
      // continue while loop -> next for-loop iteration restarts steps
    }

    // Ensure final UI shows tick on whatever the currentStep was
    if (mounted) {
      setState(() => _showTick = true);
    }
    _running = false;
  }

  @override
  void didUpdateWidget(covariant LoadingIndicator oldWidget) {
    super.didUpdateWidget(oldWidget);

    // If parent toggles completion on, request stop so loop finishes current step
    if (!oldWidget.isCompleted && widget.isCompleted) {
      _stopRequested = true;
    }

    // If parent re-enables (isCompleted went false) restart loop if not running
    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];

    // Single Row — only the text and icon inside AnimatedSwitchers change.
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        // Animated replace of text (one-line only)
        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),
        // Animated replace of spinner <-> tick
        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),
                  ),
                ),
        ),
      ],
    );
  }
}