diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt index c1c4822c..5d924e0a 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt @@ -35,6 +35,7 @@ class DownloadService : Service() { const val EXTRA_PROGRESS = "progress" const val EXTRA_TOTAL = "total" const val EXTRA_QUEUE_COUNT = "queue_count" + const val EXTRA_STATUS = "status" private var isRunning = false @@ -61,7 +62,7 @@ class DownloadService : Service() { context.startService(intent) } - fun updateProgress(context: Context, trackName: String, artistName: String, progress: Long, total: Long, queueCount: Int) { + fun updateProgress(context: Context, trackName: String, artistName: String, progress: Long, total: Long, queueCount: Int, status: String = "downloading") { val intent = Intent(context, DownloadService::class.java).apply { action = ACTION_UPDATE_PROGRESS putExtra(EXTRA_TRACK_NAME, trackName) @@ -69,6 +70,7 @@ class DownloadService : Service() { putExtra(EXTRA_PROGRESS, progress) putExtra(EXTRA_TOTAL, total) putExtra(EXTRA_QUEUE_COUNT, queueCount) + putExtra(EXTRA_STATUS, status) } context.startService(intent) } @@ -77,6 +79,7 @@ class DownloadService : Service() { private var wakeLock: PowerManager.WakeLock? = null private var currentTrackName = "" private var currentArtistName = "" + private var currentStatus = "preparing" private var queueCount = 0 override fun onCreate() { @@ -89,6 +92,7 @@ class DownloadService : Service() { ACTION_START -> { currentTrackName = intent.getStringExtra(EXTRA_TRACK_NAME) ?: "" currentArtistName = intent.getStringExtra(EXTRA_ARTIST_NAME) ?: "" + currentStatus = "preparing" queueCount = intent.getIntExtra(EXTRA_QUEUE_COUNT, 0) startForegroundService() } @@ -100,6 +104,7 @@ class DownloadService : Service() { currentArtistName = intent.getStringExtra(EXTRA_ARTIST_NAME) ?: currentArtistName val progress = intent.getLongExtra(EXTRA_PROGRESS, 0) val total = intent.getLongExtra(EXTRA_TOTAL, 0) + currentStatus = intent.getStringExtra(EXTRA_STATUS) ?: currentStatus queueCount = intent.getIntExtra(EXTRA_QUEUE_COUNT, queueCount) updateNotification(progress, total) } @@ -186,7 +191,11 @@ class DownloadService : Service() { "Downloading..." } - val text = if (currentArtistName.isNotEmpty() && queueCount <= 1) { + val text = if (currentStatus == "finalizing") { + if (currentArtistName.isNotEmpty()) currentArtistName else "Embedding metadata..." + } else if (currentStatus == "preparing" && total <= 0) { + "Preparing download..." + } else if (currentArtistName.isNotEmpty() && queueCount <= 1) { currentArtistName } else if (total > 0) { val progressPercent = (progress * 100 / total).toInt() @@ -194,7 +203,7 @@ class DownloadService : Service() { val totalMB = total / (1024.0 * 1024.0) String.format("%.1f / %.1f MB (%d%%)", progressMB, totalMB, progressPercent) } else { - "Preparing download..." + "Downloading..." } val builder = NotificationCompat.Builder(this, CHANNEL_ID) @@ -207,10 +216,12 @@ class DownloadService : Service() { .setPriority(NotificationCompat.PRIORITY_LOW) .setCategory(NotificationCompat.CATEGORY_PROGRESS) - if (total > 0) { + if (currentStatus == "preparing" && total <= 0) { + builder.setProgress(0, 0, true) + } else if (total > 0) { builder.setProgress(100, (progress * 100 / total).toInt(), false) } else { - builder.setProgress(0, 0, true) + builder.setProgress(0, 0, false) } return builder.build() diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index c20aabfe..54ee3543 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2876,10 +2876,11 @@ class MainActivity: FlutterFragmentActivity() { "updateDownloadServiceProgress" -> { val trackName = call.argument("track_name") ?: "" val artistName = call.argument("artist_name") ?: "" - val progress = call.argument("progress") ?: 0L - val total = call.argument("total") ?: 0L - val queueCount = call.argument("queue_count") ?: 0 - DownloadService.updateProgress(this@MainActivity, trackName, artistName, progress, total, queueCount) + val progress = (call.argument("progress") ?: 0).toLong() + val total = (call.argument("total") ?: 0).toLong() + val queueCount = (call.argument("queue_count") ?: 0).toInt() + val status = call.argument("status") ?: "downloading" + DownloadService.updateProgress(this@MainActivity, trackName, artistName, progress, total, queueCount, status) result.success(null) } "isDownloadServiceRunning" -> { diff --git a/go_backend/progress.go b/go_backend/progress.go index 42f4562d..6fc20b65 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -273,6 +273,10 @@ func SetItemBytesReceived(itemID string, received int64) { if item.BytesTotal > 0 { item.Progress = float64(received) / float64(item.BytesTotal) } + if received > 0 { + item.IsDownloading = true + item.Status = itemProgressStatusDownloading + } markMultiProgressDirtyIfChangedLocked(item, before) } } @@ -288,6 +292,10 @@ func SetItemBytesReceivedWithSpeed(itemID string, received int64, speedMBps floa if item.BytesTotal > 0 { item.Progress = float64(received) / float64(item.BytesTotal) } + if received > 0 { + item.IsDownloading = true + item.Status = itemProgressStatusDownloading + } markMultiProgressDirtyIfChangedLocked(item, before) } } @@ -318,6 +326,10 @@ func SetItemProgress(itemID string, progress float64, bytesReceived, bytesTotal if bytesTotal > 0 { item.BytesTotal = bytesTotal } + if progress > 0 || bytesReceived > 0 || bytesTotal > 0 { + item.IsDownloading = true + item.Status = itemProgressStatusDownloading + } markMultiProgressDirtyIfChangedLocked(item, before) } } diff --git a/go_backend/progress_test.go b/go_backend/progress_test.go index 16efc98d..7390107b 100644 --- a/go_backend/progress_test.go +++ b/go_backend/progress_test.go @@ -27,8 +27,8 @@ func TestItemProgressPreparingAndDownloadingStatuses(t *testing.T) { SetItemProgress(itemID, 0.37, 0, 0) if item := multiProgress.Items[itemID]; item == nil { t.Fatal("expected item progress entry to exist after update") - } else if item.Status != itemProgressStatusPreparing { - t.Fatalf("status after progress update = %q, want %q", item.Status, itemProgressStatusPreparing) + } else if item.Status != itemProgressStatusDownloading { + t.Fatalf("status after progress update = %q, want %q", item.Status, itemProgressStatusDownloading) } SetItemDownloading(itemID) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 61d5582b..54874e2d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5818,9 +5818,27 @@ abstract class AppLocalizations { /// Notification body for queue complete - how many tracks were downloaded /// /// In en, this message translates to: - /// **'{count} tracks downloaded successfully'** + /// **'{count, plural, =1{1 track downloaded successfully} other{{count} tracks downloaded successfully}}'** String notifTracksDownloadedSuccess(int count); + /// Notification body when queue finishes with failures + /// + /// In en, this message translates to: + /// **'{completed, plural, =1{1 track downloaded} other{{completed} tracks downloaded}}, {failed, plural, =1{1 failed} other{{failed} failed}}'** + String notifDownloadsFinishedBody(int completed, int failed); + + /// Notification title when downloads are canceled by the user + /// + /// In en, this message translates to: + /// **'Downloads canceled'** + String get notifDownloadsCanceledTitle; + + /// Notification body when downloads are canceled by the user + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 download canceled by user} other{{count} downloads canceled by user}}'** + String notifDownloadsCanceledBody(int count); + /// Notification title while scanning local library /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1919bfb8..84d55fd7 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3448,6 +3448,37 @@ class AppLocalizationsDe extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 07f5f28d..e0703ac1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3410,7 +3410,44 @@ class AppLocalizationsEn extends AppLocalizations { @override String notifTracksDownloadedSuccess(int count) { - return '$count tracks downloaded successfully'; + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks downloaded successfully', + one: '1 track downloaded successfully', + ); + return '$_temp0'; + } + + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 5ac1ff36..d3c10667 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3413,6 +3413,37 @@ class AppLocalizationsEs extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 208bda92..b2d58788 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3417,6 +3417,37 @@ class AppLocalizationsFr extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index c7da821d..a6fa87b8 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -3414,6 +3414,37 @@ class AppLocalizationsHi extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 384a3536..cb6342b7 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -3412,15 +3412,28 @@ class AppLocalizationsId extends AppLocalizations { @override String notifDownloadsFinished(int completed, int failed) { - return 'Downloads Finished ($completed done, $failed failed)'; + return 'Unduhan Selesai ($completed selesai, $failed gagal)'; } @override - String get notifAllDownloadsComplete => 'All Downloads Complete'; + String get notifAllDownloadsComplete => 'Semua Unduhan Selesai'; @override String notifTracksDownloadedSuccess(int count) { - return '$count tracks downloaded successfully'; + return '$count lagu berhasil diunduh'; + } + + @override + String notifDownloadsFinishedBody(int completed, int failed) { + return '$completed lagu diunduh, $failed gagal'; + } + + @override + String get notifDownloadsCanceledTitle => 'Unduhan dibatalkan'; + + @override + String notifDownloadsCanceledBody(int count) { + return '$count unduhan dibatalkan oleh pengguna'; } @override diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 1c5464f7..eba3e4d9 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3401,6 +3401,37 @@ class AppLocalizationsJa extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index ab0567cb..47ce7031 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3394,6 +3394,37 @@ class AppLocalizationsKo extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 5e512c80..dbc0c4b7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -3414,6 +3414,37 @@ class AppLocalizationsNl extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 22ea60ce..62bc9612 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3413,6 +3413,37 @@ class AppLocalizationsPt extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ff0a2fe2..ca895859 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3473,6 +3473,37 @@ class AppLocalizationsRu extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 1a112b04..12a84e85 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -3440,6 +3440,37 @@ class AppLocalizationsTr extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 916ba7b4..495c602f 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3473,6 +3473,37 @@ class AppLocalizationsUk extends AppLocalizations { return '$count треки успішно завантажено'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Сканування локальної бібліотеки'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index f46af804..b7da2294 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -3413,6 +3413,37 @@ class AppLocalizationsZh extends AppLocalizations { return '$count tracks downloaded successfully'; } + @override + String notifDownloadsFinishedBody(int completed, int failed) { + String _temp0 = intl.Intl.pluralLogic( + completed, + locale: localeName, + other: '$completed tracks downloaded', + one: '1 track downloaded', + ); + String _temp1 = intl.Intl.pluralLogic( + failed, + locale: localeName, + other: '$failed failed', + one: '1 failed', + ); + return '$_temp0, $_temp1'; + } + + @override + String get notifDownloadsCanceledTitle => 'Downloads canceled'; + + @override + String notifDownloadsCanceledBody(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count downloads canceled by user', + one: '1 download canceled by user', + ); + return '$_temp0'; + } + @override String get notifScanningLibrary => 'Scanning local library'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 371a80ff..01e9d64a 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4465,7 +4465,7 @@ "@notifAllDownloadsComplete": { "description": "Notification title when all downloads finish successfully" }, - "notifTracksDownloadedSuccess": "{count} tracks downloaded successfully", + "notifTracksDownloadedSuccess": "{count, plural, =1{1 track downloaded successfully} other{{count} tracks downloaded successfully}}", "@notifTracksDownloadedSuccess": { "description": "Notification body for queue complete - how many tracks were downloaded", "placeholders": { @@ -4474,6 +4474,31 @@ } } }, + "notifDownloadsFinishedBody": "{completed, plural, =1{1 track downloaded} other{{completed} tracks downloaded}}, {failed, plural, =1{1 failed} other{{failed} failed}}", + "@notifDownloadsFinishedBody": { + "description": "Notification body when queue finishes with failures", + "placeholders": { + "completed": { + "type": "int" + }, + "failed": { + "type": "int" + } + } + }, + "notifDownloadsCanceledTitle": "Downloads canceled", + "@notifDownloadsCanceledTitle": { + "description": "Notification title when downloads are canceled by the user" + }, + "notifDownloadsCanceledBody": "{count, plural, =1{1 download canceled by user} other{{count} downloads canceled by user}}", + "@notifDownloadsCanceledBody": { + "description": "Notification body when downloads are canceled by the user", + "placeholders": { + "count": { + "type": "int" + } + } + }, "notifScanningLibrary": "Scanning local library", "@notifScanningLibrary": { "description": "Notification title while scanning local library" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 3b317c32..e177f7cf 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -4366,7 +4366,7 @@ "@notifDownloadComplete": { "description": "Notification title when a single download is complete" }, - "notifDownloadsFinished": "Downloads Finished ({completed} done, {failed} failed)", + "notifDownloadsFinished": "Unduhan Selesai ({completed} selesai, {failed} gagal)", "@notifDownloadsFinished": { "description": "Notification title when queue finishes with some failures", "placeholders": { @@ -4378,11 +4378,11 @@ } } }, - "notifAllDownloadsComplete": "All Downloads Complete", + "notifAllDownloadsComplete": "Semua Unduhan Selesai", "@notifAllDownloadsComplete": { "description": "Notification title when all downloads finish successfully" }, - "notifTracksDownloadedSuccess": "{count} tracks downloaded successfully", + "notifTracksDownloadedSuccess": "{count} lagu berhasil diunduh", "@notifTracksDownloadedSuccess": { "description": "Notification body for queue complete - how many tracks were downloaded", "placeholders": { @@ -4391,6 +4391,31 @@ } } }, + "notifDownloadsFinishedBody": "{completed} lagu diunduh, {failed} gagal", + "@notifDownloadsFinishedBody": { + "description": "Notification body when queue finishes with failures", + "placeholders": { + "completed": { + "type": "int" + }, + "failed": { + "type": "int" + } + } + }, + "notifDownloadsCanceledTitle": "Unduhan dibatalkan", + "@notifDownloadsCanceledTitle": { + "description": "Notification title when downloads are canceled by the user" + }, + "notifDownloadsCanceledBody": "{count} unduhan dibatalkan oleh pengguna", + "@notifDownloadsCanceledBody": { + "description": "Notification body when downloads are canceled by the user", + "placeholders": { + "count": { + "type": "int" + } + } + }, "notifScanningLibrary": "Scanning local library", "@notifScanningLibrary": { "description": "Notification title while scanning local library" diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index a81c1c19..dad08bae 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1354,6 +1354,7 @@ class DownloadQueueNotifier extends Notifier { bool _networkPausedByWifiOnly = false; String? _lastServiceTrackName; String? _lastServiceArtistName; + String? _lastServiceStatus; int _lastServicePercent = -1; int _lastServiceQueueCount = -1; DateTime _lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0); @@ -1672,6 +1673,9 @@ class DownloadQueueNotifier extends Notifier { int queuedCount = 0; int downloadingCount = 0; DownloadItem? firstDownloading; + bool hasFinalizingItem = false; + String? finalizingTrackName; + String? finalizingArtistName; for (int i = 0; i < currentItems.length; i++) { final item = currentItems[i]; if (item.status == DownloadStatus.downloading) { @@ -1679,16 +1683,18 @@ class DownloadQueueNotifier extends Notifier { firstDownloading ??= item; } if (item.status == DownloadStatus.queued || - item.status == DownloadStatus.downloading) { + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.finalizing) { queuedCount++; } + if (item.status == DownloadStatus.finalizing && !hasFinalizingItem) { + hasFinalizingItem = true; + finalizingTrackName = item.track.name; + finalizingArtistName = item.track.artistName; + } } final progressUpdates = {}; - bool hasFinalizingItem = false; - String? finalizingTrackName; - String? finalizingArtistName; - for (final entry in items.entries) { final itemId = entry.key; final localItem = lookup.byItemId[itemId]; @@ -1707,6 +1713,13 @@ class DownloadQueueNotifier extends Notifier { localItem.status == DownloadStatus.failed) { continue; } + if (localItem.status == DownloadStatus.finalizing) { + PlatformBridge.clearItemProgress(itemId).catchError((_) {}); + hasFinalizingItem = true; + finalizingTrackName = localItem.track.name; + finalizingArtistName = localItem.track.artistName; + continue; + } final rawItemProgress = entry.value; if (rawItemProgress is! Map) { continue; @@ -1718,6 +1731,10 @@ class DownloadQueueNotifier extends Notifier { final speedMBps = (itemProgress['speed_mbps'] as num?)?.toDouble() ?? 0.0; final isDownloading = itemProgress['is_downloading'] as bool? ?? false; final status = itemProgress['status'] as String? ?? 'downloading'; + final progressFromBackend = + (itemProgress['progress'] as num?)?.toDouble() ?? 0.0; + final hasRealProgress = + bytesReceived > 0 || bytesTotal > 0 || progressFromBackend > 0; if (status == 'finalizing') { progressUpdates[itemId] = const _ProgressUpdate( @@ -1730,7 +1747,7 @@ class DownloadQueueNotifier extends Notifier { continue; } - if (status == 'preparing') { + if (status == 'preparing' && !hasRealProgress) { progressUpdates[itemId] = const _ProgressUpdate( status: DownloadStatus.downloading, progress: 0.0, @@ -1745,10 +1762,7 @@ class DownloadQueueNotifier extends Notifier { continue; } - final progressFromBackend = - (itemProgress['progress'] as num?)?.toDouble() ?? 0.0; - - if (isDownloading) { + if (isDownloading || hasRealProgress) { double percentage = 0.0; if (bytesTotal > 0) { percentage = bytesReceived / bytesTotal; @@ -1798,6 +1812,10 @@ class DownloadQueueNotifier extends Notifier { continue; } final update = entry.value; + if (current.status == DownloadStatus.finalizing && + update.status != DownloadStatus.finalizing) { + continue; + } final next = current.copyWith( status: update.status, progress: update.progress, @@ -1833,7 +1851,16 @@ class DownloadQueueNotifier extends Notifier { if (hasFinalizingItem && finalizingTrackName != null) { final safeArtistName = finalizingArtistName ?? ''; - if (finalizingTrackName != _lastFinalizingTrackName || + if (Platform.isAndroid) { + _maybeUpdateAndroidDownloadService( + trackName: finalizingTrackName, + artistName: _notificationService.embeddingMetadataLabel, + progress: 100, + total: 100, + queueCount: queuedCount, + status: 'finalizing', + ); + } else if (finalizingTrackName != _lastFinalizingTrackName || safeArtistName != _lastFinalizingArtistName) { _notificationService.showDownloadFinalizing( trackName: finalizingTrackName, @@ -1848,18 +1875,18 @@ class DownloadQueueNotifier extends Notifier { _lastFinalizingArtistName = null; if (items.isNotEmpty) { - final firstEntry = items.entries.first; - final rawFirstProgress = firstEntry.value; - if (rawFirstProgress is! Map) { - return; - } - final firstProgress = Map.from(rawFirstProgress); - final bytesReceived = - (firstProgress['bytes_received'] as num?)?.toInt() ?? 0; - final bytesTotal = (firstProgress['bytes_total'] as num?)?.toInt() ?? 0; - final backendStatus = firstProgress['status'] as String? ?? 'downloading'; - if (downloadingCount > 0 && firstDownloading != null) { + final rawProgress = items[firstDownloading.id]; + if (rawProgress is! Map) { + return; + } + final selectedProgress = Map.from(rawProgress); + final bytesReceived = + (selectedProgress['bytes_received'] as num?)?.toInt() ?? 0; + final bytesTotal = + (selectedProgress['bytes_total'] as num?)?.toInt() ?? 0; + final backendStatus = + selectedProgress['status'] as String? ?? 'downloading'; final trackName = downloadingCount == 1 ? firstDownloading.track.name : '$downloadingCount downloads'; @@ -1870,24 +1897,29 @@ class DownloadQueueNotifier extends Notifier { int notifProgress = bytesReceived; int notifTotal = bytesTotal; - if (backendStatus == 'preparing') { + final progressPercent = + (selectedProgress['progress'] as num?)?.toDouble() ?? 0.0; + final hasRealProgress = + bytesReceived > 0 || bytesTotal > 0 || progressPercent > 0; + + if (backendStatus == 'preparing' && !hasRealProgress) { notifProgress = 0; - notifTotal = 100; + notifTotal = 0; } else if (bytesTotal <= 0) { - final progressPercent = - (firstProgress['progress'] as num?)?.toDouble() ?? 0.0; notifProgress = (progressPercent * 100).toInt(); notifTotal = 100; } + final serviceStatus = notifTotal <= 0 ? 'preparing' : 'downloading'; - final safeNotifTotal = notifTotal > 0 ? notifTotal : 1; - if (_shouldUpdateProgressNotification( - trackName: trackName, - artistName: artistName, - progress: notifProgress, - total: safeNotifTotal, - queueCount: queuedCount, - )) { + if (!Platform.isAndroid && + _shouldUpdateProgressNotification( + trackName: trackName, + artistName: artistName, + progress: notifProgress, + total: notifTotal, + queueCount: queuedCount, + )) { + final safeNotifTotal = notifTotal > 0 ? notifTotal : 1; _notificationService.showDownloadProgress( trackName: trackName, artistName: artistName, @@ -1901,8 +1933,9 @@ class DownloadQueueNotifier extends Notifier { trackName: firstDownloading.track.name, artistName: firstDownloading.track.artistName, progress: notifProgress, - total: safeNotifTotal, + total: notifTotal, queueCount: queuedCount, + status: serviceStatus, ); } } @@ -1915,22 +1948,28 @@ class DownloadQueueNotifier extends Notifier { required int progress, required int total, required int queueCount, + String status = 'downloading', }) { final now = DateTime.now(); - final safeTotal = total > 0 ? total : 1; - final progressPercent = ((progress * 100) / safeTotal) - .round() - .clamp(0, 100) - .toInt(); - final progressBucket = progressPercent == 100 - ? 100 - : ((progressPercent ~/ _serviceProgressStepPercent) * - _serviceProgressStepPercent) - .clamp(0, 100); + final progressBucket = total <= 0 + ? -1 + : (() { + final progressPercent = ((progress * 100) / total) + .round() + .clamp(0, 100) + .toInt(); + return progressPercent == 100 + ? 100 + : ((progressPercent ~/ _serviceProgressStepPercent) * + _serviceProgressStepPercent) + .clamp(0, 100) + .toInt(); + })(); final didContentChange = trackName != _lastServiceTrackName || artistName != _lastServiceArtistName || + status != _lastServiceStatus || queueCount != _lastServiceQueueCount || progressBucket != _lastServicePercent; final allowHeartbeat = @@ -1942,6 +1981,7 @@ class DownloadQueueNotifier extends Notifier { _lastServiceTrackName = trackName; _lastServiceArtistName = artistName; + _lastServiceStatus = status; _lastServicePercent = progressBucket; _lastServiceQueueCount = queueCount; _lastServiceUpdateAt = now; @@ -1950,8 +1990,9 @@ class DownloadQueueNotifier extends Notifier { trackName: trackName, artistName: artistName, progress: progress, - total: safeTotal, + total: total, queueCount: queueCount, + status: status, ).catchError((_) {}); } @@ -1969,6 +2010,7 @@ class DownloadQueueNotifier extends Notifier { _usingProgressStream = false; _lastServiceTrackName = null; _lastServiceArtistName = null; + _lastServiceStatus = null; _lastServicePercent = -1; _lastServiceQueueCount = -1; _lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0); @@ -3015,6 +3057,26 @@ class DownloadQueueNotifier extends Notifier { updatedItems[index] = next; state = state.copyWith(items: updatedItems); + if (Platform.isAndroid && status == DownloadStatus.finalizing) { + PlatformBridge.clearItemProgress(id).catchError((_) {}); + final queueCount = updatedItems + .where( + (entry) => + entry.status == DownloadStatus.queued || + entry.status == DownloadStatus.downloading || + entry.status == DownloadStatus.finalizing, + ) + .length; + _maybeUpdateAndroidDownloadService( + trackName: next.track.name, + artistName: _notificationService.embeddingMetadataLabel, + progress: 100, + total: 100, + queueCount: queueCount, + status: 'finalizing', + ); + } + if (status == DownloadStatus.completed || status == DownloadStatus.failed || status == DownloadStatus.skipped) { @@ -4327,6 +4389,7 @@ class DownloadQueueNotifier extends Notifier { orElse: () => state.items.first, ); try { + await _notificationService.cancelDownloadNotification(); await PlatformBridge.startDownloadService( trackName: firstItem.track.name, artistName: firstItem.track.artistName, @@ -4470,7 +4533,8 @@ class DownloadQueueNotifier extends Notifier { _log.i( 'Queue stats - completed: $_completedInSession, failed: $_failedInSession, totalAtStart: $_totalQueuedAtStart', ); - if (!stoppedWhilePaused && _totalQueuedAtStart > 0) { + final hasSessionResults = _completedInSession > 0 || _failedInSession > 0; + if (!stoppedWhilePaused && _totalQueuedAtStart > 0 && hasSessionResults) { await _notificationService.showQueueComplete( completedCount: _completedInSession, failedCount: _failedInSession, @@ -4483,6 +4547,10 @@ class DownloadQueueNotifier extends Notifier { _log.i('Auto-exported failed downloads to: $exportPath'); } } + } else if (!stoppedWhilePaused && _totalQueuedAtStart > 0) { + await _notificationService.showQueueCanceled( + canceledCount: _totalQueuedAtStart, + ); } if (stoppedWhilePaused) { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 9b3e8822..196ad8d3 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -23,6 +23,9 @@ class NotificationService { _l10n = l10n; } + String get embeddingMetadataLabel => + _l10n?.notifEmbeddingMetadata ?? 'Embedding metadata...'; + static const int downloadProgressId = 1; static const int updateDownloadId = 2; static const int libraryScanId = 3; @@ -282,11 +285,17 @@ class NotificationService { required int failedCount, }) async { if (!_isInitialized) await initialize(); + if (completedCount <= 0 && failedCount <= 0) return; final title = failedCount > 0 ? (_l10n?.notifDownloadsFinished(completedCount, failedCount) ?? 'Downloads Finished ($completedCount done, $failedCount failed)') : (_l10n?.notifAllDownloadsComplete ?? 'All Downloads Complete'); + final body = failedCount > 0 + ? (_l10n?.notifDownloadsFinishedBody(completedCount, failedCount) ?? + '$completedCount downloaded, $failedCount failed') + : (_l10n?.notifTracksDownloadedSuccess(completedCount) ?? + '$completedCount tracks downloaded successfully'); const androidDetails = AndroidNotificationDetails( channelId, @@ -313,9 +322,46 @@ class NotificationService { await _showSafely( id: downloadProgressId, title: title, - body: - _l10n?.notifTracksDownloadedSuccess(completedCount) ?? - '$completedCount tracks downloaded successfully', + body: body, + details: details, + ); + } + + Future showQueueCanceled({required int canceledCount}) async { + if (!_isInitialized) await initialize(); + if (canceledCount <= 0) return; + + final title = _l10n?.notifDownloadsCanceledTitle ?? 'Downloads canceled'; + final body = + _l10n?.notifDownloadsCanceledBody(canceledCount) ?? + '$canceledCount downloads canceled by user'; + + const androidDetails = AndroidNotificationDetails( + channelId, + channelName, + channelDescription: channelDescription, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + playSound: false, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: false, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _showSafely( + id: downloadProgressId, + title: title, + body: body, details: details, ); } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index df4480d3..c26ad1b3 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -816,6 +816,7 @@ class PlatformBridge { required int progress, required int total, required int queueCount, + String status = 'downloading', }) async { await _channel.invokeMethod('updateDownloadServiceProgress', { 'track_name': trackName, @@ -823,6 +824,7 @@ class PlatformBridge { 'progress': progress, 'total': total, 'queue_count': queueCount, + 'status': status, }); }