Files
SpotiFLAC-Mobile/lib/widgets/library_track_selection_bar.dart
T
zarzet a93e552b3e feat(library): add batch playlist and queue actions
Expose playlist, play-next and queue actions across Library search and album selections, including selected artist albums. Sort playlists, keep long selection toolbars scrollable, separate file operations in metadata menus, and show related local songs without online playback.
2026-10-02 18:11:37 +07:00

138 lines
5.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/widgets/app_action_button.dart';
import 'package:spotiflac_android/widgets/selection_action_button.dart';
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
/// Shared Library toolbar for playlist/queue actions and audio file operations.
/// FLAC upgrade applies only to eligible local selections.
class LibraryTrackSelectionBar extends StatelessWidget {
const LibraryTrackSelectionBar({
super.key,
required this.selectedCount,
required this.allSelected,
required this.onClose,
required this.onToggleSelectAll,
required this.bottomPadding,
required this.onReEnrich,
required this.onConvert,
required this.onReplayGain,
required this.onDelete,
this.flacEligibleCount = 0,
this.onQueueFlac,
this.playbackActions,
});
final int selectedCount;
final bool allSelected;
final VoidCallback onClose;
final VoidCallback onToggleSelectAll;
final double bottomPadding;
final VoidCallback onReEnrich;
final VoidCallback onConvert;
final void Function({required bool remove}) onReplayGain;
final VoidCallback onDelete;
/// FLAC upgrade is offered when this is positive and [onQueueFlac] is set.
final int flacEligibleCount;
final VoidCallback? onQueueFlac;
final Widget? playbackActions;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final hasSelection = selectedCount > 0;
return SelectionBottomBar(
selectedCount: selectedCount,
allSelected: allSelected,
onClose: onClose,
onToggleSelectAll: onToggleSelectAll,
bottomPadding: bottomPadding,
children: [
?playbackActions,
if (playbackActions != null)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(context.l10n.trackEditMetadata),
),
LayoutBuilder(
builder: (context, constraints) {
const spacing = 8.0;
final columns = (constraints.maxWidth / 320).floor().clamp(2, 4);
final itemWidth =
(constraints.maxWidth - spacing * (columns - 1)) / columns;
final actions = <Widget>[
if (onQueueFlac != null && flacEligibleCount > 0)
SelectionActionButton(
icon: Icons.download_for_offline_outlined,
label: '${context.l10n.queueFlacAction} ($flacEligibleCount)',
onPressed: onQueueFlac,
colorScheme: colorScheme,
),
SelectionActionButton(
icon: Icons.auto_fix_high_outlined,
label: '${context.l10n.trackReEnrich} ($selectedCount)',
onPressed: hasSelection ? onReEnrich : null,
colorScheme: colorScheme,
),
SelectionActionButton(
icon: Icons.swap_horiz,
label: context.l10n.selectionConvertCount(selectedCount),
onPressed: hasSelection ? onConvert : null,
colorScheme: colorScheme,
),
for (final remove in [false, true])
SelectionActionButton(
icon: remove ? Icons.remove_circle_outline : Icons.graphic_eq,
label: remove
? context.l10n.selectionRemoveReplayGainCount(
selectedCount,
)
: context.l10n.selectionReplayGainCount(selectedCount),
onPressed: hasSelection
? () => onReplayGain(remove: remove)
: null,
colorScheme: colorScheme,
),
];
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: [
for (final action in actions)
SizedBox(width: itemWidth, child: action),
],
);
},
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: AppActionButton(
isDestructive: true,
onPressed: hasSelection ? onDelete : null,
icon: const Icon(Icons.delete_outline),
label: Text(
hasSelection
? context.l10n.selectionDeleteTracksCount(selectedCount)
: context.l10n.selectionSelectToDelete,
),
style: FilledButton.styleFrom(
backgroundColor: hasSelection
? colorScheme.error
: colorScheme.surfaceContainerHighest,
foregroundColor: hasSelection
? colorScheme.onError
: colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
],
);
}
}