From 39ddb7a14f30fae1fa44b67f5fc9e522f10dfd53 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 2 Jan 2026 17:33:31 +0700 Subject: [PATCH] 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) --- CHANGELOG.md | 10 ++- ...otlin-compiler-11752605725870772170.salive | 0 android/app/src/main/AndroidManifest.xml | 3 +- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 7 ++ lib/constants/app_info.dart | 4 +- lib/providers/download_queue_provider.dart | 87 ++++++++++++++++++- lib/screens/track_metadata_screen.dart | 8 +- pubspec.lock | 8 +- pubspec.yaml | 4 +- 9 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 android/.kotlin/sessions/kotlin-compiler-11752605725870772170.salive diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c556b..ef11469 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/android/.kotlin/sessions/kotlin-compiler-11752605725870772170.salive b/android/.kotlin/sessions/kotlin-compiler-11752605725870772170.salive new file mode 100644 index 0000000..e69de29 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a698d75..05b1f2b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,8 +23,7 @@ { 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 _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 jsonList = jsonDecode(jsonStr); + final items = jsonList.map((e) => DownloadItem.fromJson(e as Map)).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 _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 { ); 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 { }).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 { }).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 { ).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 { 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 { 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 diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 6012c29..5fdec02 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -938,11 +938,9 @@ class _TrackMetadataScreenState extends ConsumerState { 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}', ); } diff --git a/pubspec.lock b/pubspec.lock index ee61251..85542da 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 520346c..705fe58 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: 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