refactor(screens): unify album header, selection UI, and batch engine

The local/downloaded album screens had drifted copies of the album
header, selection bottom bar, disc chip, batch convert/ReplayGain trio,
and assorted small helpers. All now delegate to the shared widgets, with
the online album screen's design as the reference, and both album screens
run batch actions through the queue_tab engine via UnifiedLibraryItem
(strict-superset implementation covering both DB writebacks and SAF
paths). Also folds the remaining per-screen helper copies (cover URL,
error card, byte/clock formatting, readPositiveInt) into their shared
homes.

Intentional deltas: selection-bar strings follow queue_tab's l10n keys,
both providers reload after a conversion, and audio-analysis durations
round instead of floor.
This commit is contained in:
zarzet
2026-07-11 16:36:29 +07:00
parent fca3039208
commit 05415d96d5
19 changed files with 693 additions and 3733 deletions
+4 -24
View File
@@ -14,6 +14,7 @@ import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
class AudioAnalysisData {
static const cacheVersion = 4;
@@ -1378,7 +1379,7 @@ class _AudioInfoCard extends StatelessWidget {
_MetricChip(
icon: Icons.timer_outlined,
label: context.l10n.audioAnalysisDuration,
value: _formatDuration(data.duration),
value: formatClock(data.duration),
cs: cs,
),
_MetricChip(
@@ -1391,7 +1392,7 @@ class _AudioInfoCard extends StatelessWidget {
_MetricChip(
icon: Icons.storage,
label: context.l10n.audioAnalysisFileSize,
value: _formatFileSize(data.fileSize),
value: formatBytes(data.fileSize),
cs: cs,
),
],
@@ -1488,12 +1489,6 @@ class _AudioInfoCard extends StatelessWidget {
);
}
String _formatDuration(double seconds) {
final mins = seconds ~/ 60;
final secs = (seconds % 60).floor();
return '$mins:${secs.toString().padLeft(2, '0')}';
}
String _formatChannels(BuildContext context, AudioAnalysisData data) {
final layout = data.channelLayout.trim();
if (layout.isNotEmpty && layout != 'unknown') {
@@ -1504,14 +1499,6 @@ class _AudioInfoCard extends StatelessWidget {
return data.channels > 0 ? '${data.channels}' : 'N/A';
}
String _formatFileSize(int bytes) {
if (bytes == 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
final i = (math.log(bytes) / math.log(1024)).floor();
final size = bytes / math.pow(1024, i);
return '${size.toStringAsFixed(1)} ${units[i]}';
}
String _formatFrequency(double hz) {
if (hz >= 1000) return '${(hz / 1000).toStringAsFixed(1)} kHz';
return '${hz.round()} Hz';
@@ -1766,7 +1753,7 @@ class _SpectrogramPainter extends CustomPainter {
canvas.drawLine(Offset(x, plot.top), Offset(x, plot.bottom), gridPaint);
_drawText(
canvas,
_fmtTime(ts),
formatClock(ts),
Offset(x, plot.bottom + 3),
align: _TextAlignV.topCenter,
);
@@ -1818,13 +1805,6 @@ class _SpectrogramPainter extends CustomPainter {
return 1200.0;
}
static String _fmtTime(double sec) {
final s = sec.round();
final m = s ~/ 60;
final r = s % 60;
return '$m:${r.toString().padLeft(2, '0')}';
}
@override
bool shouldRepaint(covariant _SpectrogramPainter old) =>
old.image != image ||