mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-13 19:21:23 +02:00
perf: memory and rebuild optimizations across app
- Bound Deezer cache with LRU eviction and periodic cleanup - Configure Flutter image cache limits (240 entries / 60 MiB) - Add ResizeImage wrapper for precacheImage calls - Add memCacheWidth/cacheWidth to cover images across screens - Add DownloadedEmbeddedCoverResolver as centralized cover service - Throttle download progress notifications with dedup checks - Normalize progress/speed/bytes values to reduce UI rebuilds - Optimize queue list with per-item ConsumerWidget and RepaintBoundary - Preserve derived indexes in LocalLibraryState.copyWith - Skip non-error logs when detailed logging disabled - Use async file stat and early-break loops in queue filters
This commit is contained in:
1 parent
68e6c8be35
commit
a9150b85b9
14 files changed
+891
-140
No files matched your search
@@ -673,6 +673,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
static const _queueStorageKey = 'download_queue';
|
||||
static const _progressPollingInterval = Duration(milliseconds: 800);
|
||||
static const _queueSchedulingInterval = Duration(milliseconds: 250);
|
||||
static const _bytesUiStep = 104857; // ~0.1 MiB, matches one-decimal MB UI.
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
int _totalQueuedAtStart = 0;
|
||||
@@ -686,6 +687,55 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
int _lastServicePercent = -1;
|
||||
int _lastServiceQueueCount = -1;
|
||||
DateTime _lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
String? _lastFinalizingTrackName;
|
||||
String? _lastFinalizingArtistName;
|
||||
String? _lastNotifTrackName;
|
||||
String? _lastNotifArtistName;
|
||||
int _lastNotifPercent = -1;
|
||||
int _lastNotifQueueCount = -1;
|
||||
|
||||
double _normalizeProgressForUi(double value) {
|
||||
final clamped = value.clamp(0.0, 1.0).toDouble();
|
||||
if (clamped <= 0) return 0;
|
||||
if (clamped >= 1) return 1;
|
||||
final rounded = double.parse(clamped.toStringAsFixed(2));
|
||||
return rounded == 0 ? 0.01 : rounded;
|
||||
}
|
||||
|
||||
double _normalizeSpeedForUi(double value) {
|
||||
if (value <= 0) return 0;
|
||||
return double.parse(value.toStringAsFixed(1));
|
||||
}
|
||||
|
||||
int _normalizeBytesForUi(int value) {
|
||||
if (value <= 0) return 0;
|
||||
return (value ~/ _bytesUiStep) * _bytesUiStep;
|
||||
}
|
||||
|
||||
bool _shouldUpdateProgressNotification({
|
||||
required String trackName,
|
||||
required String artistName,
|
||||
required int progress,
|
||||
required int total,
|
||||
required int queueCount,
|
||||
}) {
|
||||
final safeTotal = total > 0 ? total : 1;
|
||||
final percent = ((progress * 100) / safeTotal).round().clamp(0, 100);
|
||||
final changed =
|
||||
trackName != _lastNotifTrackName ||
|
||||
artistName != _lastNotifArtistName ||
|
||||
percent != _lastNotifPercent ||
|
||||
queueCount != _lastNotifQueueCount;
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastNotifTrackName = trackName;
|
||||
_lastNotifArtistName = artistName;
|
||||
_lastNotifPercent = percent;
|
||||
_lastNotifQueueCount = queueCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
DownloadQueueState build() {
|
||||
@@ -854,12 +904,15 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
} else {
|
||||
percentage = progressFromBackend;
|
||||
}
|
||||
final normalizedProgress = _normalizeProgressForUi(percentage);
|
||||
final normalizedSpeed = _normalizeSpeedForUi(speedMBps);
|
||||
final normalizedBytes = _normalizeBytesForUi(bytesReceived);
|
||||
|
||||
progressUpdates[itemId] = _ProgressUpdate(
|
||||
status: DownloadStatus.downloading,
|
||||
progress: percentage,
|
||||
speedMBps: speedMBps,
|
||||
bytesReceived: bytesReceived,
|
||||
progress: normalizedProgress,
|
||||
speedMBps: normalizedSpeed,
|
||||
bytesReceived: normalizedBytes,
|
||||
);
|
||||
|
||||
final mbReceived = bytesReceived / (1024 * 1024);
|
||||
@@ -914,12 +967,20 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
if (hasFinalizingItem && finalizingTrackName != null) {
|
||||
_notificationService.showDownloadFinalizing(
|
||||
trackName: finalizingTrackName,
|
||||
artistName: finalizingArtistName ?? '',
|
||||
);
|
||||
final safeArtistName = finalizingArtistName ?? '';
|
||||
if (finalizingTrackName != _lastFinalizingTrackName ||
|
||||
safeArtistName != _lastFinalizingArtistName) {
|
||||
_notificationService.showDownloadFinalizing(
|
||||
trackName: finalizingTrackName,
|
||||
artistName: safeArtistName,
|
||||
);
|
||||
_lastFinalizingTrackName = finalizingTrackName;
|
||||
_lastFinalizingArtistName = safeArtistName;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_lastFinalizingTrackName = null;
|
||||
_lastFinalizingArtistName = null;
|
||||
|
||||
if (items.isNotEmpty) {
|
||||
final firstEntry = items.entries.first;
|
||||
@@ -945,19 +1006,28 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
notifTotal = 100;
|
||||
}
|
||||
|
||||
_notificationService.showDownloadProgress(
|
||||
final safeNotifTotal = notifTotal > 0 ? notifTotal : 1;
|
||||
if (_shouldUpdateProgressNotification(
|
||||
trackName: trackName,
|
||||
artistName: artistName,
|
||||
progress: notifProgress,
|
||||
total: notifTotal > 0 ? notifTotal : 1,
|
||||
);
|
||||
total: safeNotifTotal,
|
||||
queueCount: queuedCount,
|
||||
)) {
|
||||
_notificationService.showDownloadProgress(
|
||||
trackName: trackName,
|
||||
artistName: artistName,
|
||||
progress: notifProgress,
|
||||
total: safeNotifTotal,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
_maybeUpdateAndroidDownloadService(
|
||||
trackName: firstDownloading.track.name,
|
||||
artistName: firstDownloading.track.artistName,
|
||||
progress: notifProgress,
|
||||
total: notifTotal > 0 ? notifTotal : 1,
|
||||
total: safeNotifTotal,
|
||||
queueCount: queuedCount,
|
||||
);
|
||||
}
|
||||
@@ -1023,6 +1093,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_lastServicePercent = -1;
|
||||
_lastServiceQueueCount = -1;
|
||||
_lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
_lastFinalizingTrackName = null;
|
||||
_lastFinalizingArtistName = null;
|
||||
_lastNotifTrackName = null;
|
||||
_lastNotifArtistName = null;
|
||||
_lastNotifPercent = -1;
|
||||
_lastNotifQueueCount = -1;
|
||||
}
|
||||
|
||||
Future<void> _initOutputDir() async {
|
||||
|
||||
@@ -39,16 +39,23 @@ class LocalLibraryState {
|
||||
this.scanWasCancelled = false,
|
||||
this.lastScannedAt,
|
||||
this.excludedDownloadedCount = 0,
|
||||
}) : _isrcSet = items
|
||||
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
|
||||
.map((item) => item.isrc!)
|
||||
.toSet(),
|
||||
_trackKeySet = items.map((item) => item.matchKey).toSet(),
|
||||
_byIsrc = Map.fromEntries(
|
||||
items
|
||||
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
|
||||
.map((item) => MapEntry(item.isrc!, item)),
|
||||
);
|
||||
Set<String>? isrcSet,
|
||||
Set<String>? trackKeySet,
|
||||
Map<String, LocalLibraryItem>? byIsrc,
|
||||
}) : _isrcSet =
|
||||
isrcSet ??
|
||||
items
|
||||
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
|
||||
.map((item) => item.isrc!)
|
||||
.toSet(),
|
||||
_trackKeySet = trackKeySet ?? items.map((item) => item.matchKey).toSet(),
|
||||
_byIsrc =
|
||||
byIsrc ??
|
||||
Map.fromEntries(
|
||||
items
|
||||
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
|
||||
.map((item) => MapEntry(item.isrc!, item)),
|
||||
);
|
||||
|
||||
bool hasIsrc(String isrc) => _isrcSet.contains(isrc);
|
||||
|
||||
@@ -86,8 +93,11 @@ class LocalLibraryState {
|
||||
DateTime? lastScannedAt,
|
||||
int? excludedDownloadedCount,
|
||||
}) {
|
||||
final nextItems = items ?? this.items;
|
||||
final keepDerivedIndex = identical(nextItems, this.items);
|
||||
|
||||
return LocalLibraryState(
|
||||
items: items ?? this.items,
|
||||
items: nextItems,
|
||||
isScanning: isScanning ?? this.isScanning,
|
||||
scanProgress: scanProgress ?? this.scanProgress,
|
||||
scanCurrentFile: scanCurrentFile ?? this.scanCurrentFile,
|
||||
@@ -98,6 +108,9 @@ class LocalLibraryState {
|
||||
lastScannedAt: lastScannedAt ?? this.lastScannedAt,
|
||||
excludedDownloadedCount:
|
||||
excludedDownloadedCount ?? this.excludedDownloadedCount,
|
||||
isrcSet: keepDerivedIndex ? _isrcSet : null,
|
||||
trackKeySet: keepDerivedIndex ? _trackKeySet : null,
|
||||
byIsrc: keepDerivedIndex ? _byIsrc : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -397,14 +410,33 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
_progressTimer = Timer.periodic(_progressPollingInterval, (_) async {
|
||||
try {
|
||||
final progress = await PlatformBridge.getLibraryScanProgress();
|
||||
|
||||
state = state.copyWith(
|
||||
scanProgress: (progress['progress_pct'] as num?)?.toDouble() ?? 0,
|
||||
scanCurrentFile: progress['current_file'] as String?,
|
||||
scanTotalFiles: progress['total_files'] as int? ?? 0,
|
||||
scannedFiles: progress['scanned_files'] as int? ?? 0,
|
||||
scanErrorCount: progress['error_count'] as int? ?? 0,
|
||||
final nextProgress =
|
||||
(progress['progress_pct'] as num?)?.toDouble() ?? 0;
|
||||
final normalizedProgress = ((nextProgress * 10).round() / 10).clamp(
|
||||
0.0,
|
||||
100.0,
|
||||
);
|
||||
final currentFile = progress['current_file'] as String?;
|
||||
final totalFiles = progress['total_files'] as int? ?? 0;
|
||||
final scannedFiles = progress['scanned_files'] as int? ?? 0;
|
||||
final errorCount = progress['error_count'] as int? ?? 0;
|
||||
|
||||
final shouldUpdateState =
|
||||
state.scanProgress != normalizedProgress ||
|
||||
state.scanCurrentFile != currentFile ||
|
||||
state.scanTotalFiles != totalFiles ||
|
||||
state.scannedFiles != scannedFiles ||
|
||||
state.scanErrorCount != errorCount;
|
||||
|
||||
if (shouldUpdateState) {
|
||||
state = state.copyWith(
|
||||
scanProgress: normalizedProgress,
|
||||
scanCurrentFile: currentFile,
|
||||
scanTotalFiles: totalFiles,
|
||||
scannedFiles: scannedFiles,
|
||||
scanErrorCount: errorCount,
|
||||
);
|
||||
}
|
||||
|
||||
if (progress['is_complete'] == true) {
|
||||
_stopProgressPolling();
|
||||
|
||||
Reference in new issue
Block a user