feat(auto): page local-music folders instead of truncating at 50 [size:exception]

Folders over the 50-item cap now show a "Mas..." item that reveals
the next page on tap, instead of silently dropping the rest. Paging
slices the cheap raw list before building any MediaItem, so items
beyond the requested page are never resolved (art, title) -- proven
by a call-count test. Also swaps the raw SAF content:// URI shown in
settings for a parsed, human-readable folder name with a localized
fallback across all 13 locales.

servicio_audio.dart is untouched; this stays entirely within the
local-music tree/dispatch layer.
This commit is contained in:
2026-07-19 22:14:05 +02:00
parent 977cbcd8cc
commit 725169cd31
25 changed files with 1601 additions and 40 deletions
+47
View File
@@ -21,6 +21,53 @@ bool esArchivoAudio(String? mime, String? nombre) {
return mimeRecortado.toLowerCase().startsWith('audio/');
}
/// Pure-Dart, SAF-URI-parsing derivation of a human-readable folder name
/// (Design ADR-4) — NO native round-trip. SAF tree URIs are
/// `content://<authority>/tree/<encoded-documentId>`; `Uri.pathSegments`
/// already percent-decodes each segment, so the segment right after `tree`
/// is the decoded documentId (e.g. `primary:Music/MyFolder`,
/// `1A2B-3C4D:Music`). The trailing readable part of that documentId is
/// extracted: everything after the last `/` when present, else everything
/// after the last `:`, trimmed. An unparseable [treeUri], a missing/empty
/// `tree` segment, or an empty-after-trim result all fall back to
/// [nombreGenerico] — this function NEVER returns the raw `content://` URI
/// and NEVER returns an empty string.
///
/// [nombreGenerico] is the caller-supplied fallback text (Design ADR-4/ADR-5
/// — genuine phone UI, localized via `AppLocalizations.localMusicFolderGenericName`
/// at the call site in `pantalla_ajustes.dart`). Taking it as a plain
/// `String` parameter — rather than a `BuildContext`/`AppLocalizations`
/// dependency — keeps this function pure and unit-testable without a
/// widget tree, mirroring `pantalla_reproductor.dart`'s
/// `_formatearDuracion(AppLocalizations l10n, ...)` precedent, minus the
/// Flutter-generated-class coupling.
String nombreCarpetaDesdeUri(String treeUri, {required String nombreGenerico}) {
final uri = Uri.tryParse(treeUri);
if (uri == null) return nombreGenerico;
final segmentos = uri.pathSegments;
final indiceTree = segmentos.indexOf('tree');
if (indiceTree == -1 || indiceTree + 1 >= segmentos.length) {
return nombreGenerico;
}
final documentId = segmentos[indiceTree + 1];
if (documentId.isEmpty) return nombreGenerico;
final String segmento;
final ultimaBarra = documentId.lastIndexOf('/');
if (ultimaBarra >= 0) {
segmento = documentId.substring(ultimaBarra + 1);
} else {
final ultimosDosPuntos = documentId.lastIndexOf(':');
segmento = ultimosDosPuntos >= 0
? documentId.substring(ultimosDosPuntos + 1)
: documentId;
}
final recortado = segmento.trim();
return recortado.isEmpty ? nombreGenerico : recortado;
}
/// Browse-source abstraction for the local-music branch of the Android Auto
/// tree (Design "Interfaces / Contracts"), mirroring [FuenteEmisorasAuto]'s
/// (`navegacion_auto.dart`) cold-start-safe, never-throws contract. Kept as