fix(auto): sort local-music subfolders before files
itemsLocales sorted a folder's children by name only, mixing directories and files. A subfolder whose name sorted after enough tracks (e.g. "Live" behind 80 numbered tracks) landed on a later "Más..." page, making it unreachable without paging through every track first. Sort directories before files, then by name within each group -- the standard file-browser convention. Subfolders now always land on page 0.
This commit is contained in:
@@ -30,6 +30,21 @@ List<T> paginaDe<T>(List<T> items, {required int pagina, required int tamano}) =
|
||||
bool hayPaginaSiguiente(int total, {required int pagina, required int tamano}) =>
|
||||
total > (pagina + 1) * tamano;
|
||||
|
||||
/// Browse-tree ordering comparator for a local-music folder's children
|
||||
/// (Design "Directories before files", item 1): directories sort before
|
||||
/// files regardless of name, and within each group, alphabetically by
|
||||
/// [NodoLocal.nombre] -- the standard file-browser convention. Fixes a
|
||||
/// driver-facing bug where a folder's subfolders could land on a later
|
||||
/// "Más…" page whenever enough tracks sorted alphabetically ahead of them
|
||||
/// (e.g. a "Live" subfolder behind 80 numbered tracks), making the
|
||||
/// subfolder unreachable without paging through every track first.
|
||||
int compararNodoLocalParaNavegacion(NodoLocal a, NodoLocal b) {
|
||||
if (a.esDirectorio != b.esDirectorio) {
|
||||
return a.esDirectorio ? -1 : 1;
|
||||
}
|
||||
return a.nombre.compareTo(b.nombre);
|
||||
}
|
||||
|
||||
const _prefijoEmisora = 'emisora:';
|
||||
|
||||
/// Local-track media-id prefix (Design "media-id scheme"), collision-free
|
||||
@@ -434,7 +449,7 @@ class ConstructorArbolAuto {
|
||||
MediaItem Function(NodoLocal, Map<String, MetadatosPista>)? construirItem,
|
||||
}) async {
|
||||
final construir = construirItem ?? _itemLocal;
|
||||
final ordenados = [...nodos]..sort((a, b) => a.nombre.compareTo(b.nombre));
|
||||
final ordenados = [...nodos]..sort(compararNodoLocalParaNavegacion);
|
||||
final paginaActual = paginaDe(ordenados, pagina: pagina, tamano: tamano);
|
||||
final docIds = paginaActual
|
||||
.where((n) => !n.esDirectorio)
|
||||
|
||||
@@ -2829,6 +2829,177 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ── Item 1: Android Auto subfolder visibility ───────────────────────────
|
||||
// `itemsLocales` used to sort a folder's children by name ONLY, mixing
|
||||
// directories and files -- a subfolder whose name sorted alphabetically
|
||||
// after enough tracks landed on a later "Más…" page, making it invisible
|
||||
// until the driver paged through every track first. Fix: directories
|
||||
// before files, then by name within each group (standard file-browser
|
||||
// convention) -- subfolders always land on the first page.
|
||||
group(
|
||||
'compararNodoLocalParaNavegacion (Design "Directories before files")',
|
||||
() {
|
||||
test('a directory always sorts before a file, regardless of name', () {
|
||||
const archivo = NodoLocal(
|
||||
documentId: 'f1',
|
||||
nombre: 'AAA archivo.mp3',
|
||||
esDirectorio: false,
|
||||
);
|
||||
const carpeta = NodoLocal(
|
||||
documentId: 'd1',
|
||||
nombre: 'ZZZ carpeta',
|
||||
esDirectorio: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
compararNodoLocalParaNavegacion(carpeta, archivo),
|
||||
lessThan(0),
|
||||
);
|
||||
expect(
|
||||
compararNodoLocalParaNavegacion(archivo, carpeta),
|
||||
greaterThan(0),
|
||||
);
|
||||
});
|
||||
|
||||
test('two directories sort alphabetically by name', () {
|
||||
const a = NodoLocal(
|
||||
documentId: 'd1',
|
||||
nombre: 'Alpha',
|
||||
esDirectorio: true,
|
||||
);
|
||||
const b = NodoLocal(
|
||||
documentId: 'd2',
|
||||
nombre: 'Beta',
|
||||
esDirectorio: true,
|
||||
);
|
||||
|
||||
expect(compararNodoLocalParaNavegacion(a, b), lessThan(0));
|
||||
expect(compararNodoLocalParaNavegacion(b, a), greaterThan(0));
|
||||
});
|
||||
|
||||
test('two files sort alphabetically by name', () {
|
||||
const a = NodoLocal(
|
||||
documentId: 'f1',
|
||||
nombre: 'Alpha.mp3',
|
||||
esDirectorio: false,
|
||||
);
|
||||
const b = NodoLocal(
|
||||
documentId: 'f2',
|
||||
nombre: 'Beta.mp3',
|
||||
esDirectorio: false,
|
||||
);
|
||||
|
||||
expect(compararNodoLocalParaNavegacion(a, b), lessThan(0));
|
||||
});
|
||||
|
||||
test('equal esDirectorio and equal name compares equal', () {
|
||||
const a = NodoLocal(
|
||||
documentId: 'f1',
|
||||
nombre: 'Same.mp3',
|
||||
esDirectorio: false,
|
||||
);
|
||||
const b = NodoLocal(
|
||||
documentId: 'f2',
|
||||
nombre: 'Same.mp3',
|
||||
esDirectorio: false,
|
||||
);
|
||||
|
||||
expect(compararNodoLocalParaNavegacion(a, b), 0);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'ConstructorArbolAuto.itemsLocales: subfolders sort before files '
|
||||
'(Android Auto subfolder-visibility bug)',
|
||||
() {
|
||||
test(
|
||||
'a subfolder whose name sorts AFTER every one of 80 numbered '
|
||||
'tracks (e.g. "Live") still lands on page 0 -- pure alphabetical '
|
||||
'mixing used to push it to a later page, making it unreachable '
|
||||
'without paging through every track first (the reported bug)',
|
||||
() async {
|
||||
final nodos = [
|
||||
for (var i = 1; i <= 80; i++)
|
||||
NodoLocal(
|
||||
documentId: 'track-$i',
|
||||
nombre: '${i.toString().padLeft(3, '0')} Track.mp3',
|
||||
esDirectorio: false,
|
||||
),
|
||||
const NodoLocal(
|
||||
documentId: 'folder-live',
|
||||
nombre: 'Live',
|
||||
esDirectorio: true,
|
||||
),
|
||||
];
|
||||
|
||||
final pagina0 = await ConstructorArbolAuto().itemsLocales(
|
||||
nodos,
|
||||
documentIdPadre: 'root',
|
||||
metadatosDe: _metadatosVacio,
|
||||
);
|
||||
|
||||
expect(
|
||||
pagina0.where((i) => i.id == 'carpeta_local:folder-live'),
|
||||
hasLength(1),
|
||||
reason:
|
||||
'the "Live" subfolder must be visible on the FIRST page, '
|
||||
'not hidden behind 80 tracks on a later page',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'mixed directories and files: ALL directories precede ALL files, '
|
||||
'each group still alphabetical by name',
|
||||
() async {
|
||||
final nodos = [
|
||||
const NodoLocal(
|
||||
documentId: 'f-z',
|
||||
nombre: 'zzz.mp3',
|
||||
esDirectorio: false,
|
||||
),
|
||||
const NodoLocal(
|
||||
documentId: 'd-z',
|
||||
nombre: 'ZZZ Carpeta',
|
||||
esDirectorio: true,
|
||||
),
|
||||
const NodoLocal(
|
||||
documentId: 'f-a',
|
||||
nombre: 'aaa.mp3',
|
||||
esDirectorio: false,
|
||||
),
|
||||
const NodoLocal(
|
||||
documentId: 'd-a',
|
||||
nombre: 'AAA Carpeta',
|
||||
esDirectorio: true,
|
||||
),
|
||||
];
|
||||
|
||||
final items = await ConstructorArbolAuto().itemsLocales(
|
||||
nodos,
|
||||
documentIdPadre: 'root',
|
||||
metadatosDe: _metadatosVacio,
|
||||
);
|
||||
final soloArbol = items
|
||||
.where(
|
||||
(i) =>
|
||||
i.id.startsWith('carpeta_local:') ||
|
||||
i.id.startsWith('pista:'),
|
||||
)
|
||||
.toList();
|
||||
|
||||
expect(soloArbol.map((i) => i.id).toList(), [
|
||||
'carpeta_local:d-a',
|
||||
'carpeta_local:d-z',
|
||||
'pista:f-a',
|
||||
'pista:f-z',
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Fake `metadatosDe` that always resolves to an empty map — used by every
|
||||
|
||||
Reference in New Issue
Block a user