mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-23 18:11:02 +02:00
refactor(screens): share album-screen scaffolding and track flows
- SelectionModeMixin + CollapsingHeaderScrollMixin extracted; local/downloaded album screens now share AlbumTrackTile, AlbumScaffoldBody, DestructiveSelectionButton, HeaderMetaRow, and confirmAndDeleteTracks - track_detail_actions.dart: shared downloadSingleTrack, queueTracksSkippingDownloaded, download-all confirm, queued snackbar, release-date formatter, list footer, love-all - TtlCache<T> replaces the copied 10-minute static caches; album fetch branches share _applyAlbumMetadata - playlist error card now uses ErrorCard; formatMegabytes shared by queue tab and update dialog
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
/// Shows a delete-confirmation dialog, deletes the given [ids] one by one via
|
||||
/// [deleteItem], then shows a "deleted N tracks" snackbar.
|
||||
/// [onExitSelectionMode] runs right after the delete loop, matching the
|
||||
/// screens' original ordering.
|
||||
///
|
||||
/// Returns the number of items [deleteItem] reported deleted, or null if the
|
||||
/// user cancelled the dialog.
|
||||
Future<int?> confirmAndDeleteTracks({
|
||||
required BuildContext context,
|
||||
required List<String> ids,
|
||||
required Future<bool> Function(String id) deleteItem,
|
||||
required VoidCallback onExitSelectionMode,
|
||||
}) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.l10n.downloadedAlbumDeleteSelected),
|
||||
content: Text(context.l10n.downloadedAlbumDeleteMessage(ids.length)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text(context.l10n.dialogDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !context.mounted) return null;
|
||||
|
||||
var deletedCount = 0;
|
||||
for (final id in ids) {
|
||||
if (await deleteItem(id)) deletedCount++;
|
||||
}
|
||||
|
||||
onExitSelectionMode();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarDeletedTracks(deletedCount))),
|
||||
);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
@@ -55,6 +55,11 @@ String? normalizeRemoteHttpUrl(String? value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Byte count expressed as a plain megabyte number with 1 decimal, e.g. "3.4".
|
||||
String formatMegabytes(num bytes) {
|
||||
return (bytes / (1024 * 1024)).toStringAsFixed(1);
|
||||
}
|
||||
|
||||
/// Human-readable byte size: "512 B", "3.4 KB", "12.0 MB", "1.25 GB".
|
||||
String formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/// Simple in-memory cache where each entry expires after [ttl].
|
||||
class TtlCache<T> {
|
||||
final Duration ttl;
|
||||
final Map<String, _TtlEntry<T>> _entries = {};
|
||||
|
||||
TtlCache(this.ttl);
|
||||
|
||||
T? get(String key) {
|
||||
final entry = _entries[key];
|
||||
if (entry == null) return null;
|
||||
if (DateTime.now().isAfter(entry.expiresAt)) {
|
||||
_entries.remove(key);
|
||||
return null;
|
||||
}
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
void set(String key, T value) {
|
||||
_entries[key] = _TtlEntry(value, DateTime.now().add(ttl));
|
||||
}
|
||||
}
|
||||
|
||||
class _TtlEntry<T> {
|
||||
final T value;
|
||||
final DateTime expiresAt;
|
||||
_TtlEntry(this.value, this.expiresAt);
|
||||
}
|
||||
Reference in New Issue
Block a user