mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-05 12:18:02 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb05353b7e | |||
| 7ac92d77e5 |
+2
-4
@@ -13,9 +13,6 @@ Thumbs.db
|
|||||||
# Reference folder (development only)
|
# Reference folder (development only)
|
||||||
referensi/
|
referensi/
|
||||||
|
|
||||||
# Development notes
|
|
||||||
COMPARISON_PC_vs_ANDROID.md
|
|
||||||
|
|
||||||
# Old spotiflac_android folder (moved to root)
|
# Old spotiflac_android folder (moved to root)
|
||||||
spotiflac_android/
|
spotiflac_android/
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ go_backend/*.xcframework/
|
|||||||
|
|
||||||
# Android
|
# Android
|
||||||
android/.gradle/
|
android/.gradle/
|
||||||
android/app/libs/
|
android/app/libs/gobackend.aar
|
||||||
android/local.properties
|
android/local.properties
|
||||||
android/*.iml
|
android/*.iml
|
||||||
android/key.properties
|
android/key.properties
|
||||||
@@ -52,3 +49,4 @@ ios/Pods/
|
|||||||
ios/.symlinks/
|
ios/.symlinks/
|
||||||
ios/Flutter/Flutter.framework/
|
ios/Flutter/Flutter.framework/
|
||||||
ios/Flutter/Flutter.podspec
|
ios/Flutter/Flutter.podspec
|
||||||
|
android/app/libs/gobackend-sources.jar
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# 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
|
## [2.0.6] - 2026-01-05
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
/// App version and info constants
|
/// App version and info constants
|
||||||
/// Update version here only - all other files will reference this
|
/// Update version here only - all other files will reference this
|
||||||
class AppInfo {
|
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 buildNumber = '37';
|
||||||
static const String fullVersion = '$version+$buildNumber';
|
static const String fullVersion = '$version+$buildNumber';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:file_picker/file_picker.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/providers/settings_provider.dart';
|
||||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||||
|
|
||||||
@@ -113,8 +115,10 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||||||
SettingsItem(
|
SettingsItem(
|
||||||
icon: Icons.folder_outlined,
|
icon: Icons.folder_outlined,
|
||||||
title: 'Download Directory',
|
title: 'Download Directory',
|
||||||
subtitle: settings.downloadDirectory.isEmpty ? 'Music/SpotiFLAC' : settings.downloadDirectory,
|
subtitle: settings.downloadDirectory.isEmpty
|
||||||
onTap: () => _pickDirectory(ref),
|
? (Platform.isIOS ? 'App Documents Folder' : 'Music/SpotiFLAC')
|
||||||
|
: settings.downloadDirectory,
|
||||||
|
onTap: () => _pickDirectory(context, ref),
|
||||||
),
|
),
|
||||||
SettingsItem(
|
SettingsItem(
|
||||||
icon: Icons.create_new_folder_outlined,
|
icon: Icons.create_new_folder_outlined,
|
||||||
@@ -161,9 +165,90 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickDirectory(WidgetRef ref) async {
|
Future<void> _pickDirectory(BuildContext context, WidgetRef ref) async {
|
||||||
final result = await FilePicker.platform.getDirectoryPath();
|
if (Platform.isIOS) {
|
||||||
if (result != null) ref.read(settingsProvider.notifier).setDownloadDirectory(result);
|
// 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) {
|
String _getFolderOrganizationLabel(String value) {
|
||||||
|
|||||||
+103
-21
@@ -205,29 +205,35 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
|
if (Platform.isIOS) {
|
||||||
dialogTitle: 'Select Download Folder',
|
// iOS: Show options dialog
|
||||||
);
|
await _showIOSDirectoryOptions();
|
||||||
|
|
||||||
if (selectedDirectory != null) {
|
|
||||||
setState(() => _selectedDirectory = selectedDirectory);
|
|
||||||
} else {
|
} else {
|
||||||
final defaultDir = await _getDefaultDirectory();
|
// Android: Use file picker
|
||||||
if (mounted) {
|
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
|
||||||
final useDefault = await showDialog<bool>(
|
dialogTitle: 'Select Download Folder',
|
||||||
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) {
|
if (selectedDirectory != null) {
|
||||||
setState(() => _selectedDirectory = defaultDir);
|
setState(() => _selectedDirectory = selectedDirectory);
|
||||||
|
} else {
|
||||||
|
final defaultDir = await _getDefaultDirectory();
|
||||||
|
if (mounted) {
|
||||||
|
final useDefault = await showDialog<bool>(
|
||||||
|
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<SetupScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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<String> _getDefaultDirectory() async {
|
Future<String> _getDefaultDirectory() async {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: spotiflac_android
|
name: spotiflac_android
|
||||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 2.0.7+37
|
version: 2.0.7-preview2+38
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.0
|
sdk: ^3.10.0
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: spotiflac_android
|
name: spotiflac_android
|
||||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 2.0.7+37
|
version: 2.0.7-preview2+38
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.0
|
sdk: ^3.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user