refactor: simplify setup flow and update collection actions

This commit is contained in:
zarzet
2026-02-27 13:47:40 +07:00
parent ab26d84632
commit 2fe8f659bc
4 changed files with 24 additions and 251 deletions
+1 -1
View File
@@ -826,7 +826,7 @@ class _AlbumTrackItem extends ConsumerWidget {
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
),
if (isInLocalLibrary) ...[
if (isInLocalLibrary || isInHistory) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
+13 -66
View File
@@ -798,7 +798,7 @@ class _LibraryTracksFolderScreenState
_buildShuffleButton(entries),
const SizedBox(width: 12),
],
_buildDownloadAllCenterButton(context, entries),
_buildPlayAllCenterButton(entries),
],
),
],
@@ -831,7 +831,7 @@ class _LibraryTracksFolderScreenState
);
}
// ── Shuffle / Download buttons ──
// ── Shuffle / Play buttons ──
Widget _buildShuffleButton(List<CollectionTrackEntry> entries) {
return Container(
@@ -854,15 +854,12 @@ class _LibraryTracksFolderScreenState
);
}
Widget _buildDownloadAllCenterButton(
BuildContext context,
List<CollectionTrackEntry> entries,
) {
Widget _buildPlayAllCenterButton(List<CollectionTrackEntry> entries) {
final tracks = entries.map((e) => e.track).toList(growable: false);
return FilledButton.icon(
onPressed: tracks.isEmpty ? null : () => _downloadAll(context, tracks),
icon: const Icon(Icons.download_rounded, size: 18),
label: Text(context.l10n.downloadAllCount(tracks.length)),
onPressed: tracks.isEmpty ? null : () => _playAll(tracks),
icon: const Icon(Icons.play_arrow_rounded, size: 18),
label: Text(context.l10n.playAllCount(tracks.length)),
style: FilledButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
@@ -885,65 +882,15 @@ class _LibraryTracksFolderScreenState
});
}
void _downloadAll(BuildContext context, List<Track> tracks) {
void _playAll(List<Track> tracks) {
if (tracks.isEmpty) return;
showDialog(
context: context,
builder: (dialogContext) {
final colorScheme = Theme.of(dialogContext).colorScheme;
return AlertDialog(
backgroundColor: colorScheme.surfaceContainerHigh,
title: const Text('Download All'),
content: Text('Download ${tracks.length} tracks?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.dialogCancel),
),
FilledButton(
onPressed: () {
Navigator.pop(dialogContext);
_executeDownloadAll(context, tracks);
},
child: const Text('Download'),
),
],
);
},
);
}
void _executeDownloadAll(BuildContext context, List<Track> tracks) {
final settings = ref.read(settingsProvider);
if (settings.askQualityBeforeDownload) {
DownloadServicePicker.show(
context,
trackName: '${tracks.length} tracks',
artistName: '',
onSelect: (quality, service) {
ref
.read(downloadQueueProvider.notifier)
.addMultipleToQueue(tracks, service, qualityOverride: quality);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarAddedTracksToQueue(tracks.length),
),
),
);
},
final messenger = ScaffoldMessenger.of(context);
ref.read(playbackProvider.notifier).playTrackList(tracks).catchError((e) {
if (!mounted) return;
messenger.showSnackBar(
SnackBar(content: Text('Cannot play local tracks: $e')),
);
} else {
ref
.read(downloadQueueProvider.notifier)
.addMultipleToQueue(tracks, settings.defaultService);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)),
),
);
}
});
}
void _showCoverOptionsSheet(BuildContext context, bool hasCustomCover) {
+9 -19
View File
@@ -10,8 +10,8 @@ import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
import 'package:spotiflac_android/widgets/download_service_picker.dart';
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
class PlaylistScreen extends ConsumerStatefulWidget {
@@ -308,7 +308,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
const SizedBox(width: 12),
_buildDownloadAllCenterButton(context),
const SizedBox(width: 12),
_buildShufflePlayButton(),
_buildAddToPlaylistButton(context),
],
),
],
@@ -505,26 +505,16 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
);
}
Widget _buildShufflePlayButton() {
Widget _buildAddToPlaylistButton(BuildContext context) {
return _buildCircleButton(
icon: Icons.shuffle_rounded,
tooltip: 'Shuffle Play',
onPressed: _tracks.isEmpty ? null : _shufflePlayLocal,
icon: Icons.playlist_add,
tooltip: 'Add to Playlist',
onPressed: _tracks.isEmpty
? null
: () => showAddTracksToPlaylistSheet(context, ref, _tracks),
);
}
void _shufflePlayLocal() {
if (_tracks.isEmpty) return;
final shuffled = [..._tracks]..shuffle();
final messenger = ScaffoldMessenger.of(context);
ref.read(playbackProvider.notifier).playTrackList(shuffled).catchError((e) {
if (!mounted) return;
messenger.showSnackBar(
SnackBar(content: Text('Cannot shuffle play local tracks: $e')),
);
});
}
void _confirmDownloadAll(BuildContext context) {
if (_tracks.isEmpty) return;
showDialog(
@@ -717,7 +707,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
),
if (isInLocalLibrary) ...[
if (isInLocalLibrary || isInHistory) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
+1 -165
View File
@@ -31,11 +31,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
bool _isLoading = false;
int _androidSdkVersion = 0;
// Mode selection
String _selectedMode = 'downloader';
// We add 1 for the Welcome step
int get _totalSteps => (_androidSdkVersion >= 33 ? 4 : 3) + 1;
int get _totalSteps => _androidSdkVersion >= 33 ? 4 : 3;
@override
void initState() {
@@ -466,8 +462,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
return _notificationPermissionGranted;
case 2:
return _selectedDirectory != null;
case 3:
return true; // Mode selection always has a default
}
} else {
switch (logicStep) {
@@ -475,8 +469,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
return _storagePermissionGranted;
case 1:
return _selectedDirectory != null;
case 2:
return true; // Mode selection always has a default
}
}
return false;
@@ -553,7 +545,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
if (_androidSdkVersion >= 33)
_buildNotificationStep(colorScheme),
_buildDirectoryStep(colorScheme),
_buildModeSelectionStep(colorScheme),
],
),
),
@@ -747,38 +738,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
),
);
}
Widget _buildModeSelectionStep(ColorScheme colorScheme) {
return _StepLayout(
title: context.l10n.setupModeSelectionTitle,
description: context.l10n.setupModeSelectionDescription,
icon: Icons.tune,
child: Column(
children: [
_ModeCard(
icon: Icons.download,
title: context.l10n.setupModeDownloaderTitle,
features: [
context.l10n.setupModeDownloaderFeature1,
context.l10n.setupModeDownloaderFeature2,
context.l10n.setupModeDownloaderFeature3,
],
isSelected: _selectedMode == 'downloader',
onTap: () => setState(() => _selectedMode = 'downloader'),
colorScheme: colorScheme,
),
const SizedBox(height: 16),
Text(
context.l10n.setupModeChangeableLater,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
class _StepLayout extends StatelessWidget {
@@ -888,126 +847,3 @@ class _SuccessCard extends StatelessWidget {
);
}
}
class _ModeCard extends StatelessWidget {
final IconData icon;
final String title;
final List<String> features;
final bool isSelected;
final VoidCallback onTap;
final ColorScheme colorScheme;
const _ModeCard({
required this.icon,
required this.title,
required this.features,
required this.isSelected,
required this.onTap,
required this.colorScheme,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? colorScheme.primary
: colorScheme.outlineVariant,
width: isSelected ? 2 : 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 22,
color: isSelected
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
size: 22,
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurface,
),
),
),
],
),
const SizedBox(height: 8),
...features.map(
(feature) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'\u2022 ',
style: TextStyle(
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
),
),
Expanded(
child: Text(
feature,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
height: 1.4,
),
),
),
],
),
),
),
],
),
),
],
),
),
);
}
}