mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-04-21 19:16:01 +02:00
25de009ebc
Add reusable BatchProgressDialog widget with circular/linear progress indicators, cancel support, and track detail display. Uses ValueNotifier pattern to communicate progress from caller to dialog across navigator routes.
194 lines
5.8 KiB
Dart
194 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||
|
||
/// Progress state communicated from caller to dialog via [ValueNotifier].
|
||
class _BatchProgress {
|
||
final int current;
|
||
final String? detail;
|
||
const _BatchProgress({this.current = 0, this.detail});
|
||
}
|
||
|
||
/// A reusable progress dialog for batch operations like conversion and
|
||
/// re-enrich. Follows the same visual style as [_FetchingProgressDialog] in
|
||
/// artist_screen.dart.
|
||
///
|
||
/// Uses a static [ValueNotifier] so callers do not need the dialog's
|
||
/// [BuildContext] to push updates – unlike `findAncestorStateOfType` which
|
||
/// fails because the dialog lives in a separate navigator route.
|
||
///
|
||
/// Usage:
|
||
/// ```dart
|
||
/// var cancelled = false;
|
||
/// BatchProgressDialog.show(
|
||
/// context: context,
|
||
/// title: 'Converting...',
|
||
/// total: items.length,
|
||
/// icon: Icons.transform,
|
||
/// onCancel: () {
|
||
/// cancelled = true;
|
||
/// BatchProgressDialog.dismiss(context);
|
||
/// },
|
||
/// );
|
||
///
|
||
/// for (int i = 0; i < items.length; i++) {
|
||
/// if (cancelled) break;
|
||
/// BatchProgressDialog.update(current: i + 1, detail: items[i].name);
|
||
/// await doWork(items[i]);
|
||
/// }
|
||
///
|
||
/// BatchProgressDialog.dismiss(context);
|
||
/// ```
|
||
class BatchProgressDialog extends StatefulWidget {
|
||
final String title;
|
||
final int total;
|
||
final IconData icon;
|
||
final VoidCallback onCancel;
|
||
final ValueNotifier<_BatchProgress> _progressNotifier;
|
||
|
||
// ignore: prefer_const_constructors_in_immutables
|
||
BatchProgressDialog._({
|
||
required this.title,
|
||
required this.total,
|
||
required this.icon,
|
||
required this.onCancel,
|
||
required ValueNotifier<_BatchProgress> progressNotifier,
|
||
}) : _progressNotifier = progressNotifier;
|
||
|
||
// ── Static bookkeeping ──────────────────────────────────────────────
|
||
|
||
static ValueNotifier<_BatchProgress>? _activeNotifier;
|
||
|
||
/// Show the dialog. Call [update] to push progress, [dismiss] to close.
|
||
static void show({
|
||
required BuildContext context,
|
||
required String title,
|
||
required int total,
|
||
required VoidCallback onCancel,
|
||
IconData icon = Icons.transform,
|
||
}) {
|
||
_activeNotifier = ValueNotifier(const _BatchProgress());
|
||
final notifier = _activeNotifier!;
|
||
|
||
showDialog<void>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (_) => BatchProgressDialog._(
|
||
title: title,
|
||
total: total,
|
||
icon: icon,
|
||
onCancel: onCancel,
|
||
progressNotifier: notifier,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Update the progress of the currently visible dialog.
|
||
/// No [BuildContext] needed – communicates via [ValueNotifier].
|
||
static void update({required int current, String? detail}) {
|
||
_activeNotifier?.value = _BatchProgress(current: current, detail: detail);
|
||
}
|
||
|
||
/// Dismiss the dialog and clean up.
|
||
static void dismiss(BuildContext context) {
|
||
_activeNotifier = null;
|
||
Navigator.of(context, rootNavigator: true).pop();
|
||
}
|
||
|
||
@override
|
||
State<BatchProgressDialog> createState() => _BatchProgressDialogState();
|
||
}
|
||
|
||
class _BatchProgressDialogState extends State<BatchProgressDialog> {
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
widget._progressNotifier.addListener(_onChanged);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
widget._progressNotifier.removeListener(_onChanged);
|
||
super.dispose();
|
||
}
|
||
|
||
void _onChanged() {
|
||
if (mounted) setState(() {});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final textTheme = Theme.of(context).textTheme;
|
||
|
||
final current = widget._progressNotifier.value.current;
|
||
final detail = widget._progressNotifier.value.detail;
|
||
final progress = widget.total > 0 ? current / widget.total : 0.0;
|
||
|
||
return AlertDialog(
|
||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const SizedBox(height: 8),
|
||
SizedBox(
|
||
width: 64,
|
||
height: 64,
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
CircularProgressIndicator(
|
||
value: progress > 0 ? progress : null,
|
||
strokeWidth: 4,
|
||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||
),
|
||
Icon(widget.icon, color: colorScheme.primary, size: 24),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
Text(
|
||
widget.title,
|
||
style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'$current / ${widget.total}',
|
||
style: textTheme.bodyMedium?.copyWith(
|
||
color: colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
if (detail != null && detail.isNotEmpty) ...[
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
detail,
|
||
style: textTheme.bodySmall?.copyWith(
|
||
color: colorScheme.onSurfaceVariant,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
const SizedBox(height: 12),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(4),
|
||
child: LinearProgressIndicator(
|
||
value: progress > 0 ? progress : null,
|
||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||
minHeight: 6,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: widget.onCancel,
|
||
child: Text(context.l10n.dialogCancel),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|