mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-04-03 18:32:28 +02:00
fix: persist download queue to survive app restart (v1.6.1)
- Download queue now persisted to SharedPreferences - Auto-restore pending downloads on app restart - Interrupted downloads reset to queued and auto-resumed - singleTask launch mode to prevent app restart on share intent - onNewIntent handler for proper intent handling - Reverted share_plus to 10.1.4 (12.0.1 has Kotlin build issues)
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.1] - 2026-01-02
|
||||
|
||||
### Fixed
|
||||
- **Share Intent App Restart**: Fixed download queue being lost when sharing from Spotify while downloads are in progress
|
||||
- Download queue is now persisted to storage and automatically restored on app restart
|
||||
- Interrupted downloads (marked as "downloading") are reset to "queued" and auto-resumed
|
||||
- Changed launch mode to `singleTask` to reuse existing activity instead of restarting
|
||||
- Added `onNewIntent` handler to properly receive new share intents
|
||||
|
||||
## [1.6.0] - 2026-01-02
|
||||
|
||||
### Added
|
||||
@@ -33,7 +42,6 @@
|
||||
### Improved
|
||||
- **Code Quality**: Replaced all `print()` statements with structured logging using `logger` package
|
||||
- **Dependencies Updated**:
|
||||
- `share_plus`: 10.1.4 → 12.0.1
|
||||
- `flutter_local_notifications`: 18.0.1 → 19.0.0
|
||||
- `build_runner`: 2.4.15 → 2.10.4
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import android.content.Intent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -14,6 +15,12 @@ class MainActivity: FlutterActivity() {
|
||||
private val CHANNEL = "com.zarz.spotiflac/backend"
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
// Update the intent so receive_sharing_intent can access the new data
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
|
||||
@@ -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 = '1.6.0';
|
||||
static const String buildNumber = '25';
|
||||
static const String version = '1.6.1';
|
||||
static const String buildNumber = '26';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
static const String appName = 'SpotiFLAC';
|
||||
|
||||
@@ -242,18 +242,90 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
Timer? _progressTimer;
|
||||
int _downloadCount = 0; // Counter for connection cleanup
|
||||
static const _cleanupInterval = 50; // Cleanup every 50 downloads
|
||||
static const _queueStorageKey = 'download_queue'; // Storage key for queue persistence
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
int _totalQueuedAtStart = 0; // Track total items when queue started
|
||||
bool _isLoaded = false;
|
||||
|
||||
@override
|
||||
DownloadQueueState build() {
|
||||
// Initialize output directory asynchronously
|
||||
// Initialize output directory and load persisted queue asynchronously
|
||||
Future.microtask(() async {
|
||||
await _initOutputDir();
|
||||
await _loadQueueFromStorage();
|
||||
});
|
||||
return const DownloadQueueState();
|
||||
}
|
||||
|
||||
/// Load persisted queue from storage (for app restart recovery)
|
||||
Future<void> _loadQueueFromStorage() async {
|
||||
if (_isLoaded) return;
|
||||
_isLoaded = true;
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString(_queueStorageKey);
|
||||
if (jsonStr != null && jsonStr.isNotEmpty) {
|
||||
final List<dynamic> jsonList = jsonDecode(jsonStr);
|
||||
final items = jsonList.map((e) => DownloadItem.fromJson(e as Map<String, dynamic>)).toList();
|
||||
|
||||
// Reset downloading items to queued (they were interrupted)
|
||||
final restoredItems = items.map((item) {
|
||||
if (item.status == DownloadStatus.downloading) {
|
||||
return item.copyWith(status: DownloadStatus.queued, progress: 0);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
|
||||
// Only restore queued/downloading items (not completed/failed/skipped)
|
||||
final pendingItems = restoredItems.where((item) =>
|
||||
item.status == DownloadStatus.queued
|
||||
).toList();
|
||||
|
||||
if (pendingItems.isNotEmpty) {
|
||||
state = state.copyWith(items: pendingItems);
|
||||
_log.i('Restored ${pendingItems.length} pending items from storage');
|
||||
|
||||
// Auto-resume queue processing
|
||||
Future.microtask(() => _processQueue());
|
||||
} else {
|
||||
_log.d('No pending items to restore');
|
||||
// Clear storage since nothing to restore
|
||||
await prefs.remove(_queueStorageKey);
|
||||
}
|
||||
} else {
|
||||
_log.d('No queue found in storage');
|
||||
}
|
||||
} catch (e) {
|
||||
_log.e('Failed to load queue from storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Save current queue to storage (only pending items)
|
||||
Future<void> _saveQueueToStorage() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Only persist queued and downloading items
|
||||
final pendingItems = state.items.where((item) =>
|
||||
item.status == DownloadStatus.queued ||
|
||||
item.status == DownloadStatus.downloading
|
||||
).toList();
|
||||
|
||||
if (pendingItems.isEmpty) {
|
||||
// Clear storage if no pending items
|
||||
await prefs.remove(_queueStorageKey);
|
||||
_log.d('Cleared queue storage (no pending items)');
|
||||
} else {
|
||||
final jsonList = pendingItems.map((e) => e.toJson()).toList();
|
||||
await prefs.setString(_queueStorageKey, jsonEncode(jsonList));
|
||||
_log.d('Saved ${pendingItems.length} pending items to storage');
|
||||
}
|
||||
} catch (e) {
|
||||
_log.e('Failed to save queue to storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _startProgressPolling(String itemId) {
|
||||
_progressTimer?.cancel();
|
||||
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) async {
|
||||
@@ -461,6 +533,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
|
||||
state = state.copyWith(items: [...state.items, item]);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
|
||||
if (!state.isProcessing) {
|
||||
// Run in microtask to not block UI
|
||||
@@ -487,6 +560,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(items: [...state.items, ...newItems]);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
|
||||
if (!state.isProcessing) {
|
||||
// Run in microtask to not block UI
|
||||
@@ -508,6 +582,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(items: items);
|
||||
|
||||
// Persist queue when status changes to completed/failed/skipped (item removed from pending)
|
||||
if (status == DownloadStatus.completed ||
|
||||
status == DownloadStatus.failed ||
|
||||
status == DownloadStatus.skipped) {
|
||||
_saveQueueToStorage();
|
||||
}
|
||||
}
|
||||
|
||||
void updateProgress(String id, double progress) {
|
||||
@@ -526,10 +607,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
).toList();
|
||||
|
||||
state = state.copyWith(items: items);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
state = state.copyWith(items: [], isPaused: false);
|
||||
_saveQueueToStorage(); // Clear persisted queue
|
||||
}
|
||||
|
||||
/// Pause the download queue
|
||||
@@ -571,6 +654,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return item;
|
||||
}).toList();
|
||||
state = state.copyWith(items: items);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
|
||||
// Start processing if not already
|
||||
if (!state.isProcessing) {
|
||||
@@ -582,6 +666,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
void removeItem(String id) {
|
||||
final items = state.items.where((item) => item.id != id).toList();
|
||||
state = state.copyWith(items: items);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
}
|
||||
|
||||
/// Embed metadata and cover to a FLAC file after M4A conversion
|
||||
|
||||
@@ -938,11 +938,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
files: [XFile(item.filePath)],
|
||||
text: '${item.trackName} - ${item.artistName}',
|
||||
),
|
||||
await Share.shareXFiles(
|
||||
[XFile(item.filePath)],
|
||||
text: '${item.trackName} - ${item.artistName}',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -876,18 +876,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840"
|
||||
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.1"
|
||||
version: "10.1.4"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "5.0.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: spotiflac_android
|
||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
||||
publish_to: 'none'
|
||||
version: 1.6.0+25
|
||||
version: 1.6.1+26
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
@@ -46,7 +46,7 @@ dependencies:
|
||||
# Utils
|
||||
url_launcher: ^6.3.1
|
||||
device_info_plus: ^12.3.0
|
||||
share_plus: ^12.0.1
|
||||
share_plus: ^10.1.4
|
||||
receive_sharing_intent: ^1.8.1
|
||||
logger: ^2.5.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user