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:
zarzet
2026-01-02 17:33:31 +07:00
parent bd9b527161
commit 39ddb7a14f
9 changed files with 114 additions and 17 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

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 = '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';

View File

@@ -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

View File

@@ -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}',
);
}

View File

@@ -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:

View File

@@ -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