mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
Retain the outgoing decoder only while its frame remains displayed, then release its provider subscription as soon as replacement artwork is ready. Verify decoder disposal while preserving cover transitions.
101 lines
3.3 KiB
Dart
101 lines
3.3 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:spotiflac_android/providers/player_artwork_video_provider.dart';
|
|
import 'package:spotiflac_android/widgets/motion_header_banner.dart';
|
|
import 'package:spotiflac_android/widgets/player_artwork.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
class MornyePlayerArtwork extends ConsumerStatefulWidget {
|
|
const MornyePlayerArtwork({
|
|
super.key,
|
|
required this.mediaItem,
|
|
this.videoUrl,
|
|
this.onAspectRatioChanged,
|
|
this.onError,
|
|
this.resolvingVideo = false,
|
|
});
|
|
|
|
final MediaItem mediaItem;
|
|
final String? videoUrl;
|
|
final ValueChanged<double>? onAspectRatioChanged;
|
|
final VoidCallback? onError;
|
|
final bool resolvingVideo;
|
|
|
|
@override
|
|
ConsumerState<MornyePlayerArtwork> createState() =>
|
|
_MornyePlayerArtworkState();
|
|
}
|
|
|
|
class _MornyePlayerArtworkState extends ConsumerState<MornyePlayerArtwork> {
|
|
String? _displayedSource;
|
|
VideoPlayerController? _displayedController;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final fallback = Align(
|
|
alignment: Alignment.topCenter,
|
|
child: AspectRatio(
|
|
aspectRatio: 1,
|
|
child: PlayerArtwork(
|
|
artUri: widget.mediaItem.artUri?.toString(),
|
|
colorScheme: Theme.of(context).colorScheme,
|
|
cacheWidth:
|
|
(MediaQuery.sizeOf(context).width *
|
|
MediaQuery.devicePixelRatioOf(context))
|
|
.round(),
|
|
),
|
|
),
|
|
);
|
|
if (MediaQuery.disableAnimationsOf(context)) {
|
|
_displayedSource = null;
|
|
_displayedController = null;
|
|
return fallback;
|
|
}
|
|
final videoUrl = widget.videoUrl;
|
|
final prepared = videoUrl == null
|
|
? null
|
|
: ref.watch(playerArtworkVideoProvider(videoUrl));
|
|
if (prepared?.hasError == true) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) widget.onError?.call();
|
|
});
|
|
}
|
|
if (prepared?.value case final controller?) {
|
|
_displayedSource = videoUrl;
|
|
_displayedController = controller;
|
|
} else if (!widget.resolvingVideo &&
|
|
(videoUrl == null || prepared?.hasError == true)) {
|
|
_displayedSource = null;
|
|
_displayedController = null;
|
|
}
|
|
// Retain the outgoing decoder only while it is still displayed. Watching
|
|
// it before accepting the new frame keeps its heap alive until a later
|
|
// unrelated rebuild, even after the old banner has been removed.
|
|
if (_displayedSource != null && _displayedSource != videoUrl) {
|
|
ref.watch(playerArtworkVideoProvider(_displayedSource!));
|
|
}
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
fallback,
|
|
if (_displayedController != null)
|
|
MotionHeaderBanner(
|
|
videoUrl: _displayedSource!,
|
|
fallback: const SizedBox.shrink(),
|
|
controller: _displayedController,
|
|
fadeDuration: Duration.zero,
|
|
onAspectRatioChanged: (ratio) {
|
|
if (_displayedSource == widget.videoUrl) {
|
|
widget.onAspectRatioChanged?.call(ratio);
|
|
}
|
|
},
|
|
onError: () {
|
|
if (_displayedSource == widget.videoUrl) widget.onError?.call();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|