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}) =>
|
bool hayPaginaSiguiente(int total, {required int pagina, required int tamano}) =>
|
||||||
total > (pagina + 1) * 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:';
|
const _prefijoEmisora = 'emisora:';
|
||||||
|
|
||||||
/// Local-track media-id prefix (Design "media-id scheme"), collision-free
|
/// Local-track media-id prefix (Design "media-id scheme"), collision-free
|
||||||
@@ -434,7 +449,7 @@ class ConstructorArbolAuto {
|
|||||||
MediaItem Function(NodoLocal, Map<String, MetadatosPista>)? construirItem,
|
MediaItem Function(NodoLocal, Map<String, MetadatosPista>)? construirItem,
|
||||||
}) async {
|
}) async {
|
||||||
final construir = construirItem ?? _itemLocal;
|
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 paginaActual = paginaDe(ordenados, pagina: pagina, tamano: tamano);
|
||||||
final docIds = paginaActual
|
final docIds = paginaActual
|
||||||
.where((n) => !n.esDirectorio)
|
.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
|
/// Fake `metadatosDe` that always resolves to an empty map — used by every
|
||||||
|
|||||||
Reference in New Issue
Block a user