fix: sync download progress notification states

This commit is contained in:
zarzet
2026-05-04 17:24:57 +07:00
parent a4dc776bfb
commit 82e317c4a8
24 changed files with 700 additions and 70 deletions
@@ -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()
@@ -2876,10 +2876,11 @@ class MainActivity: FlutterFragmentActivity() {
"updateDownloadServiceProgress" -> {
val trackName = call.argument<String>("track_name") ?: ""
val artistName = call.argument<String>("artist_name") ?: ""
val progress = call.argument<Long>("progress") ?: 0L
val total = call.argument<Long>("total") ?: 0L
val queueCount = call.argument<Int>("queue_count") ?: 0
DownloadService.updateProgress(this@MainActivity, trackName, artistName, progress, total, queueCount)
val progress = (call.argument<Number>("progress") ?: 0).toLong()
val total = (call.argument<Number>("total") ?: 0).toLong()
val queueCount = (call.argument<Number>("queue_count") ?: 0).toInt()
val status = call.argument<String>("status") ?: "downloading"
DownloadService.updateProgress(this@MainActivity, trackName, artistName, progress, total, queueCount, status)
result.success(null)
}
"isDownloadServiceRunning" -> {
+12
View File
@@ -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)
}
}
+2 -2
View File
@@ -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)
+19 -1
View File
@@ -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:
+31
View File
@@ -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';
+38 -1
View File
@@ -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
+31
View File
@@ -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';
+31
View File
@@ -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';
+31
View File
@@ -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';
+16 -3
View File
@@ -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
+31
View File
@@ -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';
+31
View File
@@ -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';
+31
View File
@@ -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';
+31
View File
@@ -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';
+31
View File
@@ -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';
+31
View File
@@ -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';
+31
View File
@@ -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 => 'Сканування локальної бібліотеки';
+31
View File
@@ -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';
+26 -1
View File
@@ -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"
+28 -3
View File
@@ -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"
+115 -47
View File
@@ -1354,6 +1354,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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 = <String, _ProgressUpdate>{};
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
_lastFinalizingArtistName = null;
if (items.isNotEmpty) {
final firstEntry = items.entries.first;
final rawFirstProgress = firstEntry.value;
if (rawFirstProgress is! Map) {
return;
}
final firstProgress = Map<String, dynamic>.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<String, dynamic>.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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
_lastServiceTrackName = trackName;
_lastServiceArtistName = artistName;
_lastServiceStatus = status;
_lastServicePercent = progressBucket;
_lastServiceQueueCount = queueCount;
_lastServiceUpdateAt = now;
@@ -1950,8 +1990,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackName: trackName,
artistName: artistName,
progress: progress,
total: safeTotal,
total: total,
queueCount: queueCount,
status: status,
).catchError((_) {});
}
@@ -1969,6 +2010,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
_usingProgressStream = false;
_lastServiceTrackName = null;
_lastServiceArtistName = null;
_lastServiceStatus = null;
_lastServicePercent = -1;
_lastServiceQueueCount = -1;
_lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0);
@@ -3015,6 +3057,26 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
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<DownloadQueueState> {
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<DownloadQueueState> {
_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<DownloadQueueState> {
_log.i('Auto-exported failed downloads to: $exportPath');
}
}
} else if (!stoppedWhilePaused && _totalQueuedAtStart > 0) {
await _notificationService.showQueueCanceled(
canceledCount: _totalQueuedAtStart,
);
}
if (stoppedWhilePaused) {
+49 -3
View File
@@ -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<void> 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,
);
}
+2
View File
@@ -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,
});
}