diff --git a/.gitignore b/.gitignore index 3b3b6361..d0594280 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ ios/Pods/ ios/.symlinks/ ios/Flutter/Flutter.framework/ ios/Flutter/Flutter.podspec +android/app/libs/gobackend-sources.jar diff --git a/CHANGELOG.md b/CHANGELOG.md index eebd49df..8f384dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [2.0.7-preview2] - 2026-01-06 + +### Fixed +- **iOS Directory Picker**: Fixed unable to select download folder on iOS + - iOS limitation: Empty folders cannot be selected via document picker + - Added "App Documents Folder" option as recommended default + - Shows info message explaining iOS limitation + - Files saved to app Documents folder are accessible via iOS Files app + +## [2.0.7-preview] - 2026-01-05 + +### Changed +- **Reduced APK Size**: Replaced FFmpeg plugin with custom AAR containing only required codecs + - arm64 APK: 46.6 MB (previously 51 MB) + - arm32 APK: 59 MB (previously 64 MB) + - Only includes FLAC, MP3 (LAME), and AAC codecs + - Removed x86/x86_64 architectures (emulator only) + +### Technical +- Custom FFmpeg AAR with arm64-v8a and armeabi-v7a only +- Native MethodChannel bridge for FFmpeg operations +- Separate iOS build configuration with ffmpeg_kit_flutter plugin + ## [2.0.6] - 2026-01-05 ### Fixed diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index a54a3600..7b94baa0 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,7 +1,7 @@ /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '2.0.7-preview'; + static const String version = '2.0.7-preview2'; static const String buildNumber = '37'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 4630139d..05848ddb 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1,6 +1,8 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -113,8 +115,10 @@ class DownloadSettingsPage extends ConsumerWidget { SettingsItem( icon: Icons.folder_outlined, title: 'Download Directory', - subtitle: settings.downloadDirectory.isEmpty ? 'Music/SpotiFLAC' : settings.downloadDirectory, - onTap: () => _pickDirectory(ref), + subtitle: settings.downloadDirectory.isEmpty + ? (Platform.isIOS ? 'App Documents Folder' : 'Music/SpotiFLAC') + : settings.downloadDirectory, + onTap: () => _pickDirectory(context, ref), ), SettingsItem( icon: Icons.create_new_folder_outlined, @@ -161,9 +165,90 @@ class DownloadSettingsPage extends ConsumerWidget { ); } - Future _pickDirectory(WidgetRef ref) async { - final result = await FilePicker.platform.getDirectoryPath(); - if (result != null) ref.read(settingsProvider.notifier).setDownloadDirectory(result); + Future _pickDirectory(BuildContext context, WidgetRef ref) async { + if (Platform.isIOS) { + // iOS: Show options dialog + _showIOSDirectoryOptions(context, ref); + } else { + // Android: Use file picker + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) ref.read(settingsProvider.notifier).setDownloadDirectory(result); + } + } + + void _showIOSDirectoryOptions(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28))), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text('Download Location', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ListTile( + leading: Icon(Icons.folder_special, color: colorScheme.primary), + title: const Text('App Documents Folder'), + subtitle: const Text('Recommended - accessible via Files app'), + trailing: Icon(Icons.check_circle, color: colorScheme.primary), + onTap: () async { + final dir = await getApplicationDocumentsDirectory(); + ref.read(settingsProvider.notifier).setDownloadDirectory(dir.path); + if (ctx.mounted) Navigator.pop(ctx); + }, + ), + ListTile( + leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), + title: const Text('Choose from Files'), + subtitle: const Text('Select iCloud or other location'), + onTap: () async { + Navigator.pop(ctx); + // Note: iOS requires folder to have at least one file to be selectable + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) { + ref.read(settingsProvider.notifier).setDownloadDirectory(result); + } + }, + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.tertiaryContainer.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 20, color: colorScheme.tertiary), + const SizedBox(width: 12), + Expanded( + child: Text( + 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ); } String _getFolderOrganizationLabel(String value) { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index dccc0374..584efbd5 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -205,29 +205,35 @@ class _SetupScreenState extends ConsumerState { setState(() => _isLoading = true); try { - String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: 'Select Download Folder', - ); - - if (selectedDirectory != null) { - setState(() => _selectedDirectory = selectedDirectory); + if (Platform.isIOS) { + // iOS: Show options dialog + await _showIOSDirectoryOptions(); } else { - final defaultDir = await _getDefaultDirectory(); - if (mounted) { - final useDefault = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Use Default Folder?'), - content: Text('No folder selected. Would you like to use the default Music folder?\n\n$defaultDir'), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), - TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Use Default')), - ], - ), - ); + // Android: Use file picker + String? selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: 'Select Download Folder', + ); - if (useDefault == true) { - setState(() => _selectedDirectory = defaultDir); + if (selectedDirectory != null) { + setState(() => _selectedDirectory = selectedDirectory); + } else { + final defaultDir = await _getDefaultDirectory(); + if (mounted) { + final useDefault = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Use Default Folder?'), + content: Text('No folder selected. Would you like to use the default Music folder?\n\n$defaultDir'), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Use Default')), + ], + ), + ); + + if (useDefault == true) { + setState(() => _selectedDirectory = defaultDir); + } } } } @@ -236,6 +242,82 @@ class _SetupScreenState extends ConsumerState { } } + Future _showIOSDirectoryOptions() async { + final colorScheme = Theme.of(context).colorScheme; + + await showModalBottomSheet( + context: context, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28))), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text('Download Location', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ListTile( + leading: Icon(Icons.folder_special, color: colorScheme.primary), + title: const Text('App Documents Folder'), + subtitle: const Text('Recommended - accessible via Files app'), + trailing: Icon(Icons.check_circle, color: colorScheme.primary), + onTap: () async { + final dir = await _getDefaultDirectory(); + setState(() => _selectedDirectory = dir); + if (ctx.mounted) Navigator.pop(ctx); + }, + ), + ListTile( + leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), + title: const Text('Choose from Files'), + subtitle: const Text('Select iCloud or other location'), + onTap: () async { + Navigator.pop(ctx); + // Note: iOS requires folder to have at least one file to be selectable + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) { + setState(() => _selectedDirectory = result); + } + }, + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.tertiaryContainer.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 20, color: colorScheme.tertiary), + const SizedBox(width: 12), + Expanded( + child: Text( + 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } + Future _getDefaultDirectory() async { if (Platform.isIOS) { final appDir = await getApplicationDocumentsDirectory(); diff --git a/pubspec.yaml b/pubspec.yaml index 1b2de416..46b1529e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: 'none' -version: 2.0.7+37 +version: 2.0.7-preview2+38 environment: sdk: ^3.10.0 diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index e68b3bd4..0706750f 100644 --- a/pubspec_ios.yaml +++ b/pubspec_ios.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: 'none' -version: 2.0.7+37 +version: 2.0.7-preview2+38 environment: sdk: ^3.10.0