mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-04-11 06:12:37 +02:00
Resolve API (api.zarz.moe): - Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API - Add SongLink fallback when resolve API fails for Spotify (two-layer resilience) - Remove dead code: page parser, XOR-obfuscated keys, legacy helpers Multi-artist tag fix (#288): - Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments - Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift) - Add PlatformBridge.rewriteSplitArtistTags() in Dart - Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active - Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks Code cleanup: - Remove unused imports, dead code, and redundant comments across Go and Dart - Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
187 lines
5.3 KiB
Dart
187 lines
5.3 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||
|
||
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 ValueNotifier<_BatchProgress>? _activeNotifier;
|
||
|
||
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,
|
||
),
|
||
);
|
||
}
|
||
|
||
static void update({required int current, String? detail}) {
|
||
_activeNotifier?.value = _BatchProgress(current: current, detail: detail);
|
||
}
|
||
|
||
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),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|