mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-18 14:24:53 +02:00
merge: sync bugfixes from main (permission error, Android 13+ storage)
This commit is contained in:
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## [3.0.0-alpha.3] - Upcoming
|
||||
|
||||
### Added
|
||||
|
||||
- **Separate Singles Folder**: Option to organize downloads into Albums/ and Singles/ folders
|
||||
- Based on `album_type` from Spotify/Deezer metadata
|
||||
- Toggle in Settings > Download > Separate Singles Folder
|
||||
- Singles saved to `{output}/Singles/`, albums to `{output}/Albums/`
|
||||
|
||||
### Performance
|
||||
|
||||
- **Parallel API Calls**: Download URL fetching now uses parallel requests
|
||||
- Tidal: All 8 APIs requested simultaneously, first success wins
|
||||
- Qobuz: Both APIs requested simultaneously, first success wins
|
||||
- Significantly reduces download URL fetch time
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track
|
||||
- Detects existing entries by Spotify ID, Deezer ID, or ISRC
|
||||
- Replaces existing entry and moves to top of list
|
||||
- Auto-deduplicates existing history on app load
|
||||
- **Extension Search Fallback**: Fixed error when extension is disabled but still called for search
|
||||
- Now checks if extension is still enabled before calling custom search
|
||||
- Auto-resets search provider to default if extension was disabled
|
||||
- **Permission Error Message**: Fixed download showing "Song not found" when actually a permission error
|
||||
- Now shows proper message: "Cannot write to folder, check storage permission"
|
||||
- Added `permission` error type detection in backend
|
||||
- **Android 13+ Storage Permission**: Fixed storage permission not working on Android 13+
|
||||
- Android 13+ now requests both `MANAGE_EXTERNAL_STORAGE` and `READ_MEDIA_AUDIO`
|
||||
- `MANAGE_EXTERNAL_STORAGE` opens Settings (system-level, persists across app data clear)
|
||||
- `READ_MEDIA_AUDIO` shows dialog (app-level, resets on app data clear)
|
||||
- Proper permission check before showing "granted" status
|
||||
|
||||
---
|
||||
|
||||
## [3.0.0-alpha.2] - 2026-01-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1015,6 +1015,12 @@ func errorResponse(msg string) (string, error) {
|
||||
strings.Contains(lowerMsg, "try using vpn") ||
|
||||
strings.Contains(lowerMsg, "change dns") {
|
||||
errorType = "isp_blocked"
|
||||
} else if strings.Contains(lowerMsg, "permission") ||
|
||||
strings.Contains(lowerMsg, "operation not permitted") ||
|
||||
strings.Contains(lowerMsg, "access denied") ||
|
||||
strings.Contains(lowerMsg, "failed to create file") ||
|
||||
strings.Contains(lowerMsg, "failed to create directory") {
|
||||
errorType = "permission"
|
||||
} else if strings.Contains(lowerMsg, "not found") ||
|
||||
strings.Contains(lowerMsg, "not available") ||
|
||||
strings.Contains(lowerMsg, "no results") ||
|
||||
|
||||
@@ -19,6 +19,7 @@ enum DownloadErrorType {
|
||||
notFound, // Track not found on any service
|
||||
rateLimit, // Rate limited by service
|
||||
network, // Network/connection error
|
||||
permission, // File/folder permission error
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@@ -88,6 +89,8 @@ class DownloadItem {
|
||||
return 'Rate limit reached, try again later';
|
||||
case DownloadErrorType.network:
|
||||
return 'Connection failed, check your internet';
|
||||
case DownloadErrorType.permission:
|
||||
return 'Cannot write to folder, check storage permission';
|
||||
default:
|
||||
return error ?? 'An error occurred';
|
||||
}
|
||||
|
||||
@@ -51,4 +51,5 @@ const _$DownloadErrorTypeEnumMap = {
|
||||
DownloadErrorType.notFound: 'notFound',
|
||||
DownloadErrorType.rateLimit: 'rateLimit',
|
||||
DownloadErrorType.network: 'network',
|
||||
DownloadErrorType.permission: 'permission',
|
||||
};
|
||||
|
||||
@@ -1792,6 +1792,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
case 'network':
|
||||
errorType = DownloadErrorType.network;
|
||||
break;
|
||||
case 'permission':
|
||||
errorType = DownloadErrorType.permission;
|
||||
break;
|
||||
default:
|
||||
errorType = DownloadErrorType.unknown;
|
||||
}
|
||||
|
||||
@@ -66,24 +66,38 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
// Check storage permission
|
||||
PermissionStatus storageStatus;
|
||||
bool storageGranted = false;
|
||||
|
||||
if (_androidSdkVersion >= 33) {
|
||||
storageStatus = await Permission.audio.status;
|
||||
// Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO
|
||||
final manageStatus = await Permission.manageExternalStorage.status;
|
||||
final audioStatus = await Permission.audio.status;
|
||||
debugPrint('[Permission] Android 13+ check: MANAGE_EXTERNAL_STORAGE=$manageStatus, READ_MEDIA_AUDIO=$audioStatus');
|
||||
storageGranted = manageStatus.isGranted && audioStatus.isGranted;
|
||||
} else if (_androidSdkVersion >= 30) {
|
||||
storageStatus = await Permission.manageExternalStorage.status;
|
||||
// Android 11-12: Need MANAGE_EXTERNAL_STORAGE only
|
||||
final manageStatus = await Permission.manageExternalStorage.status;
|
||||
debugPrint('[Permission] Android 11-12 check: MANAGE_EXTERNAL_STORAGE=$manageStatus');
|
||||
storageGranted = manageStatus.isGranted;
|
||||
} else {
|
||||
storageStatus = await Permission.storage.status;
|
||||
// Android 10 and below: Use legacy storage permission
|
||||
final storageStatus = await Permission.storage.status;
|
||||
debugPrint('[Permission] Android 10- check: STORAGE=$storageStatus');
|
||||
storageGranted = storageStatus.isGranted;
|
||||
}
|
||||
|
||||
debugPrint('[Permission] Final storageGranted=$storageGranted');
|
||||
|
||||
// Check notification permission (Android 13+)
|
||||
PermissionStatus notificationStatus = PermissionStatus.granted;
|
||||
if (_androidSdkVersion >= 33) {
|
||||
notificationStatus = await Permission.notification.status;
|
||||
debugPrint('[Permission] Notification=$notificationStatus');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_storagePermissionGranted = storageStatus.isGranted;
|
||||
_storagePermissionGranted = storageGranted;
|
||||
_notificationPermissionGranted = notificationStatus.isGranted;
|
||||
});
|
||||
}
|
||||
@@ -97,17 +111,57 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
if (Platform.isIOS) {
|
||||
setState(() => _storagePermissionGranted = true);
|
||||
} else if (Platform.isAndroid) {
|
||||
PermissionStatus status;
|
||||
bool allGranted = false;
|
||||
|
||||
if (_androidSdkVersion >= 33) {
|
||||
// Android 13+: Use audio permission
|
||||
status = await Permission.audio.request();
|
||||
// Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO
|
||||
|
||||
// First check/request MANAGE_EXTERNAL_STORAGE
|
||||
var manageStatus = await Permission.manageExternalStorage.status;
|
||||
if (!manageStatus.isGranted) {
|
||||
if (mounted) {
|
||||
final shouldOpen = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Storage Access Required'),
|
||||
content: const Text(
|
||||
'SpotiFLAC needs "All files access" permission to save music files to your chosen folder.\n\n'
|
||||
'Please enable "Allow access to manage all files" in the next screen.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Open Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldOpen == true) {
|
||||
await Permission.manageExternalStorage.request();
|
||||
// Re-check after returning from settings
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
manageStatus = await Permission.manageExternalStorage.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then request READ_MEDIA_AUDIO (this shows a dialog)
|
||||
var audioStatus = await Permission.audio.status;
|
||||
if (!audioStatus.isGranted && manageStatus.isGranted) {
|
||||
audioStatus = await Permission.audio.request();
|
||||
}
|
||||
|
||||
allGranted = manageStatus.isGranted && audioStatus.isGranted;
|
||||
|
||||
} else if (_androidSdkVersion >= 30) {
|
||||
// Android 11-12: Need MANAGE_EXTERNAL_STORAGE
|
||||
// This opens system settings, not a dialog
|
||||
status = await Permission.manageExternalStorage.status;
|
||||
if (!status.isGranted) {
|
||||
// Show explanation dialog first
|
||||
// Android 11-12: Need MANAGE_EXTERNAL_STORAGE only
|
||||
var manageStatus = await Permission.manageExternalStorage.status;
|
||||
if (!manageStatus.isGranted) {
|
||||
if (mounted) {
|
||||
final shouldOpen = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -131,23 +185,33 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
);
|
||||
|
||||
if (shouldOpen == true) {
|
||||
status = await Permission.manageExternalStorage.request();
|
||||
await Permission.manageExternalStorage.request();
|
||||
// Re-check after returning from settings
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
manageStatus = await Permission.manageExternalStorage.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
allGranted = manageStatus.isGranted;
|
||||
|
||||
} else {
|
||||
// Android 10 and below: Use legacy storage permission
|
||||
status = await Permission.storage.request();
|
||||
final status = await Permission.storage.request();
|
||||
allGranted = status.isGranted;
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
_showPermissionDeniedDialog('Storage');
|
||||
setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (status.isGranted) {
|
||||
if (allGranted) {
|
||||
setState(() => _storagePermissionGranted = true);
|
||||
} else if (status.isPermanentlyDenied) {
|
||||
_showPermissionDeniedDialog('Storage');
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Permission denied. Please grant permission to continue.')),
|
||||
const SnackBar(content: Text('Permission denied. Please grant all permissions to continue.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user