feat(audio): add WAV and AIFF support + settings-style metadata menu

WAV/AIFF: library scan, quality probe, native tag read/write via embedded ID3 chunk (RIFF id3 / AIFF ID3), cover art, ReadFileMetadata, ExtractLyrics, and FLAC<->WAV/AIFF conversion (PCM, bit-depth preserved via ffprobe). Treat WAV/AIFF as lossless across all convert sheets (no bitrate picker, Lossless labels) via isLosslessConversionTarget. Native MIME maps for SAF. Redesign the track metadata three-dot menu to a settings-style grouped card with a single divider above Share.
This commit is contained in:
zarzet
2026-06-12 21:10:37 +07:00
parent 2a2e2924eb
commit b8b670642c
17 changed files with 1481 additions and 172 deletions
+12 -49
View File
@@ -4836,8 +4836,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
if (formats.isEmpty) return;
String selectedFormat = formats.first;
bool isLosslessTarget =
selectedFormat == 'ALAC' || selectedFormat == 'FLAC';
bool isLosslessTarget = isLosslessConversionTarget(selectedFormat);
String defaultBitrateForFormat(String format) {
if (format == 'Opus') return '128k';
if (format == 'AAC') return '256k';
@@ -4909,8 +4908,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
if (selected) {
setSheetState(() {
selectedFormat = format;
isLosslessTarget =
format == 'ALAC' || format == 'FLAC';
isLosslessTarget = isLosslessConversionTarget(
format,
);
if (!isLosslessTarget) {
selectedBitrate = defaultBitrateForFormat(
format,
@@ -5049,7 +5049,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
return;
}
final isLossless = targetFormat == 'ALAC' || targetFormat == 'FLAC';
final isLossless = isLosslessConversionTarget(targetFormat);
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
@@ -5085,8 +5085,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final total = selectedItems.length;
final historyDb = HistoryDatabase.instance;
final newQuality =
(targetFormat.toUpperCase() == 'ALAC' ||
targetFormat.toUpperCase() == 'FLAC')
isLosslessConversionTarget(targetFormat)
? '${targetFormat.toUpperCase()} Lossless'
: '${targetFormat.toUpperCase()} ${bitrate.trim().toLowerCase()}';
final settings = ref.read(settingsProvider);
@@ -5196,27 +5195,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final baseName = dotIdx > 0
? oldFileName.substring(0, dotIdx)
: oldFileName;
String newExt;
String mimeType;
switch (targetFormat.toLowerCase()) {
case 'opus':
newExt = '.opus';
mimeType = 'audio/opus';
break;
case 'alac':
case 'aac':
newExt = '.m4a';
mimeType = 'audio/mp4';
break;
case 'flac':
newExt = '.flac';
mimeType = 'audio/flac';
break;
default:
newExt = '.mp3';
mimeType = 'audio/mpeg';
break;
}
final convTarget = convertTargetExtAndMime(targetFormat);
final newExt = convTarget.ext;
final mimeType = convTarget.mime;
final newFileName = '$baseName$newExt';
final safUri = await PlatformBridge.createSafFileFromPath(
@@ -5309,27 +5290,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final baseName = dotIdx > 0
? oldFileName.substring(0, dotIdx)
: oldFileName;
String newExt;
String mimeType;
switch (targetFormat.toLowerCase()) {
case 'opus':
newExt = '.opus';
mimeType = 'audio/opus';
break;
case 'alac':
case 'aac':
newExt = '.m4a';
mimeType = 'audio/mp4';
break;
case 'flac':
newExt = '.flac';
mimeType = 'audio/flac';
break;
default:
newExt = '.mp3';
mimeType = 'audio/mpeg';
break;
}
final convTarget = convertTargetExtAndMime(targetFormat);
final newExt = convTarget.ext;
final mimeType = convTarget.mime;
final newFileName = '$baseName$newExt';
final safUri = await PlatformBridge.createSafFileFromPath(