mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-06 20:57:57 +02:00
feat: replace batch operation snackbars with progress dialog
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.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user