feat: multi-select batch delete and album grouping in history

- Add multi-select mode with long-press to select tracks
- Add bottom action bar for selection (Material 3 style)
- Add filter tabs: All/Albums/Singles
- Add album grouping view when Albums filter selected
- Add DownloadedAlbumScreen for viewing tracks in an album
- Reactive UI updates when tracks deleted
- Auto-pop when album has <2 tracks
- Update issue templates with (Stable Version) text
- Bump version to 2.2.8
This commit is contained in:
zarzet
2026-01-12 06:18:32 +07:00
parent a6d488696b
commit c673581c32
12 changed files with 1598 additions and 417 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ body:
options:
- label: I have searched existing issues and this bug hasn't been reported yet
required: true
- label: I am using the latest version of SpotiFLAC
- label: I am using the latest version of SpotiFLAC (Stable Version)
required: true
- type: textarea
+1 -1
View File
@@ -16,7 +16,7 @@ body:
options:
- label: I have tried downloading with a different service (Tidal/Qobuz/Amazon)
required: true
- label: I am using the latest version of SpotiFLAC
- label: I am using the latest version of SpotiFLAC (Stable Version)
required: true
- type: dropdown
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## [2.2.8] - 2026-01-12
### Added
- **Multi-Select Batch Delete**: Long-press tracks in History to enter selection mode
- Select multiple tracks at once
- "Select All" and "Delete Selected" actions
- Modern Material 3 bottom action bar (slides up from bottom)
- Works in both grid and list view modes
- **History Filter Tabs**: Filter history by All/Albums/Singles
- Album = tracks where album has >1 track in history
- Single = tracks where album has only 1 track in history
- Filter chips show counts for each category
- **Album Grouping View**: When "Albums" filter is selected, tracks are grouped by album
- Album cards displayed in 2-column grid with cover art and track count badge
- Tap album to open dedicated album detail screen
- Album detail shows all downloaded tracks from that album
- Multi-select delete support within album view
- Auto-navigates back when album has <2 tracks remaining
### Changed
- **Issue Templates**: Updated version confirmation checkbox to specify "(Stable Version)"
## [2.2.7] - 2026-01-11
### Added
+2 -2
View File
@@ -1,8 +1,8 @@
/// App version and info constants
/// Update version here only - all other files will reference this
class AppInfo {
static const String version = '2.2.7';
static const String buildNumber = '49';
static const String version = '2.2.8';
static const String buildNumber = '50';
static const String fullVersion = '$version+$buildNumber';
+4
View File
@@ -18,6 +18,7 @@ class AppSettings {
final bool hasSearchedBefore; // Hide helper text after first search
final String folderOrganization; // none, artist, album, artist_album
final String historyViewMode; // list, grid
final String historyFilterMode; // all, albums, singles
final bool askQualityBeforeDownload; // Show quality picker before each download
final String spotifyClientId; // Custom Spotify client ID (empty = use default)
final String spotifyClientSecret; // Custom Spotify client secret (empty = use default)
@@ -40,6 +41,7 @@ class AppSettings {
this.hasSearchedBefore = false, // Default: show helper text
this.folderOrganization = 'none', // Default: no folder organization
this.historyViewMode = 'grid', // Default: grid view
this.historyFilterMode = 'all', // Default: show all
this.askQualityBeforeDownload = true, // Default: ask quality before download
this.spotifyClientId = '', // Default: use built-in credentials
this.spotifyClientSecret = '', // Default: use built-in credentials
@@ -63,6 +65,7 @@ class AppSettings {
bool? hasSearchedBefore,
String? folderOrganization,
String? historyViewMode,
String? historyFilterMode,
bool? askQualityBeforeDownload,
String? spotifyClientId,
String? spotifyClientSecret,
@@ -85,6 +88,7 @@ class AppSettings {
hasSearchedBefore: hasSearchedBefore ?? this.hasSearchedBefore,
folderOrganization: folderOrganization ?? this.folderOrganization,
historyViewMode: historyViewMode ?? this.historyViewMode,
historyFilterMode: historyFilterMode ?? this.historyFilterMode,
askQualityBeforeDownload: askQualityBeforeDownload ?? this.askQualityBeforeDownload,
spotifyClientId: spotifyClientId ?? this.spotifyClientId,
spotifyClientSecret: spotifyClientSecret ?? this.spotifyClientSecret,
+2
View File
@@ -21,6 +21,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
hasSearchedBefore: json['hasSearchedBefore'] as bool? ?? false,
folderOrganization: json['folderOrganization'] as String? ?? 'none',
historyViewMode: json['historyViewMode'] as String? ?? 'grid',
historyFilterMode: json['historyFilterMode'] as String? ?? 'all',
askQualityBeforeDownload: json['askQualityBeforeDownload'] as bool? ?? true,
spotifyClientId: json['spotifyClientId'] as String? ?? '',
spotifyClientSecret: json['spotifyClientSecret'] as String? ?? '',
@@ -46,6 +47,7 @@ Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
'hasSearchedBefore': instance.hasSearchedBefore,
'folderOrganization': instance.folderOrganization,
'historyViewMode': instance.historyViewMode,
'historyFilterMode': instance.historyFilterMode,
'askQualityBeforeDownload': instance.askQualityBeforeDownload,
'spotifyClientId': instance.spotifyClientId,
'spotifyClientSecret': instance.spotifyClientSecret,
+5
View File
@@ -148,6 +148,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
void setHistoryFilterMode(String mode) {
state = state.copyWith(historyFilterMode: mode);
_saveSettings();
}
void setAskQualityBeforeDownload(bool enabled) {
state = state.copyWith(askQualityBeforeDownload: enabled);
_saveSettings();
+573
View File
@@ -0,0 +1,573 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:open_filex/open_filex.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
/// Screen to display downloaded tracks from a specific album
class DownloadedAlbumScreen extends ConsumerStatefulWidget {
final String albumName;
final String artistName;
final String? coverUrl;
const DownloadedAlbumScreen({
super.key,
required this.albumName,
required this.artistName,
this.coverUrl,
});
@override
ConsumerState<DownloadedAlbumScreen> createState() => _DownloadedAlbumScreenState();
}
class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
// Multi-select state
bool _isSelectionMode = false;
final Set<String> _selectedIds = {};
/// Get tracks for this album from history provider (reactive)
List<DownloadHistoryItem> _getAlbumTracks(List<DownloadHistoryItem> allItems) {
return allItems.where((item) {
final itemKey = '${item.albumName}|${item.albumArtist ?? item.artistName}';
final albumKey = '${widget.albumName}|${widget.artistName}';
return itemKey == albumKey;
}).toList()
..sort((a, b) {
final aNum = a.trackNumber ?? 999;
final bNum = b.trackNumber ?? 999;
if (aNum != bNum) return aNum.compareTo(bNum);
return a.trackName.compareTo(b.trackName);
});
}
void _enterSelectionMode(String itemId) {
HapticFeedback.mediumImpact();
setState(() {
_isSelectionMode = true;
_selectedIds.add(itemId);
});
}
void _exitSelectionMode() {
setState(() {
_isSelectionMode = false;
_selectedIds.clear();
});
}
void _toggleSelection(String itemId) {
setState(() {
if (_selectedIds.contains(itemId)) {
_selectedIds.remove(itemId);
if (_selectedIds.isEmpty) {
_isSelectionMode = false;
}
} else {
_selectedIds.add(itemId);
}
});
}
void _selectAll(List<DownloadHistoryItem> tracks) {
setState(() {
_selectedIds.addAll(tracks.map((e) => e.id));
});
}
Future<void> _deleteSelected(List<DownloadHistoryItem> currentTracks) async {
final count = _selectedIds.length;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Selected'),
content: Text('Delete $count ${count == 1 ? 'track' : 'tracks'} from this album?\n\nThis will also delete the files from storage.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Delete'),
),
],
),
);
if (confirmed == true && mounted) {
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
final idsToDelete = _selectedIds.toList();
int deletedCount = 0;
for (final id in idsToDelete) {
final item = currentTracks.where((e) => e.id == id).firstOrNull;
if (item != null) {
try {
final file = File(item.filePath);
if (await file.exists()) {
await file.delete();
}
} catch (_) {}
historyNotifier.removeFromHistory(id);
deletedCount++;
}
}
_exitSelectionMode();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}')),
);
}
}
}
Future<void> _openFile(String filePath) async {
try {
await OpenFilex.open(filePath);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cannot open file: $e')),
);
}
}
}
void _navigateToMetadataScreen(DownloadHistoryItem item) {
Navigator.push(context, PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 250),
pageBuilder: (context, animation, secondaryAnimation) => TrackMetadataScreen(item: item),
transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child),
));
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final bottomPadding = MediaQuery.of(context).padding.bottom;
// Watch history and get tracks for this album (reactive!)
final allHistoryItems = ref.watch(downloadHistoryProvider.select((s) => s.items));
final tracks = _getAlbumTracks(allHistoryItems);
// Auto-pop if album has less than 2 tracks (no longer an "album")
if (tracks.length < 2) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) Navigator.pop(context);
});
return const SizedBox.shrink();
}
// Clean up selected IDs that no longer exist
final validIds = tracks.map((t) => t.id).toSet();
_selectedIds.removeWhere((id) => !validIds.contains(id));
if (_selectedIds.isEmpty && _isSelectionMode) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() => _isSelectionMode = false);
});
}
return PopScope(
canPop: !_isSelectionMode,
onPopInvokedWithResult: (didPop, result) {
if (!didPop && _isSelectionMode) {
_exitSelectionMode();
}
},
child: Scaffold(
body: Stack(
children: [
CustomScrollView(
slivers: [
_buildAppBar(context, colorScheme),
_buildInfoCard(context, colorScheme, tracks),
_buildTrackListHeader(context, colorScheme, tracks),
_buildTrackList(context, colorScheme, tracks),
SliverToBoxAdapter(child: SizedBox(height: _isSelectionMode ? 120 : 32)),
],
),
// Bottom Selection Action Bar
AnimatedPositioned(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
left: 0,
right: 0,
bottom: _isSelectionMode ? 0 : -(200 + bottomPadding),
child: _buildSelectionBottomBar(context, colorScheme, tracks, bottomPadding),
),
],
),
),
);
}
Widget _buildAppBar(BuildContext context, ColorScheme colorScheme) {
return SliverAppBar(
expandedHeight: 280,
pinned: true,
stretch: true,
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
if (widget.coverUrl != null)
CachedNetworkImage(
imageUrl: widget.coverUrl!,
fit: BoxFit.cover,
color: Colors.black.withValues(alpha: 0.5),
colorBlendMode: BlendMode.darken,
memCacheWidth: 600,
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
colorScheme.surface.withValues(alpha: 0.8),
colorScheme.surface,
],
stops: const [0.0, 0.7, 1.0],
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Container(
width: 140,
height: 140,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: widget.coverUrl != null
? CachedNetworkImage(imageUrl: widget.coverUrl!, fit: BoxFit.cover, memCacheWidth: 280)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(Icons.album, size: 48, color: colorScheme.onSurfaceVariant),
),
),
),
),
),
],
),
stretchModes: const [StretchMode.zoomBackground, StretchMode.blurBackground],
),
leading: IconButton(
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: colorScheme.surface.withValues(alpha: 0.8), shape: BoxShape.circle),
child: Icon(Icons.arrow_back, color: colorScheme.onSurface),
),
onPressed: () => Navigator.pop(context),
),
);
}
Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme, List<DownloadHistoryItem> tracks) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.albumName,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: colorScheme.onSurface),
),
const SizedBox(height: 4),
Text(
widget.artistName,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: colorScheme.onSurfaceVariant),
),
const SizedBox(height: 12),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(20)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download_done, size: 14, color: colorScheme.onPrimaryContainer),
const SizedBox(width: 4),
Text('${tracks.length} downloaded', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
],
),
),
const SizedBox(width: 8),
if (_getCommonQuality(tracks) != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getCommonQuality(tracks)!.startsWith('24')
? colorScheme.tertiaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: Text(
_getCommonQuality(tracks)!,
style: TextStyle(
color: _getCommonQuality(tracks)!.startsWith('24')
? colorScheme.onTertiaryContainer
: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
],
),
),
),
),
);
}
String? _getCommonQuality(List<DownloadHistoryItem> tracks) {
if (tracks.isEmpty) return null;
final firstQuality = tracks.first.quality;
if (firstQuality == null) return null;
for (final track in tracks) {
if (track.quality != firstQuality) return null;
}
return firstQuality;
}
Widget _buildTrackListHeader(BuildContext context, ColorScheme colorScheme, List<DownloadHistoryItem> tracks) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
child: Row(
children: [
Icon(Icons.queue_music, size: 20, color: colorScheme.primary),
const SizedBox(width: 8),
Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
const Spacer(),
if (!_isSelectionMode)
TextButton.icon(
onPressed: tracks.isNotEmpty ? () => _enterSelectionMode(tracks.first.id) : null,
icon: const Icon(Icons.checklist, size: 18),
label: const Text('Select'),
style: TextButton.styleFrom(visualDensity: VisualDensity.compact),
),
],
),
),
);
}
Widget _buildTrackList(BuildContext context, ColorScheme colorScheme, List<DownloadHistoryItem> tracks) {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final track = tracks[index];
return KeyedSubtree(
key: ValueKey(track.id),
child: _buildTrackItem(context, colorScheme, track),
);
},
childCount: tracks.length,
),
);
}
Widget _buildTrackItem(BuildContext context, ColorScheme colorScheme, DownloadHistoryItem track) {
final isSelected = _selectedIds.contains(track.id);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card(
elevation: 0,
color: isSelected ? colorScheme.primaryContainer.withValues(alpha: 0.3) : Colors.transparent,
margin: const EdgeInsets.symmetric(vertical: 2),
child: ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: _isSelectionMode
? () => _toggleSelection(track.id)
: () => _navigateToMetadataScreen(track),
onLongPress: _isSelectionMode ? null : () => _enterSelectionMode(track.id),
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isSelectionMode) ...[
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: isSelected ? colorScheme.primary : Colors.transparent,
shape: BoxShape.circle,
border: Border.all(color: isSelected ? colorScheme.primary : colorScheme.outline, width: 2),
),
child: isSelected
? Icon(Icons.check, color: colorScheme.onPrimary, size: 16)
: null,
),
const SizedBox(width: 12),
],
SizedBox(
width: 24,
child: Text(
track.trackNumber?.toString() ?? '-',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
title: Text(
track.trackName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
),
subtitle: Text(
track.artistName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
trailing: _isSelectionMode ? null : IconButton(
onPressed: () => _openFile(track.filePath),
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
style: IconButton.styleFrom(
backgroundColor: colorScheme.primaryContainer.withValues(alpha: 0.3),
),
),
),
),
);
}
Widget _buildSelectionBottomBar(BuildContext context, ColorScheme colorScheme, List<DownloadHistoryItem> tracks, double bottomPadding) {
final selectedCount = _selectedIds.length;
final allSelected = selectedCount == tracks.length && tracks.isNotEmpty;
return Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 12,
offset: const Offset(0, -4),
),
],
),
child: SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, bottomPadding > 0 ? 8 : 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
Row(
children: [
IconButton.filledTonal(
onPressed: _exitSelectionMode,
icon: const Icon(Icons.close),
style: IconButton.styleFrom(
backgroundColor: colorScheme.surfaceContainerHighest,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$selectedCount selected',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
Text(
allSelected ? 'All tracks selected' : 'Tap tracks to select',
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant),
),
],
),
),
TextButton.icon(
onPressed: () {
if (allSelected) {
_exitSelectionMode();
} else {
_selectAll(tracks);
}
},
icon: Icon(allSelected ? Icons.deselect : Icons.select_all, size: 20),
label: Text(allSelected ? 'Deselect' : 'Select All'),
style: TextButton.styleFrom(foregroundColor: colorScheme.primary),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: selectedCount > 0 ? () => _deleteSelected(tracks) : null,
icon: const Icon(Icons.delete_outline),
label: Text(
selectedCount > 0
? 'Delete $selectedCount ${selectedCount == 1 ? 'track' : 'tracks'}'
: 'Select tracks to delete',
),
style: FilledButton.styleFrom(
backgroundColor: selectedCount > 0 ? colorScheme.error : colorScheme.surfaceContainerHighest,
foregroundColor: selectedCount > 0 ? colorScheme.onError : colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
),
),
],
),
),
),
);
}
}
+978 -398
View File
File diff suppressed because it is too large Load Diff
@@ -359,8 +359,9 @@ class DownloadSettingsPage extends ConsumerWidget {
} else {
// Android: Use file picker
final result = await FilePicker.platform.getDirectoryPath();
if (result != null)
if (result != null) {
ref.read(settingsProvider.notifier).setDownloadDirectory(result);
}
}
}
@@ -512,9 +513,7 @@ class DownloadSettingsPage extends ConsumerWidget {
example: 'SpotiFLAC/Track.flac',
isSelected: current == 'none',
onTap: () {
ref
.read(settingsProvider.notifier)
.setFolderOrganization('none');
ref.read(settingsProvider.notifier).setFolderOrganization('none');
Navigator.pop(context);
},
),
@@ -524,9 +523,7 @@ class DownloadSettingsPage extends ConsumerWidget {
example: 'SpotiFLAC/Artist Name/Track.flac',
isSelected: current == 'artist',
onTap: () {
ref
.read(settingsProvider.notifier)
.setFolderOrganization('artist');
ref.read(settingsProvider.notifier).setFolderOrganization('artist');
Navigator.pop(context);
},
),
@@ -536,9 +533,7 @@ class DownloadSettingsPage extends ConsumerWidget {
example: 'SpotiFLAC/Album Name/Track.flac',
isSelected: current == 'album',
onTap: () {
ref
.read(settingsProvider.notifier)
.setFolderOrganization('album');
ref.read(settingsProvider.notifier).setFolderOrganization('album');
Navigator.pop(context);
},
),
@@ -548,9 +543,7 @@ class DownloadSettingsPage extends ConsumerWidget {
example: 'SpotiFLAC/Artist/Album/Track.flac',
isSelected: current == 'artist_album',
onTap: () {
ref
.read(settingsProvider.notifier)
.setFolderOrganization('artist_album');
ref.read(settingsProvider.notifier).setFolderOrganization('artist_album');
Navigator.pop(context);
},
),
+1 -1
View File
@@ -1,7 +1,7 @@
name: spotiflac_android
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
publish_to: "none"
version: 2.2.7+49
version: 2.2.8+50
environment:
sdk: ^3.10.0
+1 -1
View File
@@ -1,7 +1,7 @@
name: spotiflac_android
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
publish_to: "none"
version: 2.2.7+49
version: 2.2.8+50
environment:
sdk: ^3.10.0