Files
SpotiFLAC-Mobile/lib/widgets/audio_analysis_info_card.dart
T

314 lines
11 KiB
Dart

part of 'audio_analysis_widget.dart';
// Read-only info card with metric chips for a finished analysis.
class _AudioInfoCard extends StatelessWidget {
final AudioAnalysisData data;
final VoidCallback? onRescan;
const _AudioInfoCard({required this.data, this.onRescan});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final nyquist = data.sampleRate / 2;
return Card(
elevation: 0,
color: settingsGroupColor(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.analytics_outlined, color: cs.primary, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
context.l10n.audioAnalysisTitle,
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
if (onRescan != null)
IconButton(
icon: const Icon(Icons.refresh, size: 20),
tooltip: context.l10n.audioAnalysisRescan,
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
color: cs.onSurfaceVariant,
onPressed: onRescan,
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 16,
runSpacing: 8,
children: [
if (data.codec.isNotEmpty)
_MetricChip(
icon: Icons.memory,
label: context.l10n.audioAnalysisCodec,
value: data.codec,
cs: cs,
),
if (data.container.isNotEmpty)
_MetricChip(
icon: Icons.inventory_2_outlined,
label: context.l10n.audioAnalysisContainer,
value: data.container,
cs: cs,
),
_MetricChip(
icon: Icons.graphic_eq,
label: context.l10n.audioAnalysisSampleRate,
value: '${(data.sampleRate / 1000).toStringAsFixed(1)} kHz',
cs: cs,
),
_MetricChip(
icon: Icons.audio_file,
label: context.l10n.audioAnalysisBitDepth,
value: data.bitDepth,
cs: cs,
),
if (data.decodedSampleFormat.isNotEmpty)
_MetricChip(
icon: Icons.data_object,
label: context.l10n.audioAnalysisDecodedFormat,
value: data.decodedSampleFormat,
cs: cs,
),
if (data.bitrate > 0)
_MetricChip(
icon: Icons.speed,
label: context.l10n.trackConvertBitrate,
value: _formatBitrate(data.bitrate),
cs: cs,
),
_MetricChip(
icon: Icons.surround_sound,
label: context.l10n.audioAnalysisChannels,
value: _formatChannels(context, data),
cs: cs,
),
_MetricChip(
icon: Icons.timer_outlined,
label: context.l10n.audioAnalysisDuration,
value: formatClock(data.duration),
cs: cs,
),
_MetricChip(
icon: Icons.multiline_chart,
label: context.l10n.audioAnalysisNyquist,
value: '${(nyquist / 1000).toStringAsFixed(1)} kHz',
cs: cs,
),
if (data.fileSize > 0)
_MetricChip(
icon: Icons.storage,
label: context.l10n.audioAnalysisFileSize,
value: formatBytes(data.fileSize),
cs: cs,
),
],
),
const SizedBox(height: 8),
Divider(color: cs.outlineVariant),
const SizedBox(height: 8),
Wrap(
spacing: 16,
runSpacing: 8,
children: [
_MetricChip(
icon: Icons.trending_up,
label: context.l10n.audioAnalysisDynamicRange,
value: '${data.dynamicRange.toStringAsFixed(2)} dB',
cs: cs,
),
_MetricChip(
icon: Icons.show_chart,
label: context.l10n.audioAnalysisPeak,
value: '${data.peakAmplitude.toStringAsFixed(2)} dB',
cs: cs,
),
_MetricChip(
icon: Icons.equalizer,
label: context.l10n.audioAnalysisRms,
value: '${data.rmsLevel.toStringAsFixed(2)} dB',
cs: cs,
),
if (data.integratedLufs != null)
_MetricChip(
icon: Icons.volume_up_outlined,
label: context.l10n.audioAnalysisLufs,
value: '${data.integratedLufs!.toStringAsFixed(1)} LUFS',
cs: cs,
),
if (data.truePeakDb != null)
_MetricChip(
icon: Icons.warning_amber_outlined,
label: context.l10n.audioAnalysisTruePeak,
value: '${data.truePeakDb!.toStringAsFixed(2)} dBTP',
cs: cs,
),
_MetricChip(
icon: Icons.report_gmailerrorred_outlined,
label: context.l10n.audioAnalysisClipping,
value: _formatClipping(context, data.clippingSamples),
cs: cs,
),
if (data.spectralCutoffHz != null)
_MetricChip(
icon: Icons.filter_alt_outlined,
label: context.l10n.audioAnalysisSpectralCutoff,
value: _formatFrequency(data.spectralCutoffHz!),
cs: cs,
),
_MetricChip(
icon: Icons.numbers,
label: context.l10n.audioAnalysisSamples,
value: _formatNumber(data.totalSamples),
cs: cs,
),
],
),
if (data.channelStats.length > 1) ...[
const SizedBox(height: 8),
Divider(color: cs.outlineVariant),
const SizedBox(height: 8),
Text(
context.l10n.audioAnalysisChannelStats,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 8,
children: data.channelStats.map((stats) {
return _MetricChip(
icon: Icons.surround_sound,
label: 'Ch ${stats.channel}',
value: _formatChannelStats(stats),
cs: cs,
);
}).toList(),
),
],
],
),
),
);
}
String _formatChannels(BuildContext context, AudioAnalysisData data) {
final layout = data.channelLayout.trim();
if (layout.isNotEmpty && layout != 'unknown') {
return data.channels > 0 ? '${data.channels} ($layout)' : layout;
}
if (data.channels == 2) return context.l10n.audioAnalysisStereo;
if (data.channels == 1) return context.l10n.audioAnalysisMono;
return data.channels > 0 ? '${data.channels}' : 'N/A';
}
String _formatFrequency(double hz) {
if (hz >= 1000) return '${(hz / 1000).toStringAsFixed(1)} kHz';
return '${hz.round()} Hz';
}
String _formatBitrate(int bitsPerSecond) {
if (bitsPerSecond >= 1000000) {
return '${(bitsPerSecond / 1000000).toStringAsFixed(2)} Mbps';
}
return '${(bitsPerSecond / 1000).round()} kbps';
}
String _formatClipping(BuildContext context, int samples) {
if (samples <= 0) return context.l10n.audioAnalysisNoClipping;
return _formatNumber(samples);
}
String _formatChannelStats(ChannelAnalysisStats stats) {
final parts = <String>[];
if (stats.peakDb != null) {
parts.add('P ${stats.peakDb!.toStringAsFixed(1)}');
}
if (stats.rmsDb != null) {
parts.add('R ${stats.rmsDb!.toStringAsFixed(1)}');
}
if (stats.dynamicRangeDb != null) {
parts.add('DR ${stats.dynamicRangeDb!.toStringAsFixed(1)}');
}
if (stats.peakCount > 0 && (stats.peakDb ?? -100) >= -0.1) {
parts.add('Clip ${_formatNumber(stats.peakCount)}');
}
return parts.isEmpty ? 'N/A' : parts.join(' / ');
}
String _formatNumber(int n) {
if (n >= 1000000) return '${(n / 1000000).toStringAsFixed(1)}M';
if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}K';
return n.toString();
}
}
class _MetricChip extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final ColorScheme cs;
const _MetricChip({
required this.icon,
required this.label,
required this.value,
required this.cs,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(
'$label: ',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
Flexible(
child: Text(
value,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: cs.onSurface,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
}