fix(ui): center modals on large screens, modernize edit-metadata + convert sheets, themed badges, fix artist skeleton and format-editor crash

- app: clear displayFeatures so bottom sheets/dialogs center on large/foldable screens

- edit metadata sheet: card sections, modern label-above inputs, elegant collapsible headers, removed title icon

- convert + batch convert: modern card-based sheets; shared BatchConvertSheet widget

- queue: keep selection toolbar hidden until modal close animation finishes

- 24-bit and In Library badges now use primary dynamic color

- artist skeleton: remove duplicate name/listeners lines, keep cover placeholder

- files settings: own filename-format controller in a StatefulWidget to fix use-after-dispose crash
This commit is contained in:
zarzet
2026-06-13 02:08:06 +07:00
parent b8b670642c
commit ca413a16fa
26 changed files with 1564 additions and 1224 deletions
+3 -3
View File
@@ -1088,7 +1088,7 @@ class _AlbumTrackItem extends ConsumerWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: colorScheme.tertiaryContainer,
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Row(
@@ -1097,7 +1097,7 @@ class _AlbumTrackItem extends ConsumerWidget {
Icon(
Icons.folder_outlined,
size: 10,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
const SizedBox(width: 3),
Text(
@@ -1105,7 +1105,7 @@ class _AlbumTrackItem extends ConsumerWidget {
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
),
],
+3 -3
View File
@@ -1586,7 +1586,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
vertical: 2,
),
decoration: BoxDecoration(
color: colorScheme.tertiaryContainer,
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Row(
@@ -1595,7 +1595,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
Icon(
Icons.folder_outlined,
size: 10,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
const SizedBox(width: 3),
Text(
@@ -1603,7 +1603,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
),
],
+14 -152
View File
@@ -17,6 +17,7 @@ import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
import 'package:spotiflac_android/widgets/batch_convert_sheet.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
@@ -967,164 +968,25 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
if (formats.isEmpty) return;
String selectedFormat = formats.first;
bool isLosslessTarget = isLosslessConversionTarget(selectedFormat);
String defaultBitrateForFormat(String format) {
if (format == 'Opus') return '128k';
if (format == 'AAC') return '256k';
return '320k';
}
String selectedBitrate = isLosslessTarget
? '320k'
: defaultBitrateForFormat(selectedFormat);
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (sheetContext) {
return StatefulBuilder(
builder: (context, setSheetState) {
final colorScheme = Theme.of(context).colorScheme;
final bitrates = ['128k', '192k', '256k', '320k'];
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(
alpha: 0.4,
),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Text(
context.l10n.selectionBatchConvertConfirmTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Text(
context.l10n.trackConvertTargetFormat,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: formats.map((format) {
final isSelected = format == selectedFormat;
return ChoiceChip(
label: Text(format),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() {
selectedFormat = format;
isLosslessTarget = isLosslessConversionTarget(
format,
);
if (!isLosslessTarget) {
selectedBitrate = defaultBitrateForFormat(
format,
);
}
});
}
},
);
}).toList(),
),
if (!isLosslessTarget) ...[
const SizedBox(height: 16),
Text(
context.l10n.trackConvertBitrate,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: bitrates.map((br) {
final isSelected = br == selectedBitrate;
return ChoiceChip(
label: Text(br),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() => selectedBitrate = br);
}
},
);
}).toList(),
),
],
if (isLosslessTarget) ...[
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.verified,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: 6),
Text(
context.l10n.trackConvertLosslessHint,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colorScheme.primary),
),
],
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
_performBatchConversion(
allTracks: allTracks,
targetFormat: selectedFormat,
bitrate: selectedBitrate,
);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
context.l10n.selectionConvertCount(
_selectedIds.length,
),
),
),
),
],
),
),
);
},
);
},
builder: (sheetContext) => BatchConvertSheet(
formats: formats,
title: context.l10n.selectionBatchConvertConfirmTitle,
confirmLabel: context.l10n.selectionConvertCount(_selectedIds.length),
onConvert: (format, bitrate) {
Navigator.pop(sheetContext);
_performBatchConversion(
allTracks: allTracks,
targetFormat: format,
bitrate: bitrate,
);
},
),
);
}
+2 -1
View File
@@ -31,6 +31,7 @@ import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
part 'home_tab_helpers.dart';
part 'home_tab_widgets.dart';
@@ -3560,7 +3561,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
decoration: InputDecoration(
hintText: _getSearchHint(),
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
fillColor: settingsGroupColor(context),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(color: colorScheme.outlineVariant),
+3 -3
View File
@@ -347,7 +347,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: colorScheme.tertiaryContainer,
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Row(
@@ -356,7 +356,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
Icon(
Icons.folder_outlined,
size: 10,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
const SizedBox(width: 3),
Text(
@@ -364,7 +364,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
),
],
@@ -1234,7 +1234,7 @@ class _CollectionTrackTile extends ConsumerWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: colorScheme.tertiaryContainer,
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Row(
@@ -1243,7 +1243,7 @@ class _CollectionTrackTile extends ConsumerWidget {
Icon(
Icons.folder_outlined,
size: 10,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
const SizedBox(width: 3),
Text(
@@ -1251,7 +1251,7 @@ class _CollectionTrackTile extends ConsumerWidget {
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
),
],
+14 -152
View File
@@ -17,6 +17,7 @@ import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/replaygain_service.dart';
import 'package:spotiflac_android/services/local_track_redownload_service.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
import 'package:spotiflac_android/widgets/batch_convert_sheet.dart';
import 'package:spotiflac_android/widgets/re_enrich_field_dialog.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
@@ -1215,164 +1216,25 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
if (formats.isEmpty) return;
String selectedFormat = formats.first;
bool isLosslessTarget = isLosslessConversionTarget(selectedFormat);
String defaultBitrateForFormat(String format) {
if (format == 'Opus') return '128k';
if (format == 'AAC') return '256k';
return '320k';
}
String selectedBitrate = isLosslessTarget
? '320k'
: defaultBitrateForFormat(selectedFormat);
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (sheetContext) {
return StatefulBuilder(
builder: (context, setSheetState) {
final colorScheme = Theme.of(context).colorScheme;
final bitrates = ['128k', '192k', '256k', '320k'];
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(
alpha: 0.4,
),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Text(
context.l10n.selectionBatchConvertConfirmTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Text(
context.l10n.trackConvertTargetFormat,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: formats.map((format) {
final isSelected = format == selectedFormat;
return ChoiceChip(
label: Text(format),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() {
selectedFormat = format;
isLosslessTarget = isLosslessConversionTarget(
format,
);
if (!isLosslessTarget) {
selectedBitrate = defaultBitrateForFormat(
format,
);
}
});
}
},
);
}).toList(),
),
if (!isLosslessTarget) ...[
const SizedBox(height: 16),
Text(
context.l10n.trackConvertBitrate,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: bitrates.map((br) {
final isSelected = br == selectedBitrate;
return ChoiceChip(
label: Text(br),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() => selectedBitrate = br);
}
},
);
}).toList(),
),
],
if (isLosslessTarget) ...[
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.verified,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: 6),
Text(
context.l10n.trackConvertLosslessHint,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colorScheme.primary),
),
],
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
_performBatchConversion(
allTracks: allTracks,
targetFormat: selectedFormat,
bitrate: selectedBitrate,
);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
context.l10n.selectionConvertCount(
_selectedIds.length,
),
),
),
),
],
),
),
);
},
);
},
builder: (sheetContext) => BatchConvertSheet(
formats: formats,
title: context.l10n.selectionBatchConvertConfirmTitle,
confirmLabel: context.l10n.selectionConvertCount(_selectedIds.length),
onConvert: (format, bitrate) {
Navigator.pop(sheetContext);
_performBatchConversion(
allTracks: allTracks,
targetFormat: format,
bitrate: bitrate,
);
},
),
);
}
+19 -14
View File
@@ -23,6 +23,7 @@ import 'package:spotiflac_android/services/update_checker.dart';
import 'package:spotiflac_android/widgets/app_announcement_dialog.dart';
import 'package:spotiflac_android/widgets/update_dialog.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('MainShell');
@@ -570,20 +571,24 @@ class _MainShellState extends ConsumerState<MainShell>
);
},
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex.clamp(0, maxIndex),
onDestinationSelected: _onNavTap,
animationDuration: const Duration(milliseconds: 500),
backgroundColor: Theme.of(context).brightness == Brightness.dark
? Color.alphaBlend(
Colors.white.withValues(alpha: 0.05),
Theme.of(context).colorScheme.surface,
)
: Color.alphaBlend(
Colors.black.withValues(alpha: 0.03),
Theme.of(context).colorScheme.surface,
),
destinations: destinations,
bottomNavigationBar: DecoratedBox(
position: DecorationPosition.foreground,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(
context,
).colorScheme.outlineVariant.withValues(alpha: 0.5),
),
),
),
child: NavigationBar(
selectedIndex: _currentIndex.clamp(0, maxIndex),
onDestinationSelected: _onNavTap,
animationDuration: const Duration(milliseconds: 500),
backgroundColor: settingsGroupColor(context),
destinations: destinations,
),
),
),
);
+3 -3
View File
@@ -900,7 +900,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: colorScheme.tertiaryContainer,
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Row(
@@ -909,7 +909,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
Icon(
Icons.folder_outlined,
size: 10,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
const SizedBox(width: 3),
Text(
@@ -917,7 +917,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
color: colorScheme.onTertiaryContainer,
color: colorScheme.onPrimaryContainer,
),
),
],
+56 -161
View File
@@ -12,6 +12,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/models/download_item.dart';
@@ -31,6 +32,7 @@ import 'package:spotiflac_android/screens/favorite_artists_screen.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
import 'package:spotiflac_android/widgets/re_enrich_field_dialog.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
import 'package:spotiflac_android/widgets/batch_convert_sheet.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
import 'package:spotiflac_android/screens/local_album_screen.dart';
@@ -121,6 +123,12 @@ class _QueueTabState extends ConsumerState<QueueTab> {
List<UnifiedLibraryItem> _selectionOverlayItems = const [];
double _selectionOverlayBottomPadding = 0;
/// When true, the floating selection overlays are kept hidden even though
/// selection mode is still active. Used while a modal sheet/dialog launched
/// from the selection toolbar is open, so the overlay does not reappear on
/// top of (or behind) the modal's open/close animation.
bool _suppressSelectionOverlay = false;
bool _isPlaylistSelectionMode = false;
final Set<String> _selectedPlaylistIds = {};
OverlayEntry? _playlistSelectionOverlayEntry;
@@ -809,7 +817,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
required double bottomPadding,
}) {
if (!mounted) return;
if (!_isSelectionMode || _isPlaylistSelectionMode) {
if (_suppressSelectionOverlay ||
!_isSelectionMode ||
_isPlaylistSelectionMode) {
_hideSelectionOverlay();
return;
}
@@ -857,7 +867,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
required double bottomPadding,
}) {
if (!mounted) return;
if (!_isPlaylistSelectionMode || _isSelectionMode) {
if (_suppressSelectionOverlay ||
!_isPlaylistSelectionMode ||
_isSelectionMode) {
_hidePlaylistSelectionOverlay();
return;
}
@@ -2775,7 +2787,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
)
: null,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
fillColor: settingsGroupColor(context),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
@@ -4835,19 +4847,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
if (formats.isEmpty) return;
String selectedFormat = formats.first;
bool isLosslessTarget = isLosslessConversionTarget(selectedFormat);
String defaultBitrateForFormat(String format) {
if (format == 'Opus') return '128k';
if (format == 'AAC') return '256k';
return '320k';
}
String selectedBitrate = isLosslessTarget
? '320k'
: defaultBitrateForFormat(selectedFormat);
var didStartConversion = false;
_suppressSelectionOverlay = true;
_hideSelectionOverlay();
_hidePlaylistSelectionOverlay();
@@ -4857,150 +4859,33 @@ class _QueueTabState extends ConsumerState<QueueTab> {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (sheetContext) {
return StatefulBuilder(
builder: (context, setSheetState) {
final colorScheme = Theme.of(context).colorScheme;
final bitrates = ['128k', '192k', '256k', '320k'];
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(
alpha: 0.4,
),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Text(
context.l10n.selectionBatchConvertConfirmTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Text(
context.l10n.trackConvertTargetFormat,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: formats.map((format) {
final isSelected = format == selectedFormat;
return ChoiceChip(
label: Text(format),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() {
selectedFormat = format;
isLosslessTarget = isLosslessConversionTarget(
format,
);
if (!isLosslessTarget) {
selectedBitrate = defaultBitrateForFormat(
format,
);
}
});
}
},
);
}).toList(),
),
if (!isLosslessTarget) ...[
const SizedBox(height: 16),
Text(
context.l10n.trackConvertBitrate,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: bitrates.map((br) {
final isSelected = br == selectedBitrate;
return ChoiceChip(
label: Text(br),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() => selectedBitrate = br);
}
},
);
}).toList(),
),
],
if (isLosslessTarget) ...[
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.verified,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: 6),
Text(
context.l10n.trackConvertLosslessHint,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colorScheme.primary),
),
],
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () {
didStartConversion = true;
Navigator.pop(context);
_performBatchConversion(
allItems: allItems,
targetFormat: selectedFormat,
bitrate: selectedBitrate,
);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
context.l10n.selectionConvertCount(
_selectedIds.length,
),
),
),
),
],
),
),
);
},
);
},
builder: (sheetContext) => BatchConvertSheet(
formats: formats,
title: context.l10n.selectionBatchConvertConfirmTitle,
confirmLabel: context.l10n.selectionConvertCount(_selectedIds.length),
onConvert: (format, bitrate) {
didStartConversion = true;
Navigator.pop(sheetContext);
_performBatchConversion(
allItems: allItems,
targetFormat: format,
bitrate: bitrate,
);
},
),
);
if (!mounted || didStartConversion) return;
// The showModalBottomSheet future completes when the sheet begins closing,
// not when its exit animation finishes. Wait out the exit transition
// (~200ms) before restoring the selection toolbar so it does not pop in
// front of the still-animating sheet.
await Future<void>.delayed(const Duration(milliseconds: 260));
if (!mounted) {
_suppressSelectionOverlay = false;
return;
}
_suppressSelectionOverlay = false;
if (didStartConversion) return;
if (_isSelectionMode) {
_syncSelectionOverlay(
items: allItems,
@@ -5399,6 +5284,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
if (selectedItems.isEmpty) return;
_suppressSelectionOverlay = true;
_hideSelectionOverlay();
_hidePlaylistSelectionOverlay();
@@ -5422,8 +5308,16 @@ class _QueueTabState extends ConsumerState<QueueTab> {
),
);
if (!mounted) return;
if (!mounted) {
_suppressSelectionOverlay = false;
return;
}
if (confirmed != true) {
// Restore after the dialog's exit animation so the toolbar does not
// appear in front of the closing dialog.
await Future<void>.delayed(const Duration(milliseconds: 220));
_suppressSelectionOverlay = false;
if (!mounted) return;
if (_isSelectionMode) {
_syncSelectionOverlay(
items: allItems,
@@ -5432,6 +5326,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
}
return;
}
_suppressSelectionOverlay = false;
var cancelled = false;
int successCount = 0;
@@ -6360,7 +6255,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
),
decoration: BoxDecoration(
color: item.quality!.startsWith('24')
? colorScheme.tertiaryContainer
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
@@ -6369,7 +6264,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
color: item.quality!.startsWith('24')
? colorScheme.onTertiaryContainer
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.w500,
@@ -6513,7 +6408,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
),
decoration: BoxDecoration(
color: item.quality!.startsWith('24')
? colorScheme.tertiary
? colorScheme.primary
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
@@ -6522,7 +6417,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
color: item.quality!.startsWith('24')
? colorScheme.onTertiary
? colorScheme.onPrimary
: colorScheme.onSurfaceVariant,
fontSize: 9,
fontWeight: FontWeight.w600,
+2
View File
@@ -89,6 +89,8 @@ class _FilterChip extends StatelessWidget {
selected: isSelected,
onSelected: (_) => onTap(),
showCheckmark: false,
backgroundColor: settingsGroupColor(context),
side: BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.6)),
);
}
}
+4 -1
View File
@@ -172,7 +172,7 @@ class _RepoTabState extends ConsumerState<RepoTab> {
),
),
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
fillColor: settingsGroupColor(context),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
@@ -651,6 +651,7 @@ class _CategoryChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
@@ -659,6 +660,8 @@ class _CategoryChip extends StatelessWidget {
selected: isSelected,
onSelected: (_) => onTap(),
showCheckmark: false,
backgroundColor: settingsGroupColor(context),
side: BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.6)),
);
}
}
+258 -216
View File
@@ -708,53 +708,14 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
String? title,
String? description,
}) {
final controller = TextEditingController(text: current);
final colorScheme = Theme.of(context).colorScheme;
final save =
onSave ?? ref.read(settingsProvider.notifier).setFilenameFormat;
final basicTags = [
'{artist}',
'{title}',
'{album}',
'{track}',
'{year}',
'{date}',
'{disc}',
];
final advancedTags = [
'{track_raw}',
'{track:02}',
'{track:1}',
'{date:%Y}',
'{date:%Y-%m-%d}',
'{disc_raw}',
'{disc:02}',
];
var showAdvancedTags = RegExp(
r'\{(?:track_raw|disc_raw|track:\d+|disc:\d+|date:[^}]+)\}',
caseSensitive: false,
).hasMatch(current);
void insertTag(String tag) {
final text = controller.text;
final selection = controller.selection;
final start = selection.start >= 0 ? selection.start : text.length;
final end = selection.end >= 0 ? selection.end : text.length;
String insertion = tag;
if (start > 0) {
final before = text.substring(0, start);
if (!before.trim().endsWith('-')) {
insertion = ' - $tag';
} else if (before.trim().endsWith('-') && !before.endsWith(' ')) {
insertion = ' $tag';
}
}
final newText = text.replaceRange(start, end, insertion);
controller.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: start + insertion.length),
);
}
// The controller is owned by a StatefulWidget so it is disposed in its
// State.dispose() (after the subtree is removed), instead of in
// whenComplete which fires while the closing/keyboard-hide animations can
// still rebuild the TextField and touch a disposed controller.
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
@@ -763,178 +724,13 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (context) => StatefulBuilder(
builder: (context, setModalState) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 32,
height: 4,
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
Text(
title ?? context.l10n.filenameFormat,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
description ??
context.l10n.downloadFilenameDescription(
'{album}',
'{artist}',
'{date}',
'{disc}',
'{title}',
'{track}',
'{year}',
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextField(
controller: controller,
decoration: InputDecoration(
hintText: '{artist} - {title}',
filled: true,
fillColor: colorScheme.surfaceContainerHighest
.withValues(alpha: 0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
autofocus: true,
),
const SizedBox(height: 24),
Text(
context.l10n.downloadFilenameInsertTag,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: basicTags.map((tag) {
return ActionChip(
label: Text(tag),
onPressed: () => insertTag(tag),
backgroundColor: colorScheme.surfaceContainerHighest
.withValues(alpha: 0.5),
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
labelStyle: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
);
}).toList(),
),
const SizedBox(height: 12),
SwitchListTile(
value: showAdvancedTags,
onChanged: (value) =>
setModalState(() => showAdvancedTags = value),
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.filenameShowAdvancedTags),
subtitle: Text(
context.l10n.filenameShowAdvancedTagsDescription,
),
),
if (showAdvancedTags) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: advancedTags.map((tag) {
return ActionChip(
label: Text(tag),
onPressed: () => insertTag(tag),
backgroundColor: colorScheme.surfaceContainerHighest
.withValues(alpha: 0.5),
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
labelStyle: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
);
}).toList(),
),
],
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(context.l10n.dialogCancel),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: FilledButton(
onPressed: () {
final save =
onSave ??
ref
.read(settingsProvider.notifier)
.setFilenameFormat;
save(controller.text);
Navigator.pop(context);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(context.l10n.dialogSave),
),
),
],
),
const SizedBox(height: 8),
],
),
),
),
),
),
builder: (context) => _FilenameFormatEditorSheet(
initialText: current,
onSave: save,
title: title,
description: description,
),
).whenComplete(controller.dispose);
);
}
void _showAlbumFolderStructurePicker(
@@ -1140,3 +936,249 @@ class _FolderOption extends StatelessWidget {
);
}
}
/// Bottom sheet body for editing a filename format. Owns its
/// [TextEditingController] and disposes it in [dispose], which runs only after
/// the sheet's subtree has been removed from the tree. This avoids the
/// "TextEditingController used after being disposed" crash that happens when
/// the controller is torn down in `whenComplete` while the closing and
/// keyboard-hide animations are still rebuilding the field.
class _FilenameFormatEditorSheet extends StatefulWidget {
final String initialText;
final void Function(String) onSave;
final String? title;
final String? description;
const _FilenameFormatEditorSheet({
required this.initialText,
required this.onSave,
this.title,
this.description,
});
@override
State<_FilenameFormatEditorSheet> createState() =>
_FilenameFormatEditorSheetState();
}
class _FilenameFormatEditorSheetState
extends State<_FilenameFormatEditorSheet> {
static const _basicTags = [
'{artist}',
'{title}',
'{album}',
'{track}',
'{year}',
'{date}',
'{disc}',
];
static const _advancedTags = [
'{track_raw}',
'{track:02}',
'{track:1}',
'{date:%Y}',
'{date:%Y-%m-%d}',
'{disc_raw}',
'{disc:02}',
];
late final TextEditingController _controller;
late bool _showAdvancedTags;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText);
_showAdvancedTags = RegExp(
r'\{(?:track_raw|disc_raw|track:\d+|disc:\d+|date:[^}]+)\}',
caseSensitive: false,
).hasMatch(widget.initialText);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _insertTag(String tag) {
final text = _controller.text;
final selection = _controller.selection;
final start = selection.start >= 0 ? selection.start : text.length;
final end = selection.end >= 0 ? selection.end : text.length;
String insertion = tag;
if (start > 0) {
final before = text.substring(0, start);
if (!before.trim().endsWith('-')) {
insertion = ' - $tag';
} else if (before.trim().endsWith('-') && !before.endsWith(' ')) {
insertion = ' $tag';
}
}
final newText = text.replaceRange(start, end, insertion);
_controller.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: start + insertion.length),
);
}
Widget _tagChip(ColorScheme colorScheme, String tag) {
return ActionChip(
label: Text(tag),
onPressed: () => _insertTag(tag),
backgroundColor: colorScheme.surfaceContainerHighest.withValues(
alpha: 0.5,
),
side: BorderSide.none,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
labelStyle: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SingleChildScrollView(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 32,
height: 4,
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
Text(
widget.title ?? context.l10n.filenameFormat,
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
widget.description ??
context.l10n.downloadFilenameDescription(
'{album}',
'{artist}',
'{date}',
'{disc}',
'{title}',
'{track}',
'{year}',
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextField(
controller: _controller,
decoration: InputDecoration(
hintText: '{artist} - {title}',
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withValues(
alpha: 0.3,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
autofocus: true,
),
const SizedBox(height: 24),
Text(
context.l10n.downloadFilenameInsertTag,
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _basicTags
.map((tag) => _tagChip(colorScheme, tag))
.toList(),
),
const SizedBox(height: 12),
SwitchListTile(
value: _showAdvancedTags,
onChanged: (value) =>
setState(() => _showAdvancedTags = value),
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.filenameShowAdvancedTags),
subtitle: Text(
context.l10n.filenameShowAdvancedTagsDescription,
),
),
if (_showAdvancedTags) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _advancedTags
.map((tag) => _tagChip(colorScheme, tag))
.toList(),
),
],
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(context.l10n.dialogCancel),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: FilledButton(
onPressed: () {
widget.onSave(_controller.text);
Navigator.pop(context);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(context.l10n.dialogSave),
),
),
],
),
const SizedBox(height: 8),
],
),
),
),
),
);
}
}
+442 -360
View File
@@ -1263,7 +1263,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
child: Row(
children: [
Expanded(
@@ -1275,132 +1275,144 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
),
),
if (_saving)
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
else
FilledButton(
FilledButton.icon(
onPressed: _save,
child: Text(context.l10n.dialogSave),
icon: const Icon(Icons.check, size: 18),
label: Text(context.l10n.dialogSave),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
),
],
),
),
const SizedBox(height: 12),
Divider(
height: 1,
color: cs.outlineVariant.withValues(alpha: 0.5),
),
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.fromLTRB(20, 6, 20, 24),
children: [
const SizedBox(height: 6),
_buildCoverEditor(cs),
_buildAutoFillSection(cs),
_field(context.l10n.editMetadataFieldTitle, _titleCtrl),
_field(context.l10n.editMetadataFieldArtist, _artistCtrl),
_field(context.l10n.editMetadataFieldAlbum, _albumCtrl),
_field(
context.l10n.editMetadataFieldAlbumArtist,
_albumArtistCtrl,
),
_field(
context.l10n.editMetadataFieldDate,
_dateCtrl,
hint: context.l10n.editMetadataFieldDateHint,
),
Row(
_sectionCard(
icon: Icons.info_outline,
title: context.l10n.trackMetadata,
children: [
Expanded(
child: _field(
context.l10n.editMetadataFieldTrackNum,
_trackNumCtrl,
keyboard: TextInputType.number,
),
_field(context.l10n.editMetadataFieldTitle, _titleCtrl),
_field(
context.l10n.editMetadataFieldArtist,
_artistCtrl,
),
const SizedBox(width: 12),
Expanded(
child: _field(
context.l10n.editMetadataFieldTrackTotal,
_trackTotalCtrl,
keyboard: TextInputType.number,
),
_field(context.l10n.editMetadataFieldAlbum, _albumCtrl),
_field(
context.l10n.editMetadataFieldAlbumArtist,
_albumArtistCtrl,
),
_field(
context.l10n.editMetadataFieldDate,
_dateCtrl,
hint: context.l10n.editMetadataFieldDateHint,
),
Row(
children: [
Expanded(
child: _field(
context.l10n.editMetadataFieldTrackNum,
_trackNumCtrl,
keyboard: TextInputType.number,
),
),
const SizedBox(width: 12),
Expanded(
child: _field(
context.l10n.editMetadataFieldTrackTotal,
_trackTotalCtrl,
keyboard: TextInputType.number,
),
),
],
),
Row(
children: [
Expanded(
child: _field(
context.l10n.editMetadataFieldDiscNum,
_discNumCtrl,
keyboard: TextInputType.number,
),
),
const SizedBox(width: 12),
Expanded(
child: _field(
context.l10n.editMetadataFieldDiscTotal,
_discTotalCtrl,
keyboard: TextInputType.number,
),
),
],
),
_field(context.l10n.editMetadataFieldGenre, _genreCtrl),
_field(context.l10n.editMetadataFieldIsrc, _isrcCtrl),
],
),
_sectionCard(
icon: Icons.lyrics_outlined,
title: context.l10n.trackLyrics,
children: [
_field(
context.l10n.trackLyrics,
_lyricsCtrl,
maxLines: 8,
keyboard: TextInputType.multiline,
),
],
),
const SizedBox(height: 12),
Row(
_sectionCard(
icon: Icons.tune,
title: context.l10n.editMetadataAdvanced,
onHeaderTap: () =>
setState(() => _showAdvanced = !_showAdvanced),
expanded: _showAdvanced,
children: [
Expanded(
child: _field(
context.l10n.editMetadataFieldDiscNum,
_discNumCtrl,
keyboard: TextInputType.number,
if (_showAdvanced) ...[
_field(
context.l10n.editMetadataFieldLabel,
_labelCtrl,
),
),
const SizedBox(width: 12),
Expanded(
child: _field(
context.l10n.editMetadataFieldDiscTotal,
_discTotalCtrl,
keyboard: TextInputType.number,
_field(
context.l10n.editMetadataFieldCopyright,
_copyrightCtrl,
),
),
_field(
context.l10n.editMetadataFieldComposer,
_composerCtrl,
),
_field(
context.l10n.editMetadataFieldComment,
_commentCtrl,
maxLines: 3,
),
],
],
),
_field(context.l10n.editMetadataFieldGenre, _genreCtrl),
_field(context.l10n.editMetadataFieldIsrc, _isrcCtrl),
_field(
context.l10n.trackLyrics,
_lyricsCtrl,
maxLines: 8,
keyboard: TextInputType.multiline,
),
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: InkWell(
onTap: () =>
setState(() => _showAdvanced = !_showAdvanced),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(
_showAdvanced
? Icons.expand_less
: Icons.expand_more,
size: 20,
color: cs.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
context.l10n.editMetadataAdvanced,
style: Theme.of(context).textTheme.labelLarge
?.copyWith(color: cs.onSurfaceVariant),
),
],
),
),
),
),
if (_showAdvanced) ...[
_field(context.l10n.editMetadataFieldLabel, _labelCtrl),
_field(
context.l10n.editMetadataFieldCopyright,
_copyrightCtrl,
),
_field(
context.l10n.editMetadataFieldComposer,
_composerCtrl,
),
_field(
context.l10n.editMetadataFieldComment,
_commentCtrl,
maxLines: 3,
),
],
const SizedBox(height: 24),
],
),
),
@@ -1411,149 +1423,104 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
}
Widget _buildAutoFillSection(ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => setState(() => _showAutoFill = !_showAutoFill),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Row(
children: [
Icon(Icons.travel_explore, size: 20, color: cs.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
context.l10n.editMetadataAutoFill,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: cs.onSurface,
fontWeight: FontWeight.w600,
),
),
),
Icon(
_showAutoFill ? Icons.expand_less : Icons.expand_more,
size: 20,
color: cs.onSurfaceVariant,
),
],
),
return _sectionCard(
icon: Icons.travel_explore,
title: context.l10n.editMetadataAutoFill,
onHeaderTap: () => setState(() => _showAutoFill = !_showAutoFill),
expanded: _showAutoFill,
children: [
if (_showAutoFill) ...[
Text(
context.l10n.editMetadataAutoFillDesc,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
const SizedBox(height: 12),
Row(
children: [
_quickSelectButton(
label: context.l10n.editMetadataSelectAll,
onTap: _selectAllFields,
cs: cs,
),
),
if (_showAutoFill) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
context.l10n.editMetadataAutoFillDesc,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
const SizedBox(width: 8),
_quickSelectButton(
label: context.l10n.editMetadataSelectEmpty,
onTap: _selectEmptyFields,
cs: cs,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
_quickSelectButton(
label: context.l10n.editMetadataSelectAll,
onTap: _selectAllFields,
cs: cs,
),
const SizedBox(width: 8),
_quickSelectButton(
label: context.l10n.editMetadataSelectEmpty,
onTap: _selectEmptyFields,
cs: cs,
),
const SizedBox(width: 8),
_quickSelectButton(
label: context.l10n.editMetadataSelectNone,
onTap: _selectNoFields,
cs: cs,
),
],
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap(
spacing: 6,
runSpacing: 4,
children: _fieldDefs.keys.map((key) {
final selected = _autoFillFields.contains(key);
return FilterChip(
label: Text(_fieldLabel(key)),
selected: selected,
onSelected: _fetching
? null
: (val) {
setState(() {
if (val) {
_autoFillFields.add(key);
} else {
_autoFillFields.remove(key);
}
});
},
selectedColor: cs.primaryContainer,
checkmarkColor: cs.onPrimaryContainer,
labelStyle: Theme.of(context).textTheme.labelSmall
?.copyWith(
color: selected
? cs.onPrimaryContainer
: cs.onSurfaceVariant,
),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}).toList(),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: (_fetching || _saving || _autoFillFields.isEmpty)
? null
: _fetchAndFill,
icon: _fetching
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.auto_fix_high),
label: Text(
_fetching
? context.l10n.editMetadataAutoFillSearching
: context.l10n.editMetadataAutoFillFetch,
),
),
),
const SizedBox(width: 8),
_quickSelectButton(
label: context.l10n.editMetadataSelectNone,
onTap: _selectNoFields,
cs: cs,
),
],
],
),
),
),
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 4,
children: _fieldDefs.keys.map((key) {
final selected = _autoFillFields.contains(key);
return FilterChip(
label: Text(_fieldLabel(key)),
selected: selected,
onSelected: _fetching
? null
: (val) {
setState(() {
if (val) {
_autoFillFields.add(key);
} else {
_autoFillFields.remove(key);
}
});
},
selectedColor: cs.primaryContainer,
checkmarkColor: cs.onPrimaryContainer,
labelStyle: Theme.of(context).textTheme.labelSmall?.copyWith(
color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}).toList(),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: (_fetching || _saving || _autoFillFields.isEmpty)
? null
: _fetchAndFill,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: _fetching
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.auto_fix_high),
label: Text(
_fetching
? context.l10n.editMetadataAutoFillSearching
: context.l10n.editMetadataAutoFillFetch,
),
),
),
const SizedBox(height: 8),
],
],
);
}
@@ -1566,7 +1533,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
onTap: _fetching ? null : onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline.withValues(alpha: 0.5)),
@@ -1584,103 +1551,97 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
Widget _buildCoverEditor(ColorScheme cs) {
final hasSelectedCover = _hasValue(_selectedCoverPath);
final hasCurrentCover = _hasValue(_currentCoverPath);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.editMetadataFieldCover,
return _sectionCard(
icon: Icons.image_outlined,
title: context.l10n.editMetadataFieldCover,
children: [
if (_loadingCurrentCover)
const Padding(
padding: EdgeInsets.only(bottom: 8),
child: LinearProgressIndicator(minHeight: 2),
)
else if (!hasCurrentCover && !hasSelectedCover)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
context.l10n.trackCoverNoEmbeddedArt,
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(color: cs.onSurface),
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
const SizedBox(height: 6),
if (_loadingCurrentCover)
const LinearProgressIndicator(minHeight: 2)
else if (!hasCurrentCover)
Text(
context.l10n.trackCoverNoEmbeddedArt,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _saving ? null : _pickCoverImage,
icon: const Icon(Icons.image_outlined),
label: Text(
hasSelectedCover
? context.l10n.trackCoverReplace
: context.l10n.trackCoverPick,
),
),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _saving ? null : _pickCoverImage,
icon: const Icon(Icons.image_outlined),
label: Text(
hasSelectedCover
? context.l10n.trackCoverReplace
: context.l10n.trackCoverPick,
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
if (hasSelectedCover) ...[
const SizedBox(width: 8),
IconButton(
tooltip: context.l10n.trackCoverClearSelected,
onPressed: _saving
? null
: () async {
await _cleanupSelectedCoverTemp();
if (!mounted) return;
setState(() {});
},
icon: const Icon(Icons.close),
),
],
],
),
if (hasCurrentCover || hasSelectedCover) ...[
const SizedBox(height: 12),
Row(
children: [
if (hasCurrentCover)
Expanded(
child: _buildCoverPreviewTile(
cs: cs,
path: _currentCoverPath!,
label: context.l10n.trackCoverCurrent,
),
),
if (hasCurrentCover && hasSelectedCover)
const SizedBox(width: 12),
if (hasSelectedCover)
Expanded(
child: _buildCoverPreviewTile(
cs: cs,
path: _selectedCoverPath!,
label:
_selectedCoverName ??
context.l10n.trackCoverSelected,
),
),
],
),
if (hasSelectedCover) ...[
const SizedBox(height: 8),
Text(
context.l10n.trackCoverReplaceNotice,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
],
),
if (hasSelectedCover) ...[
const SizedBox(width: 8),
IconButton(
tooltip: context.l10n.trackCoverClearSelected,
onPressed: _saving
? null
: () async {
await _cleanupSelectedCoverTemp();
if (!mounted) return;
setState(() {});
},
icon: const Icon(Icons.close),
),
],
],
),
),
if (hasCurrentCover || hasSelectedCover) ...[
const SizedBox(height: 12),
Row(
children: [
if (hasCurrentCover)
Expanded(
child: _buildCoverPreviewTile(
cs: cs,
path: _currentCoverPath!,
label: context.l10n.trackCoverCurrent,
),
),
if (hasCurrentCover && hasSelectedCover)
const SizedBox(width: 12),
if (hasSelectedCover)
Expanded(
child: _buildCoverPreviewTile(
cs: cs,
path: _selectedCoverPath!,
label:
_selectedCoverName ?? context.l10n.trackCoverSelected,
),
),
],
),
if (hasSelectedCover) ...[
const SizedBox(height: 8),
Text(
context.l10n.trackCoverReplaceNotice,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
],
],
const SizedBox(height: 8),
],
);
}
@@ -1738,6 +1699,16 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
);
}
/// Fill for the modern input fields. Sits one elevation step apart from the
/// section card so each field reads as a distinct, recessed surface in both
/// light and dark (including AMOLED) themes.
Color _fieldFill(ColorScheme cs) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark
? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), cs.surface)
: cs.surface;
}
Widget _field(
String label,
TextEditingController controller, {
@@ -1746,36 +1717,147 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
int maxLines = 1,
}) {
final cs = widget.colorScheme;
final radius = BorderRadius.circular(14);
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 6),
child: Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
),
),
),
TextField(
controller: controller,
keyboardType: keyboard,
maxLines: maxLines,
cursorColor: cs.primary,
style: Theme.of(context).textTheme.bodyLarge,
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: _fieldFill(cs),
isDense: true,
// Borderless by default; definition comes from the fill contrast.
// A soft primary ring appears only on focus for a clean look.
border: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide(color: cs.primary, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 15,
),
),
),
],
),
);
}
/// Shared shape for the edit sections, mirroring the bounded cards used on the
/// track metadata screen (rounded with a subtle outline).
RoundedRectangleBorder _sectionCardShape(ColorScheme cs) {
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)),
);
}
/// A titled section card matching the track metadata screen layout. When
/// [onHeaderTap] is provided the header becomes a full-width tappable row so
/// the ink ripple follows the card's rounded shape (clipped to the card),
/// and a chevron is rendered automatically based on [expanded].
Widget _sectionCard({
required IconData icon,
required String title,
required List<Widget> children,
VoidCallback? onHeaderTap,
bool expanded = true,
}) {
final cs = widget.colorScheme;
final collapsible = onHeaderTap != null;
final headerRow = Row(
children: [
Icon(icon, size: 20, color: cs.primary),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: cs.onSurface,
),
),
),
if (collapsible)
AnimatedRotation(
turns: expanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
child: Icon(
Icons.expand_more,
size: 22,
color: cs.onSurfaceVariant,
),
),
],
);
final Widget header = collapsible
? InkWell(
onTap: onHeaderTap,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: headerRow,
),
)
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: headerRow,
);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: controller,
keyboardType: keyboard,
maxLines: maxLines,
decoration: InputDecoration(
labelText: label,
hintText: hint,
filled: true,
fillColor: cs.surfaceContainerHighest.withValues(alpha: 0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.5),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
child: Card(
elevation: 0,
margin: EdgeInsets.zero,
color: settingsGroupColor(context),
shape: _sectionCardShape(cs),
clipBehavior: Clip.antiAlias,
child: AnimatedSize(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
alignment: Alignment.topCenter,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
header,
if (children.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
],
),
),
),
+188 -86
View File
@@ -1468,6 +1468,17 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
);
}
/// Shared shape for the main section cards: rounded with a subtle outline so
/// each section (Metadata, File Info, Lyrics, Audio Analysis) is bounded.
RoundedRectangleBorder _sectionCardShape(ColorScheme colorScheme) {
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
);
}
Widget _buildMetadataCard(
BuildContext context,
ColorScheme colorScheme,
@@ -1475,8 +1486,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
) {
return Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
color: settingsGroupColor(context),
shape: _sectionCardShape(colorScheme),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
@@ -1789,8 +1800,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
return Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
color: settingsGroupColor(context),
shape: _sectionCardShape(colorScheme),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
@@ -1997,8 +2008,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
Widget _buildLyricsCard(BuildContext context, ColorScheme colorScheme) {
return Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
color: settingsGroupColor(context),
shape: _sectionCardShape(colorScheme),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
@@ -3451,6 +3462,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
height: size,
fit: BoxFit.cover,
memCacheWidth: cacheWidth,
memCacheHeight: cacheWidth,
errorWidget: (_, _, _) => placeholder(),
);
}
@@ -3728,9 +3740,84 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
final colorScheme = Theme.of(context).colorScheme;
final bitrates = ['128k', '192k', '256k', '320k'];
Widget card({required Widget child}) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: settingsGroupColor(context),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
),
child: child,
);
}
Widget sectionLabel(String text) {
return Padding(
padding: const EdgeInsets.only(left: 2, bottom: 12),
child: Text(
text,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
),
),
);
}
Widget choice({
required String label,
required bool selected,
required VoidCallback onTap,
}) {
return Material(
color: selected
? colorScheme.primaryContainer
: colorScheme.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 11,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: selected
? Colors.transparent
: colorScheme.outlineVariant.withValues(
alpha: 0.6,
),
),
),
child: Text(
label,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: selected
? colorScheme.onPrimaryContainer
: colorScheme.onSurface,
fontWeight: selected
? FontWeight.w600
: FontWeight.w500,
),
),
),
),
);
}
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -3747,97 +3834,112 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 18),
Text(
context.l10n.trackConvertTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
const SizedBox(height: 4),
Text(
context.l10n.trackConvertTargetFormat,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
currentFormat,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: formats.map((format) {
final isSelected = format == selectedFormat;
return ChoiceChip(
label: Text(format),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() {
selectedFormat = format;
isLosslessTarget = isLosslessConversionTarget(
format,
);
if (!isLosslessTarget) {
selectedBitrate = defaultBitrateForFormat(
format,
);
}
});
}
},
);
}).toList(),
),
const SizedBox(height: 20),
if (!isLosslessTarget) ...[
const SizedBox(height: 16),
Text(
context.l10n.trackConvertBitrate,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: bitrates.map((br) {
final isSelected = br == selectedBitrate;
return ChoiceChip(
label: Text(br),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setSheetState(() => selectedBitrate = br);
}
},
);
}).toList(),
),
],
if (isLosslessTarget && isLosslessSource) ...[
const SizedBox(height: 16),
Row(
card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.verified,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: 6),
Text(
context.l10n.trackConvertLosslessHint,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colorScheme.primary),
sectionLabel(context.l10n.trackConvertTargetFormat),
Wrap(
spacing: 8,
runSpacing: 8,
children: formats.map((format) {
return choice(
label: format,
selected: format == selectedFormat,
onTap: () {
setSheetState(() {
selectedFormat = format;
isLosslessTarget =
isLosslessConversionTarget(format);
if (!isLosslessTarget) {
selectedBitrate =
defaultBitrateForFormat(format);
}
});
},
);
}).toList(),
),
],
),
],
const SizedBox(height: 24),
),
if (!isLosslessTarget)
card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
sectionLabel(context.l10n.trackConvertBitrate),
Wrap(
spacing: 8,
runSpacing: 8,
children: bitrates.map((br) {
return choice(
label: br,
selected: br == selectedBitrate,
onTap: () => setSheetState(
() => selectedBitrate = br,
),
);
}).toList(),
),
],
),
),
if (isLosslessTarget && isLosslessSource)
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(
alpha: 0.4,
),
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
Icon(
Icons.verified,
size: 18,
color: colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
context.l10n.trackConvertLosslessHint,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colorScheme.primary),
),
),
],
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: FilledButton(
child: FilledButton.icon(
onPressed: () {
Navigator.pop(context);
_confirmAndConvert(
@@ -3847,20 +3949,20 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
bitrate: selectedBitrate,
);
},
icon: const Icon(Icons.swap_horiz),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(14),
),
),
child: Text(
label: Text(
isLosslessTarget
? '$currentFormat -> $selectedFormat (Lossless)'
: '$currentFormat -> $selectedFormat @ $selectedBitrate',
? '$currentFormat $selectedFormat (Lossless)'
: '$currentFormat $selectedFormat @ $selectedBitrate',
),
),
),
const SizedBox(height: 8),
],
),
),