1+ import 'dart:async' ;
2+
13import 'package:flutter/foundation.dart' ;
24import 'package:flutter/material.dart' ;
35import 'package:just_audio/just_audio.dart' ;
@@ -15,49 +17,81 @@ class DocumentAudio extends StatefulWidget {
1517 final bool decrypt;
1618
1719 @override
18- State <DocumentAudio > createState () => _DocumentAudioState ();
20+ State <DocumentAudio > createState () => DocumentAudioState ();
1921}
2022
21- class _DocumentAudioState extends State <DocumentAudio > {
22- final AudioPlayer _player = AudioPlayer () ;
23+ class DocumentAudioState extends State <DocumentAudio > {
24+ AudioPlayer ? _player;
2325 bool _isLoading = true ;
2426 bool _isPlaying = false ;
27+ bool _shutDown = false ;
2528 Duration _position = Duration .zero;
2629 Duration _duration = Duration .zero;
2730 String ? _errorMessage;
2831
32+ final List <StreamSubscription <dynamic >> _subscriptions = [];
33+
34+ /// Stops playback, cancels subscriptions, and disposes the native player.
35+ /// Must be awaited before the widget is removed from the tree (e.g. before
36+ /// popping a dialog) to avoid the just_audio_windows threading crash.
37+ Future <void > shutdown () async {
38+ if (_shutDown) return ;
39+ _shutDown = true ;
40+
41+ for (final sub in _subscriptions) {
42+ sub.cancel ();
43+ }
44+ _subscriptions.clear ();
45+
46+ final player = _player;
47+ _player = null ;
48+
49+ if (player != null ) {
50+ try {
51+ await player.stop ();
52+ } catch (_) {}
53+ try {
54+ await player.dispose ();
55+ } catch (_) {}
56+ }
57+ }
58+
2959 @override
3060 void initState () {
3161 super .initState ();
32- _loadAudio ();
33- _player.playerStateStream.listen ((state) {
34- if (! mounted) return ;
35- final playing =
36- state.playing &&
37- state.processingState != ProcessingState .completed;
38- setState (() {
39- _isPlaying = playing;
40- });
41- // Seek to start when playback completes so the user can replay.
42- if (state.processingState == ProcessingState .completed) {
43- _player.seek (Duration .zero);
44- _player.pause ();
45- }
46- });
47- _player.positionStream.listen ((pos) {
48- if (mounted) {
62+ _player = AudioPlayer ();
63+ _subscriptions.add (
64+ _player! .playerStateStream.listen ((state) {
65+ if (_shutDown || ! mounted) return ;
66+ final playing =
67+ state.playing && state.processingState != ProcessingState .completed;
4968 setState (() {
50- _position = pos ;
69+ _isPlaying = playing ;
5170 });
52- }
53- });
54- _player.durationStream.listen ((dur) {
55- if (mounted && dur != null ) {
71+ if (state.processingState == ProcessingState .completed) {
72+ _player? .seek (Duration .zero).then ((_) => _player? .pause ());
73+ }
74+ }),
75+ );
76+ _subscriptions.add (
77+ _player! .positionStream.listen ((pos) {
78+ if (_shutDown || ! mounted) return ;
5679 setState (() {
57- _duration = dur ;
80+ _position = pos ;
5881 });
59- }
60- });
82+ }),
83+ );
84+ _subscriptions.add (
85+ _player! .durationStream.listen ((dur) {
86+ if (_shutDown || ! mounted) return ;
87+ if (dur != null ) {
88+ setState (() {
89+ _duration = dur;
90+ });
91+ }
92+ }),
93+ );
94+ _loadAudio ();
6195 }
6296
6397 Future <void > _loadAudio () async {
@@ -66,26 +100,25 @@ class _DocumentAudioState extends State<DocumentAudio> {
66100 documentId: widget.documentId,
67101 decrypt: widget.decrypt,
68102 );
69- if (mounted) {
70- if (file != null ) {
71- try {
72- await _player.setFilePath (file.path);
73- } catch (e) {
74- if (kDebugMode) {
75- print ("Error setting file path: $e " );
76- }
77- rethrow ;
103+ if (_shutDown || ! mounted) return ;
104+ if (file != null ) {
105+ try {
106+ await _player? .setFilePath (file.path);
107+ } catch (e) {
108+ if (kDebugMode) {
109+ print ("Error setting file path: $e " );
78110 }
79- } else {
111+ if (_shutDown || ! mounted) return ;
80112 _errorMessage = 'Fehler beim Laden' ;
81113 }
114+ } else {
115+ _errorMessage = 'Fehler beim Laden' ;
82116 }
83117 } catch (e) {
84- if (mounted) {
85- _errorMessage = 'Fehler: $e ' ;
86- }
118+ if (_shutDown || ! mounted) return ;
119+ _errorMessage = 'Fehler: $e ' ;
87120 } finally {
88- if (mounted) {
121+ if (! _shutDown && mounted) {
89122 setState (() {
90123 _isLoading = false ;
91124 });
@@ -95,7 +128,19 @@ class _DocumentAudioState extends State<DocumentAudio> {
95128
96129 @override
97130 void dispose () {
98- _player.dispose ();
131+ // Fallback: if shutdown() was not called explicitly, clean up now.
132+ if (! _shutDown) {
133+ _shutDown = true ;
134+ for (final sub in _subscriptions) {
135+ sub.cancel ();
136+ }
137+ _subscriptions.clear ();
138+ final player = _player;
139+ _player = null ;
140+ if (player != null ) {
141+ player.stop ().then ((_) => player.dispose ()).ignore ();
142+ }
143+ }
99144 super .dispose ();
100145 }
101146
@@ -159,10 +204,9 @@ class _DocumentAudioState extends State<DocumentAudio> {
159204 );
160205 }
161206
162- final progress =
163- _duration.inMilliseconds > 0
164- ? _position.inMilliseconds / _duration.inMilliseconds
165- : 0.0 ;
207+ final progress = _duration.inMilliseconds > 0
208+ ? _position.inMilliseconds / _duration.inMilliseconds
209+ : 0.0 ;
166210
167211 return Container (
168212 decoration: BoxDecoration (
@@ -183,9 +227,9 @@ class _DocumentAudioState extends State<DocumentAudio> {
183227 ),
184228 onPressed: () {
185229 if (_isPlaying) {
186- _player.pause ();
230+ _player? .pause ();
187231 } else {
188- _player.play ();
232+ _player? .play ();
189233 }
190234 },
191235 iconSize: 28 ,
@@ -196,37 +240,62 @@ class _DocumentAudioState extends State<DocumentAudio> {
196240 child: Column (
197241 mainAxisSize: MainAxisSize .min,
198242 children: [
199- ClipRRect (
200- borderRadius: BorderRadius .circular (2 ),
201- child: LinearProgressIndicator (
202- value: progress,
203- minHeight: 4 ,
204- backgroundColor: AppColors .interactiveColor.withValues (
243+ SliderTheme (
244+ data: SliderTheme .of (context).copyWith (
245+ activeTrackColor: AppColors .interactiveColor,
246+ inactiveTrackColor: AppColors .interactiveColor.withValues (
205247 alpha: 0.15 ,
206248 ),
207- valueColor: AlwaysStoppedAnimation <Color >(
208- AppColors .interactiveColor,
249+ thumbColor: AppColors .interactiveColor,
250+ overlayColor: AppColors .interactiveColor.withValues (
251+ alpha: 0.2 ,
209252 ),
210- ),
211- ),
212- const SizedBox (height: 2 ),
213- Row (
214- mainAxisAlignment: MainAxisAlignment .spaceBetween,
215- children: [
216- Text (
217- _formatDuration (_position),
218- style: const TextStyle (fontSize: 11 , color: Colors .grey),
253+ trackHeight: 4 ,
254+ thumbShape: const RoundSliderThumbShape (
255+ enabledThumbRadius: 6 ,
219256 ),
220- Text (
221- _formatDuration (_duration),
222- style: const TextStyle (fontSize: 11 , color: Colors .grey),
257+ overlayShape: const RoundSliderOverlayShape (
258+ overlayRadius: 14 ,
223259 ),
224- ],
260+ ),
261+ child: Slider (
262+ value: progress.clamp (0.0 , 1.0 ),
263+ onChanged: (value) {
264+ if (_duration.inMilliseconds > 0 ) {
265+ final position = Duration (
266+ milliseconds: (value * _duration.inMilliseconds)
267+ .round (),
268+ );
269+ _player? .seek (position);
270+ }
271+ },
272+ ),
273+ ),
274+ Padding (
275+ padding: const EdgeInsets .symmetric (horizontal: 12 ),
276+ child: Row (
277+ mainAxisAlignment: MainAxisAlignment .spaceBetween,
278+ children: [
279+ Text (
280+ _formatDuration (_position),
281+ style: const TextStyle (
282+ fontSize: 11 ,
283+ color: Colors .grey,
284+ ),
285+ ),
286+ Text (
287+ _formatDuration (_duration),
288+ style: const TextStyle (
289+ fontSize: 11 ,
290+ color: Colors .grey,
291+ ),
292+ ),
293+ ],
294+ ),
225295 ),
226296 ],
227297 ),
228298 ),
229- const SizedBox (width: 8 ),
230299 ],
231300 ),
232301 );
0 commit comments