mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
add library sort ordering
This commit is contained in:
+225
@@ -645,6 +645,212 @@ class RoomLibraryRepositoryTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun descendingTitlePagesAndAnchorsRemainGapFree() = runBlocking {
|
||||||
|
publish("g1", seedAlphabet())
|
||||||
|
val dao = catalog.catalogDao()
|
||||||
|
val expected = dao.getTitlePage(null, "", ALPHABET_SEED_SIZE)
|
||||||
|
.sortedWith(compareByDescending<ActiveTrackView> { it.titleSortKey }.thenBy { it.path })
|
||||||
|
|
||||||
|
val paged = mutableListOf<ActiveTrackView>()
|
||||||
|
var afterKey: String? = null
|
||||||
|
var afterPath = ""
|
||||||
|
while (true) {
|
||||||
|
val page = dao.getTitlePageDescending(afterKey, afterPath, 37)
|
||||||
|
if (page.isEmpty()) break
|
||||||
|
paged += page
|
||||||
|
afterKey = page.last().titleSortKey
|
||||||
|
afterPath = page.last().path
|
||||||
|
}
|
||||||
|
assertEquals(expected.map { it.path }, paged.map { it.path })
|
||||||
|
|
||||||
|
val anchor = dao.getTitleSectionAnchorsDescending().first { it.sectionLabel == "F" }
|
||||||
|
val at = dao.getTitlePageDescending(anchor.sortKey, "", 40)
|
||||||
|
val above = dao.getTitlePageBeforeDescending(anchor.sortKey, "", 40)
|
||||||
|
assertEquals("F", SortKeys.sectionLabel(at.first().title))
|
||||||
|
assertTrue(above.all { SortKeys.sectionLabel(it.title) != "F" })
|
||||||
|
val anchorIndex = expected.indexOfFirst { it.path == at.first().path }
|
||||||
|
assertEquals(
|
||||||
|
expected.subList(anchorIndex - above.size, anchorIndex).map { it.path },
|
||||||
|
above.reversed().map { it.path },
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
dao.getTitlePageBeforeDescending(expected.first().titleSortKey, expected.first().path, 40)
|
||||||
|
.isEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trackDirectionsReverseOnlyThePrimaryField() = runBlocking {
|
||||||
|
val rows = listOf(
|
||||||
|
track("g1", 1, "Zulu Beta").copy(
|
||||||
|
artist = "Zulu",
|
||||||
|
artistSortKey = SortKeys.forText("Zulu"),
|
||||||
|
album = "Beta",
|
||||||
|
albumSortKey = SortKeys.forText("Beta"),
|
||||||
|
addedAt = 30,
|
||||||
|
duration = 300.0,
|
||||||
|
),
|
||||||
|
track("g1", 2, "Zulu Alpha").copy(
|
||||||
|
artist = "Zulu",
|
||||||
|
artistSortKey = SortKeys.forText("Zulu"),
|
||||||
|
album = "Alpha",
|
||||||
|
albumSortKey = SortKeys.forText("Alpha"),
|
||||||
|
addedAt = 10,
|
||||||
|
duration = 100.0,
|
||||||
|
),
|
||||||
|
track("g1", 3, "Alpha Gamma").copy(
|
||||||
|
artist = "Alpha",
|
||||||
|
artistSortKey = SortKeys.forText("Alpha"),
|
||||||
|
album = "Gamma",
|
||||||
|
albumSortKey = SortKeys.forText("Gamma"),
|
||||||
|
addedAt = 20,
|
||||||
|
duration = 200.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
publish("g1", rows)
|
||||||
|
val dao = catalog.catalogDao()
|
||||||
|
assertEquals(
|
||||||
|
listOf(rows[1].path, rows[0].path, rows[2].path),
|
||||||
|
dao.getArtistOrderPageDescending(null, "", 0, 0, "", "", 10).map { it.path },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(rows[1].path, rows[2].path, rows[0].path),
|
||||||
|
dao.getRecentlyAddedPageAscending(null, "", 10).map { it.path },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(rows[1].path, rows[2].path, rows[0].path),
|
||||||
|
dao.getDurationPageAscending(null, "", 10).map { it.path },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
dao.getArtistOrderPageDescending(null, "", 0, 0, "", "", 10).map { it.path },
|
||||||
|
dao.getAllPathsByArtistDescending(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
dao.getTitlePageDescending(null, "", 10).map { it.path },
|
||||||
|
dao.getAllPathsByTitleDescending(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
dao.getRecentlyAddedPageAscending(null, "", 10).map { it.path },
|
||||||
|
dao.getAllPathsByRecentlyAddedAscending(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
dao.getDurationPageAscending(null, "", 10).map { it.path },
|
||||||
|
dao.getAllPathsByDurationAscending(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun albumAndArtistSummaryQueriesSupportBothDirections() = runBlocking {
|
||||||
|
val dao = catalog.catalogDao()
|
||||||
|
val revision = 7L
|
||||||
|
dao.putAlbumSummaries(
|
||||||
|
listOf(
|
||||||
|
AlbumSummaryEntity(
|
||||||
|
revision = revision,
|
||||||
|
identityKey = "z-beta",
|
||||||
|
album = "Beta",
|
||||||
|
artist = "Zulu",
|
||||||
|
year = 2020,
|
||||||
|
trackCount = 1,
|
||||||
|
totalDuration = 1.0,
|
||||||
|
latestAddedAt = 30,
|
||||||
|
nameSortKey = SortKeys.forText("Beta"),
|
||||||
|
artistSortKey = SortKeys.forText("Zulu"),
|
||||||
|
sectionLabel = "B",
|
||||||
|
isSingle = false,
|
||||||
|
),
|
||||||
|
AlbumSummaryEntity(
|
||||||
|
revision = revision,
|
||||||
|
identityKey = "z-alpha",
|
||||||
|
album = "Alpha",
|
||||||
|
artist = "Zulu",
|
||||||
|
year = null,
|
||||||
|
trackCount = 1,
|
||||||
|
totalDuration = 1.0,
|
||||||
|
latestAddedAt = 10,
|
||||||
|
nameSortKey = SortKeys.forText("Alpha"),
|
||||||
|
artistSortKey = SortKeys.forText("Zulu"),
|
||||||
|
sectionLabel = "A",
|
||||||
|
isSingle = false,
|
||||||
|
),
|
||||||
|
AlbumSummaryEntity(
|
||||||
|
revision = revision,
|
||||||
|
identityKey = "a-gamma",
|
||||||
|
album = "Gamma",
|
||||||
|
artist = "Alpha",
|
||||||
|
year = 1990,
|
||||||
|
trackCount = 1,
|
||||||
|
totalDuration = 1.0,
|
||||||
|
latestAddedAt = 20,
|
||||||
|
nameSortKey = SortKeys.forText("Gamma"),
|
||||||
|
artistSortKey = SortKeys.forText("Alpha"),
|
||||||
|
sectionLabel = "G",
|
||||||
|
isSingle = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("z-alpha", "z-beta", "a-gamma"),
|
||||||
|
dao.getAlbumArtistPageDescending(revision, true, null, "", "", 10)
|
||||||
|
.map { it.identityKey },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("a-gamma", "z-beta", "z-alpha"),
|
||||||
|
dao.getAlbumNamePageDescending(revision, true, null, "", 10)
|
||||||
|
.map { it.identityKey },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("a-gamma"),
|
||||||
|
dao.getAlbumNamePageBeforeDescending(
|
||||||
|
revision,
|
||||||
|
true,
|
||||||
|
SortKeys.forText("Beta"),
|
||||||
|
"z-beta",
|
||||||
|
10,
|
||||||
|
).map { it.identityKey },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("a-gamma", "z-beta", "z-alpha"),
|
||||||
|
dao.getAlbumYearPageAscending(revision, true, 0, null, "", "", 10)
|
||||||
|
.map { it.identityKey },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("z-alpha", "a-gamma", "z-beta"),
|
||||||
|
dao.getAlbumRecentPageAscending(revision, true, null, "", 10)
|
||||||
|
.map { it.identityKey },
|
||||||
|
)
|
||||||
|
|
||||||
|
dao.putArtistSummaries(
|
||||||
|
listOf(
|
||||||
|
artistSummary(revision, "zulu", "Zulu", 2),
|
||||||
|
artistSummary(revision, "alpha", "Alpha", 2),
|
||||||
|
artistSummary(revision, "beta", "Beta", 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("Zulu", "Beta", "Alpha"),
|
||||||
|
dao.getArtistNamePageDescending(revision, "astra", true, null, "", 10)
|
||||||
|
.map { it.artist },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("Zulu"),
|
||||||
|
dao.getArtistNamePageBeforeDescending(
|
||||||
|
revision,
|
||||||
|
"astra",
|
||||||
|
true,
|
||||||
|
SortKeys.forText("Beta"),
|
||||||
|
"beta",
|
||||||
|
10,
|
||||||
|
).map { it.artist },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("Beta", "Alpha", "Zulu"),
|
||||||
|
dao.getArtistCountPageAscending(revision, "astra", true, null, "", "", 10)
|
||||||
|
.map { it.artist },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun structuredArtistCreditsPreserveNamesContainingPunctuation() = runBlocking {
|
fun structuredArtistCreditsPreserveNamesContainingPunctuation() = runBlocking {
|
||||||
val artistNames = listOf("Earth, Wind & Fire", "The Emotions")
|
val artistNames = listOf("Earth, Wind & Fire", "The Emotions")
|
||||||
@@ -965,6 +1171,25 @@ class RoomLibraryRepositoryTest {
|
|||||||
sectionLabel = SortKeys.sectionLabel(title),
|
sectionLabel = SortKeys.sectionLabel(title),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun artistSummary(
|
||||||
|
revision: Long,
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
count: Long,
|
||||||
|
): ArtistSummaryEntity = ArtistSummaryEntity(
|
||||||
|
revision = revision,
|
||||||
|
artistKey = key,
|
||||||
|
artist = name,
|
||||||
|
groupingMode = "astra",
|
||||||
|
trackCount = count,
|
||||||
|
primaryTrackCount = count,
|
||||||
|
albumCount = 1,
|
||||||
|
nameSortKey = SortKeys.forText(name),
|
||||||
|
sectionLabel = SortKeys.sectionLabel(name),
|
||||||
|
isCollaboration = false,
|
||||||
|
artworkHashesJson = "[]",
|
||||||
|
)
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/** 26 letters x 10 tracks. */
|
/** 26 letters x 10 tracks. */
|
||||||
const val ALPHABET_SEED_SIZE = 260
|
const val ALPHABET_SEED_SIZE = 260
|
||||||
|
|||||||
+21
-6
@@ -81,11 +81,12 @@ class AstraLibraryDataModule : Module() {
|
|||||||
|
|
||||||
AsyncFunction("getTrackPage") Coroutine {
|
AsyncFunction("getTrackPage") Coroutine {
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
cursor: String?,
|
cursor: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
->
|
->
|
||||||
try {
|
try {
|
||||||
repository().getTrackPage(sort, cursor, limit)
|
repository().getTrackPage(sort, direction, cursor, limit)
|
||||||
} catch (_: StaleRevisionException) {
|
} catch (_: StaleRevisionException) {
|
||||||
mapOf("error" to "STALE_REVISION")
|
mapOf("error" to "STALE_REVISION")
|
||||||
}
|
}
|
||||||
@@ -93,11 +94,12 @@ class AstraLibraryDataModule : Module() {
|
|||||||
|
|
||||||
AsyncFunction("getTrackPageBefore") Coroutine {
|
AsyncFunction("getTrackPageBefore") Coroutine {
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
cursor: String?,
|
cursor: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
->
|
->
|
||||||
try {
|
try {
|
||||||
repository().getTrackPageBefore(sort, cursor, limit)
|
repository().getTrackPageBefore(sort, direction, cursor, limit)
|
||||||
} catch (_: StaleRevisionException) {
|
} catch (_: StaleRevisionException) {
|
||||||
mapOf("error" to "STALE_REVISION")
|
mapOf("error" to "STALE_REVISION")
|
||||||
}
|
}
|
||||||
@@ -385,12 +387,13 @@ class AstraLibraryDataModule : Module() {
|
|||||||
|
|
||||||
AsyncFunction("getAlbumPage") Coroutine {
|
AsyncFunction("getAlbumPage") Coroutine {
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
cursor: String?,
|
cursor: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
->
|
->
|
||||||
try {
|
try {
|
||||||
repository().getAlbumPage(sort, includeSingles, cursor, limit)
|
repository().getAlbumPage(sort, direction, includeSingles, cursor, limit)
|
||||||
} catch (_: StaleRevisionException) {
|
} catch (_: StaleRevisionException) {
|
||||||
mapOf("error" to "STALE_REVISION")
|
mapOf("error" to "STALE_REVISION")
|
||||||
}
|
}
|
||||||
@@ -398,12 +401,13 @@ class AstraLibraryDataModule : Module() {
|
|||||||
|
|
||||||
AsyncFunction("getAlbumPageBefore") Coroutine {
|
AsyncFunction("getAlbumPageBefore") Coroutine {
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
cursor: String?,
|
cursor: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
->
|
->
|
||||||
try {
|
try {
|
||||||
repository().getAlbumPageBefore(sort, includeSingles, cursor, limit)
|
repository().getAlbumPageBefore(sort, direction, includeSingles, cursor, limit)
|
||||||
} catch (_: StaleRevisionException) {
|
} catch (_: StaleRevisionException) {
|
||||||
mapOf("error" to "STALE_REVISION")
|
mapOf("error" to "STALE_REVISION")
|
||||||
}
|
}
|
||||||
@@ -411,13 +415,14 @@ class AstraLibraryDataModule : Module() {
|
|||||||
|
|
||||||
AsyncFunction("getArtistPage") Coroutine {
|
AsyncFunction("getArtistPage") Coroutine {
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
groupingMode: String,
|
groupingMode: String,
|
||||||
includeCollaborations: Boolean,
|
includeCollaborations: Boolean,
|
||||||
cursor: String?,
|
cursor: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
->
|
->
|
||||||
try {
|
try {
|
||||||
repository().getArtistPage(sort, groupingMode, includeCollaborations, cursor, limit)
|
repository().getArtistPage(sort, direction, groupingMode, includeCollaborations, cursor, limit)
|
||||||
} catch (_: StaleRevisionException) {
|
} catch (_: StaleRevisionException) {
|
||||||
mapOf("error" to "STALE_REVISION")
|
mapOf("error" to "STALE_REVISION")
|
||||||
}
|
}
|
||||||
@@ -425,13 +430,21 @@ class AstraLibraryDataModule : Module() {
|
|||||||
|
|
||||||
AsyncFunction("getArtistPageBefore") Coroutine {
|
AsyncFunction("getArtistPageBefore") Coroutine {
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
groupingMode: String,
|
groupingMode: String,
|
||||||
includeCollaborations: Boolean,
|
includeCollaborations: Boolean,
|
||||||
cursor: String?,
|
cursor: String?,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
->
|
->
|
||||||
try {
|
try {
|
||||||
repository().getArtistPageBefore(sort, groupingMode, includeCollaborations, cursor, limit)
|
repository().getArtistPageBefore(
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
groupingMode,
|
||||||
|
includeCollaborations,
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
} catch (_: StaleRevisionException) {
|
} catch (_: StaleRevisionException) {
|
||||||
mapOf("error" to "STALE_REVISION")
|
mapOf("error" to "STALE_REVISION")
|
||||||
}
|
}
|
||||||
@@ -560,6 +573,7 @@ class AstraLibraryDataModule : Module() {
|
|||||||
AsyncFunction("getSectionAnchors") Coroutine {
|
AsyncFunction("getSectionAnchors") Coroutine {
|
||||||
kind: String,
|
kind: String,
|
||||||
sort: String,
|
sort: String,
|
||||||
|
direction: String,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
groupingMode: String,
|
groupingMode: String,
|
||||||
includeCollaborations: Boolean,
|
includeCollaborations: Boolean,
|
||||||
@@ -567,6 +581,7 @@ class AstraLibraryDataModule : Module() {
|
|||||||
repository().getSectionAnchors(
|
repository().getSectionAnchors(
|
||||||
kind,
|
kind,
|
||||||
sort,
|
sort,
|
||||||
|
direction,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
groupingMode,
|
groupingMode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
|
|||||||
+178
-71
@@ -635,41 +635,43 @@ class AstraLibraryRepository private constructor(
|
|||||||
|
|
||||||
suspend fun getTrackPage(
|
suspend fun getTrackPage(
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
cursorRaw: String?,
|
cursorRaw: String?,
|
||||||
requestedLimit: Int,
|
requestedLimit: Int,
|
||||||
): Map<String, Any?> = withCatalogRecovery { database ->
|
): Map<String, Any?> = withCatalogRecovery { database ->
|
||||||
initialize()
|
initialize()
|
||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
val cursor = validateCursor(cursorRaw, revision, "tracks:$sort")
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
|
val cursor = validateCursor(cursorRaw, revision, "tracks:$sort:$direction")
|
||||||
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
||||||
val rows = when (sort) {
|
val rows = when (sort) {
|
||||||
"artist" -> dao.getArtistOrderPage(
|
"artist" -> (if (direction == "desc") dao::getArtistOrderPageDescending else dao::getArtistOrderPage)(
|
||||||
afterArtistKey = cursor?.text1,
|
cursor?.text1,
|
||||||
afterAlbumKey = cursor?.text2.orEmpty(),
|
cursor?.text2.orEmpty(),
|
||||||
afterDisc = cursor?.number1?.toInt() ?: 0,
|
cursor?.number1?.toInt() ?: 0,
|
||||||
afterTrack = cursor?.number2?.toInt() ?: 0,
|
cursor?.number2?.toInt() ?: 0,
|
||||||
afterTitleKey = cursor?.let(::cursorTitleKey).orEmpty(),
|
cursor?.let(::cursorTitleKey).orEmpty(),
|
||||||
afterPath = cursor?.let { cursorPath(it) }.orEmpty(),
|
cursor?.let { cursorPath(it) }.orEmpty(),
|
||||||
limit = limit,
|
limit,
|
||||||
)
|
)
|
||||||
"recently_added" -> dao.getRecentlyAddedPage(
|
"recently_added" -> (if (direction == "asc") dao::getRecentlyAddedPageAscending else dao::getRecentlyAddedPage)(
|
||||||
afterAddedAt = cursor?.number1,
|
cursor?.number1,
|
||||||
afterPath = cursor?.text1.orEmpty(),
|
cursor?.text1.orEmpty(),
|
||||||
limit = limit,
|
limit,
|
||||||
)
|
)
|
||||||
"duration" -> dao.getDurationPage(
|
"duration" -> (if (direction == "asc") dao::getDurationPageAscending else dao::getDurationPage)(
|
||||||
afterDuration = cursor?.decimal1,
|
cursor?.decimal1,
|
||||||
afterPath = cursor?.text1.orEmpty(),
|
cursor?.text1.orEmpty(),
|
||||||
limit = limit,
|
limit,
|
||||||
)
|
)
|
||||||
else -> dao.getTitlePage(
|
else -> (if (direction == "desc") dao::getTitlePageDescending else dao::getTitlePage)(
|
||||||
afterTitleKey = cursor?.text1,
|
cursor?.text1,
|
||||||
afterPath = cursor?.text2.orEmpty(),
|
cursor?.text2.orEmpty(),
|
||||||
limit = limit,
|
limit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val next = rows.lastOrNull()?.let { row -> trackCursor(revision, sort, row).encode() }
|
val next = rows.lastOrNull()?.let { row -> trackCursor(revision, sort, direction, row).encode() }
|
||||||
mapOf(
|
mapOf(
|
||||||
"items" to rows.map(ActiveTrackView::toBridgeMap),
|
"items" to rows.map(ActiveTrackView::toBridgeMap),
|
||||||
"nextCursor" to next,
|
"nextCursor" to next,
|
||||||
@@ -693,36 +695,46 @@ class AstraLibraryRepository private constructor(
|
|||||||
*/
|
*/
|
||||||
suspend fun getTrackPageBefore(
|
suspend fun getTrackPageBefore(
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
cursorRaw: String?,
|
cursorRaw: String?,
|
||||||
requestedLimit: Int,
|
requestedLimit: Int,
|
||||||
): Map<String, Any?> = withCatalogRecovery { database ->
|
): Map<String, Any?> = withCatalogRecovery { database ->
|
||||||
initialize()
|
initialize()
|
||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
val cursor = validateCursor(cursorRaw, revision, "tracks:$sort")
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
|
val cursor = validateCursor(cursorRaw, revision, "tracks:$sort:$direction")
|
||||||
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
||||||
val descending = when {
|
val descending = when {
|
||||||
cursor == null -> emptyList()
|
cursor == null -> emptyList()
|
||||||
sort == "artist" -> dao.getArtistOrderPageBefore(
|
sort == "artist" -> (if (direction == "desc") {
|
||||||
beforeArtistKey = cursor.text1.orEmpty(),
|
dao::getArtistOrderPageBeforeDescending
|
||||||
beforeAlbumKey = cursor.text2.orEmpty(),
|
} else {
|
||||||
beforeDisc = cursor.number1?.toInt() ?: 0,
|
dao::getArtistOrderPageBefore
|
||||||
beforeTrack = cursor.number2?.toInt() ?: 0,
|
})(
|
||||||
beforeTitleKey = cursorTitleKey(cursor),
|
cursor.text1.orEmpty(),
|
||||||
beforePath = cursorPath(cursor),
|
cursor.text2.orEmpty(),
|
||||||
limit = limit,
|
cursor.number1?.toInt() ?: 0,
|
||||||
|
cursor.number2?.toInt() ?: 0,
|
||||||
|
cursorTitleKey(cursor),
|
||||||
|
cursorPath(cursor),
|
||||||
|
limit,
|
||||||
)
|
)
|
||||||
sort == "title" -> dao.getTitlePageBefore(
|
sort == "title" -> (if (direction == "desc") {
|
||||||
beforeTitleKey = cursor.text1.orEmpty(),
|
dao::getTitlePageBeforeDescending
|
||||||
beforePath = cursor.text2.orEmpty(),
|
} else {
|
||||||
limit = limit,
|
dao::getTitlePageBefore
|
||||||
|
})(
|
||||||
|
cursor.text1.orEmpty(),
|
||||||
|
cursor.text2.orEmpty(),
|
||||||
|
limit,
|
||||||
)
|
)
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
// The DESC result's last row is the topmost one — the cursor for the page above this one.
|
// The DESC result's last row is the topmost one — the cursor for the page above this one.
|
||||||
val previous = descending.takeIf { it.size == limit }
|
val previous = descending.takeIf { it.size == limit }
|
||||||
?.lastOrNull()
|
?.lastOrNull()
|
||||||
?.let { row -> trackCursor(revision, sort, row).encode() }
|
?.let { row -> trackCursor(revision, sort, direction, row).encode() }
|
||||||
mapOf(
|
mapOf(
|
||||||
"items" to descending.reversed().map(ActiveTrackView::toBridgeMap),
|
"items" to descending.reversed().map(ActiveTrackView::toBridgeMap),
|
||||||
"nextCursor" to null,
|
"nextCursor" to null,
|
||||||
@@ -733,11 +745,16 @@ class AstraLibraryRepository private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Shared by the forward and backward track pages so the two can never disagree. */
|
/** Shared by the forward and backward track pages so the two can never disagree. */
|
||||||
private fun trackCursor(revision: Long, sort: String, row: ActiveTrackView): TrackPageCursor =
|
private fun trackCursor(
|
||||||
|
revision: Long,
|
||||||
|
sort: String,
|
||||||
|
direction: String,
|
||||||
|
row: ActiveTrackView,
|
||||||
|
): TrackPageCursor =
|
||||||
when (sort) {
|
when (sort) {
|
||||||
"artist" -> TrackPageCursor(
|
"artist" -> TrackPageCursor(
|
||||||
revision = revision,
|
revision = revision,
|
||||||
kind = "tracks:$sort",
|
kind = "tracks:$sort:$direction",
|
||||||
text1 = row.artistSortKey,
|
text1 = row.artistSortKey,
|
||||||
text2 = row.albumSortKey,
|
text2 = row.albumSortKey,
|
||||||
text3 = "${row.titleSortKey}\u0000${row.path}",
|
text3 = "${row.titleSortKey}\u0000${row.path}",
|
||||||
@@ -748,19 +765,19 @@ class AstraLibraryRepository private constructor(
|
|||||||
)
|
)
|
||||||
"recently_added" -> TrackPageCursor(
|
"recently_added" -> TrackPageCursor(
|
||||||
revision = revision,
|
revision = revision,
|
||||||
kind = "tracks:$sort",
|
kind = "tracks:$sort:$direction",
|
||||||
text1 = row.path,
|
text1 = row.path,
|
||||||
number1 = row.addedAt,
|
number1 = row.addedAt,
|
||||||
)
|
)
|
||||||
"duration" -> TrackPageCursor(
|
"duration" -> TrackPageCursor(
|
||||||
revision = revision,
|
revision = revision,
|
||||||
kind = "tracks:$sort",
|
kind = "tracks:$sort:$direction",
|
||||||
text1 = row.path,
|
text1 = row.path,
|
||||||
decimal1 = row.duration,
|
decimal1 = row.duration,
|
||||||
)
|
)
|
||||||
else -> TrackPageCursor(
|
else -> TrackPageCursor(
|
||||||
revision = revision,
|
revision = revision,
|
||||||
kind = "tracks:$sort",
|
kind = "tracks:$sort:$direction",
|
||||||
text1 = row.titleSortKey,
|
text1 = row.titleSortKey,
|
||||||
text2 = row.path,
|
text2 = row.path,
|
||||||
)
|
)
|
||||||
@@ -1882,17 +1899,19 @@ class AstraLibraryRepository private constructor(
|
|||||||
|
|
||||||
suspend fun getAlbumPage(
|
suspend fun getAlbumPage(
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
cursorRaw: String?,
|
cursorRaw: String?,
|
||||||
requestedLimit: Int,
|
requestedLimit: Int,
|
||||||
): Map<String, Any?> = withCatalogRecovery { database ->
|
): Map<String, Any?> = withCatalogRecovery { database ->
|
||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
val kind = "albums:$sort:${if (includeSingles) 1 else 0}"
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
|
val kind = "albums:$sort:$direction:${if (includeSingles) 1 else 0}"
|
||||||
val cursor = validateCursor(cursorRaw, revision, kind)
|
val cursor = validateCursor(cursorRaw, revision, kind)
|
||||||
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
||||||
val rows = when (sort) {
|
val rows = when (sort) {
|
||||||
"artist" -> dao.getAlbumArtistPage(
|
"artist" -> (if (direction == "desc") dao::getAlbumArtistPageDescending else dao::getAlbumArtistPage)(
|
||||||
revision,
|
revision,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor?.text1,
|
cursor?.text1,
|
||||||
@@ -1900,22 +1919,23 @@ class AstraLibraryRepository private constructor(
|
|||||||
cursor?.text3.orEmpty(),
|
cursor?.text3.orEmpty(),
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
"recently_added" -> dao.getAlbumRecentPage(
|
"recently_added" -> (if (direction == "asc") dao::getAlbumRecentPageAscending else dao::getAlbumRecentPage)(
|
||||||
revision,
|
revision,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor?.number1,
|
cursor?.number1,
|
||||||
cursor?.text1.orEmpty(),
|
cursor?.text1.orEmpty(),
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
"year" -> dao.getAlbumYearPage(
|
"year" -> (if (direction == "asc") dao::getAlbumYearPageAscending else dao::getAlbumYearPage)(
|
||||||
revision,
|
revision,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor?.number1?.toInt(),
|
cursor?.number1?.toInt() ?: 0,
|
||||||
|
cursor?.number2?.toInt(),
|
||||||
cursor?.text1.orEmpty(),
|
cursor?.text1.orEmpty(),
|
||||||
cursor?.text2.orEmpty(),
|
cursor?.text2.orEmpty(),
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
else -> dao.getAlbumNamePage(
|
else -> (if (direction == "desc") dao::getAlbumNamePageDescending else dao::getAlbumNamePage)(
|
||||||
revision,
|
revision,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor?.text1,
|
cursor?.text1,
|
||||||
@@ -1936,18 +1956,24 @@ class AstraLibraryRepository private constructor(
|
|||||||
/** Backward twin of [getAlbumPage]; see [getTrackPageBefore] for the contract. */
|
/** Backward twin of [getAlbumPage]; see [getTrackPageBefore] for the contract. */
|
||||||
suspend fun getAlbumPageBefore(
|
suspend fun getAlbumPageBefore(
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
cursorRaw: String?,
|
cursorRaw: String?,
|
||||||
requestedLimit: Int,
|
requestedLimit: Int,
|
||||||
): Map<String, Any?> = withCatalogRecovery { database ->
|
): Map<String, Any?> = withCatalogRecovery { database ->
|
||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
val kind = "albums:$sort:${if (includeSingles) 1 else 0}"
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
|
val kind = "albums:$sort:$direction:${if (includeSingles) 1 else 0}"
|
||||||
val cursor = validateCursor(cursorRaw, revision, kind)
|
val cursor = validateCursor(cursorRaw, revision, kind)
|
||||||
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
||||||
val descending = when {
|
val descending = when {
|
||||||
cursor == null -> emptyList()
|
cursor == null -> emptyList()
|
||||||
sort == "artist" -> dao.getAlbumArtistPageBefore(
|
sort == "artist" -> (if (direction == "desc") {
|
||||||
|
dao::getAlbumArtistPageBeforeDescending
|
||||||
|
} else {
|
||||||
|
dao::getAlbumArtistPageBefore
|
||||||
|
})(
|
||||||
revision,
|
revision,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor.text1.orEmpty(),
|
cursor.text1.orEmpty(),
|
||||||
@@ -1955,7 +1981,11 @@ class AstraLibraryRepository private constructor(
|
|||||||
cursor.text3.orEmpty(),
|
cursor.text3.orEmpty(),
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
sort == "name" -> dao.getAlbumNamePageBefore(
|
sort == "name" -> (if (direction == "desc") {
|
||||||
|
dao::getAlbumNamePageBeforeDescending
|
||||||
|
} else {
|
||||||
|
dao::getAlbumNamePageBefore
|
||||||
|
})(
|
||||||
revision,
|
revision,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor.text1.orEmpty(),
|
cursor.text1.orEmpty(),
|
||||||
@@ -2003,6 +2033,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
text1 = row.nameSortKey,
|
text1 = row.nameSortKey,
|
||||||
text2 = row.identityKey,
|
text2 = row.identityKey,
|
||||||
number1 = (row.year ?: 0).toLong(),
|
number1 = (row.year ?: 0).toLong(),
|
||||||
|
number2 = if (row.year == null) 1 else 0,
|
||||||
)
|
)
|
||||||
else -> TrackPageCursor(
|
else -> TrackPageCursor(
|
||||||
revision,
|
revision,
|
||||||
@@ -2014,6 +2045,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
|
|
||||||
suspend fun getArtistPage(
|
suspend fun getArtistPage(
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
groupingMode: String,
|
groupingMode: String,
|
||||||
includeCollaborations: Boolean,
|
includeCollaborations: Boolean,
|
||||||
cursorRaw: String?,
|
cursorRaw: String?,
|
||||||
@@ -2022,11 +2054,12 @@ class AstraLibraryRepository private constructor(
|
|||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
val mode = if (groupingMode == "fileTags") "fileTags" else "astra"
|
val mode = if (groupingMode == "fileTags") "fileTags" else "astra"
|
||||||
val kind = "artists:$sort:$mode:${if (includeCollaborations) 1 else 0}"
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
|
val kind = "artists:$sort:$direction:$mode:${if (includeCollaborations) 1 else 0}"
|
||||||
val cursor = validateCursor(cursorRaw, revision, kind)
|
val cursor = validateCursor(cursorRaw, revision, kind)
|
||||||
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
||||||
val rows = if (sort == "track_count") {
|
val rows = if (sort == "track_count") {
|
||||||
dao.getArtistCountPage(
|
(if (direction == "asc") dao::getArtistCountPageAscending else dao::getArtistCountPage)(
|
||||||
revision,
|
revision,
|
||||||
mode,
|
mode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
@@ -2036,7 +2069,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
dao.getArtistNamePage(
|
(if (direction == "desc") dao::getArtistNamePageDescending else dao::getArtistNamePage)(
|
||||||
revision,
|
revision,
|
||||||
mode,
|
mode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
@@ -2058,6 +2091,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
/** Backward twin of [getArtistPage]; see [getTrackPageBefore] for the contract. */
|
/** Backward twin of [getArtistPage]; see [getTrackPageBefore] for the contract. */
|
||||||
suspend fun getArtistPageBefore(
|
suspend fun getArtistPageBefore(
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
groupingMode: String,
|
groupingMode: String,
|
||||||
includeCollaborations: Boolean,
|
includeCollaborations: Boolean,
|
||||||
cursorRaw: String?,
|
cursorRaw: String?,
|
||||||
@@ -2066,13 +2100,18 @@ class AstraLibraryRepository private constructor(
|
|||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
val mode = if (groupingMode == "fileTags") "fileTags" else "astra"
|
val mode = if (groupingMode == "fileTags") "fileTags" else "astra"
|
||||||
val kind = "artists:$sort:$mode:${if (includeCollaborations) 1 else 0}"
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
|
val kind = "artists:$sort:$direction:$mode:${if (includeCollaborations) 1 else 0}"
|
||||||
val cursor = validateCursor(cursorRaw, revision, kind)
|
val cursor = validateCursor(cursorRaw, revision, kind)
|
||||||
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
val limit = requestedLimit.coerceIn(1, MAX_PAGE_SIZE)
|
||||||
val descending = if (cursor == null || sort == "track_count") {
|
val descending = if (cursor == null || sort == "track_count") {
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
dao.getArtistNamePageBefore(
|
(if (direction == "desc") {
|
||||||
|
dao::getArtistNamePageBeforeDescending
|
||||||
|
} else {
|
||||||
|
dao::getArtistNamePageBefore
|
||||||
|
})(
|
||||||
revision,
|
revision,
|
||||||
mode,
|
mode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
@@ -2689,6 +2728,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
suspend fun getSectionAnchors(
|
suspend fun getSectionAnchors(
|
||||||
kind: String,
|
kind: String,
|
||||||
sort: String,
|
sort: String,
|
||||||
|
directionRaw: String,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
groupingMode: String,
|
groupingMode: String,
|
||||||
includeCollaborations: Boolean,
|
includeCollaborations: Boolean,
|
||||||
@@ -2696,6 +2736,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
withCatalogRecovery { database ->
|
withCatalogRecovery { database ->
|
||||||
val dao = database.catalogDao()
|
val dao = database.catalogDao()
|
||||||
val revision = dao.getRevision()
|
val revision = dao.getRevision()
|
||||||
|
val direction = normalizeSortDirection(directionRaw)
|
||||||
val anchors: List<Pair<String, TrackPageCursor>> = when (kind) {
|
val anchors: List<Pair<String, TrackPageCursor>> = when (kind) {
|
||||||
"albums" -> {
|
"albums" -> {
|
||||||
val rows = dao.getAllAlbumSummaries(revision).filter { includeSingles || !it.isSingle }
|
val rows = dao.getAllAlbumSummaries(revision).filter { includeSingles || !it.isSingle }
|
||||||
@@ -2703,17 +2744,32 @@ class AstraLibraryRepository private constructor(
|
|||||||
if (sort == "artist") SortKeys.sectionLabel(row.artist) else SortKeys.sectionLabel(row.album)
|
if (sort == "artist") SortKeys.sectionLabel(row.artist) else SortKeys.sectionLabel(row.album)
|
||||||
}.map { (label, section) ->
|
}.map { (label, section) ->
|
||||||
if (sort == "artist") {
|
if (sort == "artist") {
|
||||||
val first = section.minWith(compareBy<AlbumSummaryEntity>({ it.artistSortKey }, { it.nameSortKey }, { it.identityKey }))
|
val first = section.sortedWith(
|
||||||
|
if (direction == "desc") {
|
||||||
|
compareByDescending<AlbumSummaryEntity> { it.artistSortKey }
|
||||||
|
.thenBy { it.nameSortKey }
|
||||||
|
.thenBy { it.identityKey }
|
||||||
|
} else {
|
||||||
|
compareBy<AlbumSummaryEntity>({ it.artistSortKey }, { it.nameSortKey }, { it.identityKey })
|
||||||
|
},
|
||||||
|
).first()
|
||||||
label to TrackPageCursor(
|
label to TrackPageCursor(
|
||||||
revision,
|
revision,
|
||||||
"albums:artist:${if (includeSingles) 1 else 0}",
|
"albums:artist:$direction:${if (includeSingles) 1 else 0}",
|
||||||
text1 = first.artistSortKey,
|
text1 = first.artistSortKey,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val first = section.minWith(compareBy<AlbumSummaryEntity>({ it.nameSortKey }, { it.identityKey }))
|
val first = section.sortedWith(
|
||||||
|
if (direction == "desc") {
|
||||||
|
compareByDescending<AlbumSummaryEntity> { it.nameSortKey }
|
||||||
|
.thenBy { it.identityKey }
|
||||||
|
} else {
|
||||||
|
compareBy<AlbumSummaryEntity>({ it.nameSortKey }, { it.identityKey })
|
||||||
|
},
|
||||||
|
).first()
|
||||||
label to TrackPageCursor(
|
label to TrackPageCursor(
|
||||||
revision,
|
revision,
|
||||||
"albums:name:${if (includeSingles) 1 else 0}",
|
"albums:name:$direction:${if (includeSingles) 1 else 0}",
|
||||||
text1 = first.nameSortKey,
|
text1 = first.nameSortKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2725,10 +2781,17 @@ class AstraLibraryRepository private constructor(
|
|||||||
.filter { includeCollaborations || !it.isCollaboration }
|
.filter { includeCollaborations || !it.isCollaboration }
|
||||||
.groupBy { row -> SortKeys.sectionLabel(row.artist) }
|
.groupBy { row -> SortKeys.sectionLabel(row.artist) }
|
||||||
.map { (label, section) ->
|
.map { (label, section) ->
|
||||||
val first = section.minWith(compareBy<ArtistSummaryEntity>({ it.nameSortKey }, { it.artistKey }))
|
val first = section.sortedWith(
|
||||||
|
if (direction == "desc") {
|
||||||
|
compareByDescending<ArtistSummaryEntity> { it.nameSortKey }
|
||||||
|
.thenBy { it.artistKey }
|
||||||
|
} else {
|
||||||
|
compareBy<ArtistSummaryEntity>({ it.nameSortKey }, { it.artistKey })
|
||||||
|
},
|
||||||
|
).first()
|
||||||
label to TrackPageCursor(
|
label to TrackPageCursor(
|
||||||
revision,
|
revision,
|
||||||
"artists:name:$mode:${if (includeCollaborations) 1 else 0}",
|
"artists:name:$direction:$mode:${if (includeCollaborations) 1 else 0}",
|
||||||
text1 = first.nameSortKey,
|
text1 = first.nameSortKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2740,22 +2803,38 @@ class AstraLibraryRepository private constructor(
|
|||||||
.map { (label, section) ->
|
.map { (label, section) ->
|
||||||
label to TrackPageCursor(
|
label to TrackPageCursor(
|
||||||
revision,
|
revision,
|
||||||
"tracks:artist",
|
"tracks:artist:$direction",
|
||||||
text1 = section.minOf(ArtistSectionAnchorCandidate::sortKey),
|
text1 = if (direction == "desc") {
|
||||||
|
section.maxOf(ArtistSectionAnchorCandidate::sortKey)
|
||||||
|
} else {
|
||||||
|
section.minOf(ArtistSectionAnchorCandidate::sortKey)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dao.getTitleSectionAnchors().map { row ->
|
val rows = if (direction == "desc") {
|
||||||
|
dao.getTitleSectionAnchorsDescending()
|
||||||
|
} else {
|
||||||
|
dao.getTitleSectionAnchors()
|
||||||
|
}
|
||||||
|
rows.map { row ->
|
||||||
row.sectionLabel to TrackPageCursor(
|
row.sectionLabel to TrackPageCursor(
|
||||||
revision,
|
revision,
|
||||||
"tracks:title",
|
"tracks:title:$direction",
|
||||||
text1 = row.sortKey,
|
text1 = row.sortKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
anchors.sortedWith(compareBy<Pair<String, TrackPageCursor>> { it.second.text1 }.thenBy { it.first })
|
val anchorComparator = compareBy<Pair<String, TrackPageCursor>> { it.second.text1 }
|
||||||
|
.thenBy { it.first }
|
||||||
|
val orderedAnchors = if (direction == "desc") {
|
||||||
|
anchors.sortedWith(anchorComparator.reversed())
|
||||||
|
} else {
|
||||||
|
anchors.sortedWith(anchorComparator)
|
||||||
|
}
|
||||||
|
orderedAnchors
|
||||||
.map { (label, cursor) ->
|
.map { (label, cursor) ->
|
||||||
mapOf(
|
mapOf(
|
||||||
"label" to label,
|
"label" to label,
|
||||||
@@ -2842,11 +2921,33 @@ class AstraLibraryRepository private constructor(
|
|||||||
"manual" -> (context["paths"] as? List<*>)
|
"manual" -> (context["paths"] as? List<*>)
|
||||||
?.mapNotNull { it as? String }
|
?.mapNotNull { it as? String }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
else -> when (context["sort"] as? String) {
|
else -> {
|
||||||
"artist" -> catalogDao.getAllPathsByArtist()
|
val sort = context["sort"] as? String ?: "title"
|
||||||
"recently_added" -> catalogDao.getAllPathsByRecentlyAdded()
|
val direction = (context["direction"] as? String)
|
||||||
"duration" -> catalogDao.getAllPathsByDuration()
|
?.takeIf { it == "asc" || it == "desc" }
|
||||||
else -> catalogDao.getAllPathsByTitle()
|
?: legacyTrackSortDirection(sort)
|
||||||
|
when (sort) {
|
||||||
|
"artist" -> if (direction == "desc") {
|
||||||
|
catalogDao.getAllPathsByArtistDescending()
|
||||||
|
} else {
|
||||||
|
catalogDao.getAllPathsByArtist()
|
||||||
|
}
|
||||||
|
"recently_added" -> if (direction == "asc") {
|
||||||
|
catalogDao.getAllPathsByRecentlyAddedAscending()
|
||||||
|
} else {
|
||||||
|
catalogDao.getAllPathsByRecentlyAdded()
|
||||||
|
}
|
||||||
|
"duration" -> if (direction == "asc") {
|
||||||
|
catalogDao.getAllPathsByDurationAscending()
|
||||||
|
} else {
|
||||||
|
catalogDao.getAllPathsByDuration()
|
||||||
|
}
|
||||||
|
else -> if (direction == "desc") {
|
||||||
|
catalogDao.getAllPathsByTitleDescending()
|
||||||
|
} else {
|
||||||
|
catalogDao.getAllPathsByTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2954,6 +3055,12 @@ class AstraLibraryRepository private constructor(
|
|||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun normalizeSortDirection(value: String): String =
|
||||||
|
if (value == "desc") "desc" else "asc"
|
||||||
|
|
||||||
|
private fun legacyTrackSortDirection(sort: String): String =
|
||||||
|
if (sort == "recently_added" || sort == "duration") "desc" else "asc"
|
||||||
|
|
||||||
private fun cursorPath(cursor: TrackPageCursor): String =
|
private fun cursorPath(cursor: TrackPageCursor): String =
|
||||||
cursor.text3?.substringAfter('\u0000', "") ?: ""
|
cursor.text3?.substringAfter('\u0000', "") ?: ""
|
||||||
|
|
||||||
|
|||||||
+353
-5
@@ -213,6 +213,9 @@ interface CatalogDao {
|
|||||||
@Query("SELECT path FROM active_tracks ORDER BY title_sort_key, path")
|
@Query("SELECT path FROM active_tracks ORDER BY title_sort_key, path")
|
||||||
suspend fun getAllPathsByTitle(): List<String>
|
suspend fun getAllPathsByTitle(): List<String>
|
||||||
|
|
||||||
|
@Query("SELECT path FROM active_tracks ORDER BY title_sort_key DESC, path")
|
||||||
|
suspend fun getAllPathsByTitleDescending(): List<String>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT path FROM active_tracks
|
SELECT path FROM active_tracks
|
||||||
@@ -221,12 +224,26 @@ interface CatalogDao {
|
|||||||
)
|
)
|
||||||
suspend fun getAllPathsByArtist(): List<String>
|
suspend fun getAllPathsByArtist(): List<String>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT path FROM active_tracks
|
||||||
|
ORDER BY artist_sort_key DESC, album_sort_key, disc_sort, track_sort, title_sort_key, path
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAllPathsByArtistDescending(): List<String>
|
||||||
|
|
||||||
@Query("SELECT path FROM active_tracks ORDER BY added_at DESC, path")
|
@Query("SELECT path FROM active_tracks ORDER BY added_at DESC, path")
|
||||||
suspend fun getAllPathsByRecentlyAdded(): List<String>
|
suspend fun getAllPathsByRecentlyAdded(): List<String>
|
||||||
|
|
||||||
|
@Query("SELECT path FROM active_tracks ORDER BY added_at, path")
|
||||||
|
suspend fun getAllPathsByRecentlyAddedAscending(): List<String>
|
||||||
|
|
||||||
@Query("SELECT path FROM active_tracks ORDER BY duration DESC, path")
|
@Query("SELECT path FROM active_tracks ORDER BY duration DESC, path")
|
||||||
suspend fun getAllPathsByDuration(): List<String>
|
suspend fun getAllPathsByDuration(): List<String>
|
||||||
|
|
||||||
|
@Query("SELECT path FROM active_tracks ORDER BY duration, path")
|
||||||
|
suspend fun getAllPathsByDurationAscending(): List<String>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT path FROM active_tracks
|
SELECT path FROM active_tracks
|
||||||
@@ -364,6 +381,22 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ActiveTrackView>
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM active_tracks
|
||||||
|
WHERE (:afterTitleKey IS NULL
|
||||||
|
OR title_sort_key < :afterTitleKey
|
||||||
|
OR (title_sort_key = :afterTitleKey AND path > :afterPath))
|
||||||
|
ORDER BY title_sort_key DESC, path
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getTitlePageDescending(
|
||||||
|
afterTitleKey: String?,
|
||||||
|
afterPath: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirror of [getTitlePage] walking backwards. Rows come out DESC — the caller
|
* Mirror of [getTitlePage] walking backwards. Rows come out DESC — the caller
|
||||||
* reverses them so `items` is ascending like every other page. Backward paging
|
* reverses them so `items` is ascending like every other page. Backward paging
|
||||||
@@ -384,6 +417,21 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ActiveTrackView>
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM active_tracks
|
||||||
|
WHERE title_sort_key > :beforeTitleKey
|
||||||
|
OR (title_sort_key = :beforeTitleKey AND path < :beforePath)
|
||||||
|
ORDER BY title_sort_key, path DESC
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getTitlePageBeforeDescending(
|
||||||
|
beforeTitleKey: String,
|
||||||
|
beforePath: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM active_tracks
|
SELECT * FROM active_tracks
|
||||||
@@ -411,6 +459,33 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ActiveTrackView>
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM active_tracks
|
||||||
|
WHERE (:afterArtistKey IS NULL
|
||||||
|
OR artist_sort_key < :afterArtistKey
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND album_sort_key > :afterAlbumKey)
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND album_sort_key = :afterAlbumKey AND disc_sort > :afterDisc)
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND album_sort_key = :afterAlbumKey AND disc_sort = :afterDisc
|
||||||
|
AND track_sort > :afterTrack)
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND album_sort_key = :afterAlbumKey AND disc_sort = :afterDisc
|
||||||
|
AND track_sort = :afterTrack AND title_sort_key > :afterTitleKey)
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND album_sort_key = :afterAlbumKey AND disc_sort = :afterDisc
|
||||||
|
AND track_sort = :afterTrack AND title_sort_key = :afterTitleKey AND path > :afterPath))
|
||||||
|
ORDER BY artist_sort_key DESC, album_sort_key, disc_sort, track_sort, title_sort_key, path
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getArtistOrderPageDescending(
|
||||||
|
afterArtistKey: String?,
|
||||||
|
afterAlbumKey: String,
|
||||||
|
afterDisc: Int,
|
||||||
|
afterTrack: Int,
|
||||||
|
afterTitleKey: String,
|
||||||
|
afterPath: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
/** Mirror of [getArtistOrderPage] walking backwards; rows come out DESC. */
|
/** Mirror of [getArtistOrderPage] walking backwards; rows come out DESC. */
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
@@ -439,6 +514,33 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ActiveTrackView>
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM active_tracks
|
||||||
|
WHERE artist_sort_key > :beforeArtistKey
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND album_sort_key < :beforeAlbumKey)
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND album_sort_key = :beforeAlbumKey AND disc_sort < :beforeDisc)
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND album_sort_key = :beforeAlbumKey AND disc_sort = :beforeDisc
|
||||||
|
AND track_sort < :beforeTrack)
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND album_sort_key = :beforeAlbumKey AND disc_sort = :beforeDisc
|
||||||
|
AND track_sort = :beforeTrack AND title_sort_key < :beforeTitleKey)
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND album_sort_key = :beforeAlbumKey AND disc_sort = :beforeDisc
|
||||||
|
AND track_sort = :beforeTrack AND title_sort_key = :beforeTitleKey AND path < :beforePath)
|
||||||
|
ORDER BY artist_sort_key, album_sort_key DESC, disc_sort DESC, track_sort DESC,
|
||||||
|
title_sort_key DESC, path DESC
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getArtistOrderPageBeforeDescending(
|
||||||
|
beforeArtistKey: String,
|
||||||
|
beforeAlbumKey: String,
|
||||||
|
beforeDisc: Int,
|
||||||
|
beforeTrack: Int,
|
||||||
|
beforeTitleKey: String,
|
||||||
|
beforePath: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM active_tracks
|
SELECT * FROM active_tracks
|
||||||
@@ -455,6 +557,22 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ActiveTrackView>
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM active_tracks
|
||||||
|
WHERE (:afterAddedAt IS NULL
|
||||||
|
OR added_at > :afterAddedAt
|
||||||
|
OR (added_at = :afterAddedAt AND path > :afterPath))
|
||||||
|
ORDER BY added_at, path
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getRecentlyAddedPageAscending(
|
||||||
|
afterAddedAt: Long?,
|
||||||
|
afterPath: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM active_tracks
|
SELECT * FROM active_tracks
|
||||||
@@ -471,6 +589,22 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ActiveTrackView>
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM active_tracks
|
||||||
|
WHERE (:afterDuration IS NULL
|
||||||
|
OR duration > :afterDuration
|
||||||
|
OR (duration = :afterDuration AND path > :afterPath))
|
||||||
|
ORDER BY duration, path
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getDurationPageAscending(
|
||||||
|
afterDuration: Double?,
|
||||||
|
afterPath: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ActiveTrackView>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM active_tracks
|
SELECT * FROM active_tracks
|
||||||
@@ -542,6 +676,16 @@ interface CatalogDao {
|
|||||||
)
|
)
|
||||||
suspend fun getTitleSectionAnchors(): List<SectionAnchorRow>
|
suspend fun getTitleSectionAnchors(): List<SectionAnchorRow>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT section_label, MAX(title_sort_key) AS sort_key
|
||||||
|
FROM active_tracks
|
||||||
|
GROUP BY section_label
|
||||||
|
ORDER BY sort_key DESC
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getTitleSectionAnchorsDescending(): List<SectionAnchorRow>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT artist, MIN(artist_sort_key) AS sort_key
|
SELECT artist, MIN(artist_sort_key) AS sort_key
|
||||||
@@ -595,6 +739,26 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<AlbumSummaryEntity>
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM album_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND (:includeSingles OR is_single = 0)
|
||||||
|
AND (:afterKey IS NULL
|
||||||
|
OR name_sort_key < :afterKey
|
||||||
|
OR (name_sort_key = :afterKey AND identity_key > :afterId))
|
||||||
|
ORDER BY name_sort_key DESC, identity_key
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAlbumNamePageDescending(
|
||||||
|
revision: Long,
|
||||||
|
includeSingles: Boolean,
|
||||||
|
afterKey: String?,
|
||||||
|
afterId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
/** Mirror of [getAlbumNamePage] walking backwards; rows come out DESC. */
|
/** Mirror of [getAlbumNamePage] walking backwards; rows come out DESC. */
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
@@ -615,6 +779,25 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<AlbumSummaryEntity>
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM album_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND (:includeSingles OR is_single = 0)
|
||||||
|
AND (name_sort_key > :beforeKey
|
||||||
|
OR (name_sort_key = :beforeKey AND identity_key < :beforeId))
|
||||||
|
ORDER BY name_sort_key, identity_key DESC
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAlbumNamePageBeforeDescending(
|
||||||
|
revision: Long,
|
||||||
|
includeSingles: Boolean,
|
||||||
|
beforeKey: String,
|
||||||
|
beforeId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM album_summaries
|
SELECT * FROM album_summaries
|
||||||
@@ -638,6 +821,29 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<AlbumSummaryEntity>
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM album_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND (:includeSingles OR is_single = 0)
|
||||||
|
AND (:afterArtistKey IS NULL
|
||||||
|
OR artist_sort_key < :afterArtistKey
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND name_sort_key > :afterNameKey)
|
||||||
|
OR (artist_sort_key = :afterArtistKey AND name_sort_key = :afterNameKey
|
||||||
|
AND identity_key > :afterId))
|
||||||
|
ORDER BY artist_sort_key DESC, name_sort_key, identity_key
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAlbumArtistPageDescending(
|
||||||
|
revision: Long,
|
||||||
|
includeSingles: Boolean,
|
||||||
|
afterArtistKey: String?,
|
||||||
|
afterNameKey: String,
|
||||||
|
afterId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
/** Mirror of [getAlbumArtistPage] walking backwards; rows come out DESC. */
|
/** Mirror of [getAlbumArtistPage] walking backwards; rows come out DESC. */
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
@@ -661,6 +867,28 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<AlbumSummaryEntity>
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM album_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND (:includeSingles OR is_single = 0)
|
||||||
|
AND (artist_sort_key > :beforeArtistKey
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND name_sort_key < :beforeNameKey)
|
||||||
|
OR (artist_sort_key = :beforeArtistKey AND name_sort_key = :beforeNameKey
|
||||||
|
AND identity_key < :beforeId))
|
||||||
|
ORDER BY artist_sort_key, name_sort_key DESC, identity_key DESC
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAlbumArtistPageBeforeDescending(
|
||||||
|
revision: Long,
|
||||||
|
includeSingles: Boolean,
|
||||||
|
beforeArtistKey: String,
|
||||||
|
beforeNameKey: String,
|
||||||
|
beforeId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM album_summaries
|
SELECT * FROM album_summaries
|
||||||
@@ -686,12 +914,36 @@ interface CatalogDao {
|
|||||||
SELECT * FROM album_summaries
|
SELECT * FROM album_summaries
|
||||||
WHERE revision = :revision
|
WHERE revision = :revision
|
||||||
AND (:includeSingles OR is_single = 0)
|
AND (:includeSingles OR is_single = 0)
|
||||||
AND (:afterYear IS NULL
|
AND (:afterAddedAt IS NULL
|
||||||
OR COALESCE(year, 0) < :afterYear
|
OR latest_added_at > :afterAddedAt
|
||||||
OR (COALESCE(year, 0) = :afterYear AND name_sort_key > :afterNameKey)
|
OR (latest_added_at = :afterAddedAt AND identity_key > :afterId))
|
||||||
OR (COALESCE(year, 0) = :afterYear AND name_sort_key = :afterNameKey
|
ORDER BY latest_added_at, identity_key
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAlbumRecentPageAscending(
|
||||||
|
revision: Long,
|
||||||
|
includeSingles: Boolean,
|
||||||
|
afterAddedAt: Long?,
|
||||||
|
afterId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM album_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND (:includeSingles OR is_single = 0)
|
||||||
|
AND (:afterUnknown IS NULL
|
||||||
|
OR CASE WHEN year IS NULL THEN 1 ELSE 0 END > :afterUnknown
|
||||||
|
OR (CASE WHEN year IS NULL THEN 1 ELSE 0 END = :afterUnknown
|
||||||
|
AND COALESCE(year, 0) < :afterYear)
|
||||||
|
OR (CASE WHEN year IS NULL THEN 1 ELSE 0 END = :afterUnknown
|
||||||
|
AND COALESCE(year, 0) = :afterYear AND name_sort_key > :afterNameKey)
|
||||||
|
OR (CASE WHEN year IS NULL THEN 1 ELSE 0 END = :afterUnknown
|
||||||
|
AND COALESCE(year, 0) = :afterYear AND name_sort_key = :afterNameKey
|
||||||
AND identity_key > :afterId))
|
AND identity_key > :afterId))
|
||||||
ORDER BY COALESCE(year, 0) DESC, name_sort_key, identity_key
|
ORDER BY CASE WHEN year IS NULL THEN 1 ELSE 0 END, year DESC, name_sort_key, identity_key
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
@@ -699,6 +951,35 @@ interface CatalogDao {
|
|||||||
revision: Long,
|
revision: Long,
|
||||||
includeSingles: Boolean,
|
includeSingles: Boolean,
|
||||||
afterYear: Int?,
|
afterYear: Int?,
|
||||||
|
afterUnknown: Int?,
|
||||||
|
afterNameKey: String,
|
||||||
|
afterId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<AlbumSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM album_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND (:includeSingles OR is_single = 0)
|
||||||
|
AND (:afterUnknown IS NULL
|
||||||
|
OR CASE WHEN year IS NULL THEN 1 ELSE 0 END > :afterUnknown
|
||||||
|
OR (CASE WHEN year IS NULL THEN 1 ELSE 0 END = :afterUnknown
|
||||||
|
AND COALESCE(year, 0) > :afterYear)
|
||||||
|
OR (CASE WHEN year IS NULL THEN 1 ELSE 0 END = :afterUnknown
|
||||||
|
AND COALESCE(year, 0) = :afterYear AND name_sort_key > :afterNameKey)
|
||||||
|
OR (CASE WHEN year IS NULL THEN 1 ELSE 0 END = :afterUnknown
|
||||||
|
AND COALESCE(year, 0) = :afterYear AND name_sort_key = :afterNameKey
|
||||||
|
AND identity_key > :afterId))
|
||||||
|
ORDER BY CASE WHEN year IS NULL THEN 1 ELSE 0 END, year, name_sort_key, identity_key
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getAlbumYearPageAscending(
|
||||||
|
revision: Long,
|
||||||
|
includeSingles: Boolean,
|
||||||
|
afterYear: Int?,
|
||||||
|
afterUnknown: Int?,
|
||||||
afterNameKey: String,
|
afterNameKey: String,
|
||||||
afterId: String,
|
afterId: String,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
@@ -747,6 +1028,28 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ArtistSummaryEntity>
|
): List<ArtistSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM artist_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND grouping_mode = :groupingMode
|
||||||
|
AND (:includeCollaborations OR is_collaboration = 0)
|
||||||
|
AND (:afterKey IS NULL
|
||||||
|
OR name_sort_key < :afterKey
|
||||||
|
OR (name_sort_key = :afterKey AND artist_key > :afterId))
|
||||||
|
ORDER BY name_sort_key DESC, artist_key
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getArtistNamePageDescending(
|
||||||
|
revision: Long,
|
||||||
|
groupingMode: String,
|
||||||
|
includeCollaborations: Boolean,
|
||||||
|
afterKey: String?,
|
||||||
|
afterId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ArtistSummaryEntity>
|
||||||
|
|
||||||
/** Mirror of [getArtistNamePage] walking backwards; rows come out DESC. */
|
/** Mirror of [getArtistNamePage] walking backwards; rows come out DESC. */
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
@@ -769,6 +1072,27 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ArtistSummaryEntity>
|
): List<ArtistSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM artist_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND grouping_mode = :groupingMode
|
||||||
|
AND (:includeCollaborations OR is_collaboration = 0)
|
||||||
|
AND (name_sort_key > :beforeKey
|
||||||
|
OR (name_sort_key = :beforeKey AND artist_key < :beforeId))
|
||||||
|
ORDER BY name_sort_key, artist_key DESC
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getArtistNamePageBeforeDescending(
|
||||||
|
revision: Long,
|
||||||
|
groupingMode: String,
|
||||||
|
includeCollaborations: Boolean,
|
||||||
|
beforeKey: String,
|
||||||
|
beforeId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ArtistSummaryEntity>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM artist_summaries
|
SELECT * FROM artist_summaries
|
||||||
@@ -793,6 +1117,30 @@ interface CatalogDao {
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
): List<ArtistSummaryEntity>
|
): List<ArtistSummaryEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM artist_summaries
|
||||||
|
WHERE revision = :revision
|
||||||
|
AND grouping_mode = :groupingMode
|
||||||
|
AND (:includeCollaborations OR is_collaboration = 0)
|
||||||
|
AND (:afterCount IS NULL
|
||||||
|
OR track_count > :afterCount
|
||||||
|
OR (track_count = :afterCount AND name_sort_key > :afterNameKey)
|
||||||
|
OR (track_count = :afterCount AND name_sort_key = :afterNameKey AND artist_key > :afterId))
|
||||||
|
ORDER BY track_count, name_sort_key, artist_key
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getArtistCountPageAscending(
|
||||||
|
revision: Long,
|
||||||
|
groupingMode: String,
|
||||||
|
includeCollaborations: Boolean,
|
||||||
|
afterCount: Long?,
|
||||||
|
afterNameKey: String,
|
||||||
|
afterId: String,
|
||||||
|
limit: Int,
|
||||||
|
): List<ArtistSummaryEntity>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM artist_summaries
|
SELECT * FROM artist_summaries
|
||||||
|
|||||||
@@ -240,7 +240,11 @@ export interface NativePage<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LibraryQuery =
|
export type LibraryQuery =
|
||||||
| { kind: 'library'; sort: 'artist' | 'title' | 'recently_added' | 'duration' }
|
| {
|
||||||
|
kind: 'library';
|
||||||
|
sort: 'artist' | 'title' | 'recently_added' | 'duration';
|
||||||
|
direction: 'asc' | 'desc';
|
||||||
|
}
|
||||||
| { kind: 'album'; albumKey: string }
|
| { kind: 'album'; albumKey: string }
|
||||||
| {
|
| {
|
||||||
kind: 'artist';
|
kind: 'artist';
|
||||||
@@ -362,6 +366,7 @@ declare class AstraLibraryDataModuleType extends NativeModule<AstraLibraryDataEv
|
|||||||
removeFolder(folderId: number): Promise<void>;
|
removeFolder(folderId: number): Promise<void>;
|
||||||
getTrackPage<T>(
|
getTrackPage<T>(
|
||||||
sort: 'artist' | 'title' | 'recently_added' | 'duration',
|
sort: 'artist' | 'title' | 'recently_added' | 'duration',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
limit: number
|
limit: number
|
||||||
): Promise<NativePage<T>>;
|
): Promise<NativePage<T>>;
|
||||||
@@ -372,6 +377,7 @@ declare class AstraLibraryDataModuleType extends NativeModule<AstraLibraryDataEv
|
|||||||
*/
|
*/
|
||||||
getTrackPageBefore<T>(
|
getTrackPageBefore<T>(
|
||||||
sort: 'artist' | 'title',
|
sort: 'artist' | 'title',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
limit: number
|
limit: number
|
||||||
): Promise<NativePage<T>>;
|
): Promise<NativePage<T>>;
|
||||||
@@ -489,6 +495,7 @@ declare class AstraLibraryDataModuleType extends NativeModule<AstraLibraryDataEv
|
|||||||
clearDesktopSyncBaselines(): Promise<void>;
|
clearDesktopSyncBaselines(): Promise<void>;
|
||||||
getAlbumPage<T>(
|
getAlbumPage<T>(
|
||||||
sort: 'artist' | 'name' | 'recently_added' | 'year',
|
sort: 'artist' | 'name' | 'recently_added' | 'year',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
includeSingles: boolean,
|
includeSingles: boolean,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
limit: number
|
limit: number
|
||||||
@@ -496,12 +503,14 @@ declare class AstraLibraryDataModuleType extends NativeModule<AstraLibraryDataEv
|
|||||||
/** Backward twin of `getAlbumPage`; see `getTrackPageBefore`. */
|
/** Backward twin of `getAlbumPage`; see `getTrackPageBefore`. */
|
||||||
getAlbumPageBefore<T>(
|
getAlbumPageBefore<T>(
|
||||||
sort: 'artist' | 'name',
|
sort: 'artist' | 'name',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
includeSingles: boolean,
|
includeSingles: boolean,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
limit: number
|
limit: number
|
||||||
): Promise<NativePage<T>>;
|
): Promise<NativePage<T>>;
|
||||||
getArtistPage<T>(
|
getArtistPage<T>(
|
||||||
sort: 'name' | 'track_count',
|
sort: 'name' | 'track_count',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
groupingMode: 'astra' | 'fileTags',
|
groupingMode: 'astra' | 'fileTags',
|
||||||
includeCollaborations: boolean,
|
includeCollaborations: boolean,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
@@ -510,6 +519,7 @@ declare class AstraLibraryDataModuleType extends NativeModule<AstraLibraryDataEv
|
|||||||
/** Backward twin of `getArtistPage`; see `getTrackPageBefore`. */
|
/** Backward twin of `getArtistPage`; see `getTrackPageBefore`. */
|
||||||
getArtistPageBefore<T>(
|
getArtistPageBefore<T>(
|
||||||
sort: 'name',
|
sort: 'name',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
groupingMode: 'astra' | 'fileTags',
|
groupingMode: 'astra' | 'fileTags',
|
||||||
includeCollaborations: boolean,
|
includeCollaborations: boolean,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
@@ -604,6 +614,7 @@ declare class AstraLibraryDataModuleType extends NativeModule<AstraLibraryDataEv
|
|||||||
getSectionAnchors(
|
getSectionAnchors(
|
||||||
kind: 'tracks' | 'albums' | 'artists',
|
kind: 'tracks' | 'albums' | 'artists',
|
||||||
sort: 'artist' | 'title' | 'name',
|
sort: 'artist' | 'title' | 'name',
|
||||||
|
direction: 'asc' | 'desc',
|
||||||
includeSingles: boolean,
|
includeSingles: boolean,
|
||||||
groupingMode: 'astra' | 'fileTags',
|
groupingMode: 'astra' | 'fileTags',
|
||||||
includeCollaborations: boolean
|
includeCollaborations: boolean
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
"test:memory-lifecycle": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/delayedPresence.test.mts scripts/android-memory-profile.test.mjs",
|
"test:memory-lifecycle": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/delayedPresence.test.mts scripts/android-memory-profile.test.mjs",
|
||||||
"test:ui-navigation": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/navigation/shellLayout.test.mts src/stores/playerPresence.test.mts src/navigation/tabsAnchor.test.mts src/navigation/libraryDetailBack.test.mts src/navigation/homeLibraryNavigation.test.mts src/navigation/tabTransition.test.mts src/navigation/statsTabState.test.mts src/navigation/tabStackReset.test.mts src/navigation/tabReselect.test.mts src/navigation/scrollToTopBehavior.test.mts src/library/libraryWindowTop.test.mts src/library/libraryViewMode.test.mts src/components/selectionSlideMath.test.mts src/components/topFadeMath.test.mts src/components/screenHeaderLayout.test.mts",
|
"test:ui-navigation": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/navigation/shellLayout.test.mts src/stores/playerPresence.test.mts src/navigation/tabsAnchor.test.mts src/navigation/libraryDetailBack.test.mts src/navigation/homeLibraryNavigation.test.mts src/navigation/tabTransition.test.mts src/navigation/statsTabState.test.mts src/navigation/tabStackReset.test.mts src/navigation/tabReselect.test.mts src/navigation/scrollToTopBehavior.test.mts src/library/libraryWindowTop.test.mts src/library/libraryViewMode.test.mts src/components/selectionSlideMath.test.mts src/components/topFadeMath.test.mts src/components/screenHeaderLayout.test.mts",
|
||||||
"test:library-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/library/libraryLayout.test.mts src/library/libraryViewPresentation.test.mts src/components/library/detailHeroLayout.test.mts",
|
"test:library-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/library/libraryLayout.test.mts src/library/libraryViewPresentation.test.mts src/components/library/detailHeroLayout.test.mts",
|
||||||
|
"test:library-sort": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/librarySortDirection.test.mts",
|
||||||
"test:haptics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/haptics.test.mts",
|
"test:haptics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/haptics.test.mts",
|
||||||
"test:app-dialog": "node --experimental-strip-types --test src/components/dialogs/dialogQueue.test.mts",
|
"test:app-dialog": "node --experimental-strip-types --test src/components/dialogs/dialogQueue.test.mts",
|
||||||
"test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts src/home/homeLayout.test.mts",
|
"test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts src/home/homeLayout.test.mts",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
useScreenHeader,
|
useScreenHeader,
|
||||||
} from '@/components/ScreenHeader';
|
} from '@/components/ScreenHeader';
|
||||||
import { Text } from '@/components/Text';
|
import { Text } from '@/components/Text';
|
||||||
|
import { SegmentedControl } from '@/components/SegmentedControl';
|
||||||
import { ViewModeSwitcher } from '@/components/library/ViewModeSwitcher';
|
import { ViewModeSwitcher } from '@/components/library/ViewModeSwitcher';
|
||||||
import { AlbumGridItem } from '@/components/library/AlbumGridItem';
|
import { AlbumGridItem } from '@/components/library/AlbumGridItem';
|
||||||
import { ArtistGridItem } from '@/components/library/ArtistGridItem';
|
import { ArtistGridItem } from '@/components/library/ArtistGridItem';
|
||||||
@@ -80,6 +81,7 @@ import {
|
|||||||
libraryContextBottomClearance,
|
libraryContextBottomClearance,
|
||||||
libraryContextBarVisible,
|
libraryContextBarVisible,
|
||||||
libraryContextOverlayHeight,
|
libraryContextOverlayHeight,
|
||||||
|
libraryRailBottomClearance,
|
||||||
libraryContextScrimHeight,
|
libraryContextScrimHeight,
|
||||||
} from '@/library/libraryViewPresentation';
|
} from '@/library/libraryViewPresentation';
|
||||||
import {
|
import {
|
||||||
@@ -109,6 +111,10 @@ import {
|
|||||||
ARTIST_SORT_LABELS,
|
ARTIST_SORT_LABELS,
|
||||||
type ArtistSort
|
type ArtistSort
|
||||||
} from '@/lib/artistSort';
|
} from '@/lib/artistSort';
|
||||||
|
import {
|
||||||
|
SORT_DIRECTION_LABELS,
|
||||||
|
type SortDirection,
|
||||||
|
} from '@/lib/sortDirection';
|
||||||
import {
|
import {
|
||||||
LIBRARY_LAYOUT_OPTIONS,
|
LIBRARY_LAYOUT_OPTIONS,
|
||||||
libraryGridColumns,
|
libraryGridColumns,
|
||||||
@@ -176,10 +182,16 @@ export default function LibraryScreen() {
|
|||||||
const tracks = useLibraryStore((s) => s.tracks);
|
const tracks = useLibraryStore((s) => s.tracks);
|
||||||
const trackSort = useLibraryStore((s) => s.trackSort);
|
const trackSort = useLibraryStore((s) => s.trackSort);
|
||||||
const setTrackSort = useLibraryStore((s) => s.setTrackSort);
|
const setTrackSort = useLibraryStore((s) => s.setTrackSort);
|
||||||
|
const trackSortDirection = useLibraryStore((s) => s.trackSortDirection);
|
||||||
|
const setTrackSortDirection = useLibraryStore((s) => s.setTrackSortDirection);
|
||||||
const albumSort = useLibraryStore((s) => s.albumSort);
|
const albumSort = useLibraryStore((s) => s.albumSort);
|
||||||
const setAlbumSort = useLibraryStore((s) => s.setAlbumSort);
|
const setAlbumSort = useLibraryStore((s) => s.setAlbumSort);
|
||||||
|
const albumSortDirection = useLibraryStore((s) => s.albumSortDirection);
|
||||||
|
const setAlbumSortDirection = useLibraryStore((s) => s.setAlbumSortDirection);
|
||||||
const artistSort = useLibraryStore((s) => s.artistSort);
|
const artistSort = useLibraryStore((s) => s.artistSort);
|
||||||
const setArtistSort = useLibraryStore((s) => s.setArtistSort);
|
const setArtistSort = useLibraryStore((s) => s.setArtistSort);
|
||||||
|
const artistSortDirection = useLibraryStore((s) => s.artistSortDirection);
|
||||||
|
const setArtistSortDirection = useLibraryStore((s) => s.setArtistSortDirection);
|
||||||
const albumLayout = useLibraryStore((s) => s.albumLayout);
|
const albumLayout = useLibraryStore((s) => s.albumLayout);
|
||||||
const setAlbumLayout = useLibraryStore((s) => s.setAlbumLayout);
|
const setAlbumLayout = useLibraryStore((s) => s.setAlbumLayout);
|
||||||
const artistLayout = useLibraryStore((s) => s.artistLayout);
|
const artistLayout = useLibraryStore((s) => s.artistLayout);
|
||||||
@@ -303,7 +315,11 @@ export default function LibraryScreen() {
|
|||||||
|
|
||||||
// Tap index is within sortedTracks so the tapped row is the track that plays.
|
// Tap index is within sortedTracks so the tapped row is the track that plays.
|
||||||
const playAllFrom = (index: number) => {
|
const playAllFrom = (index: number) => {
|
||||||
void playLibraryQuery({ kind: 'library', sort: trackSort }, {
|
void playLibraryQuery({
|
||||||
|
kind: 'library',
|
||||||
|
sort: trackSort,
|
||||||
|
direction: trackSortDirection,
|
||||||
|
}, {
|
||||||
anchorPath: sortedTracks[index]?.path,
|
anchorPath: sortedTracks[index]?.path,
|
||||||
source: { kind: 'library', label: 'Library' },
|
source: { kind: 'library', label: 'Library' },
|
||||||
});
|
});
|
||||||
@@ -502,6 +518,18 @@ export default function LibraryScreen() {
|
|||||||
: viewMode === 'albums'
|
: viewMode === 'albums'
|
||||||
? ALBUM_SORT_LABELS[albumSort]
|
? ALBUM_SORT_LABELS[albumSort]
|
||||||
: ARTIST_SORT_LABELS[artistSort];
|
: ARTIST_SORT_LABELS[artistSort];
|
||||||
|
const sortDirection: SortDirection =
|
||||||
|
viewMode === 'tracks'
|
||||||
|
? trackSortDirection
|
||||||
|
: viewMode === 'albums'
|
||||||
|
? albumSortDirection
|
||||||
|
: artistSortDirection;
|
||||||
|
const sortDirectionLabel = SORT_DIRECTION_LABELS[sortDirection];
|
||||||
|
const setSortDirection = (direction: SortDirection) => {
|
||||||
|
if (viewMode === 'tracks') setTrackSortDirection(direction);
|
||||||
|
if (viewMode === 'albums') setAlbumSortDirection(direction);
|
||||||
|
if (viewMode === 'artists') setArtistSortDirection(direction);
|
||||||
|
};
|
||||||
const sortSheetLabel =
|
const sortSheetLabel =
|
||||||
viewMode === 'tracks' ? 'SORT TRACKS BY' : viewMode === 'albums' ? 'SORT ALBUMS BY' : 'SORT ARTISTS BY';
|
viewMode === 'tracks' ? 'SORT TRACKS BY' : viewMode === 'albums' ? 'SORT ALBUMS BY' : 'SORT ARTISTS BY';
|
||||||
const sortItems =
|
const sortItems =
|
||||||
@@ -542,11 +570,11 @@ export default function LibraryScreen() {
|
|||||||
|
|
||||||
const surfaceHeadIdentity =
|
const surfaceHeadIdentity =
|
||||||
viewMode === 'albums'
|
viewMode === 'albums'
|
||||||
? `albums:${albumSort}:${albumLayout}:${albumColumns}`
|
? `albums:${albumSort}:${albumSortDirection}:${albumLayout}:${albumColumns}`
|
||||||
: viewMode === 'artists'
|
: viewMode === 'artists'
|
||||||
? `artists:${artistSort}:${artistLayout}:${artistColumns}`
|
? `artists:${artistSort}:${artistSortDirection}:${artistLayout}:${artistColumns}`
|
||||||
: viewMode === 'tracks'
|
: viewMode === 'tracks'
|
||||||
? `tracks:${trackSort}`
|
? `tracks:${trackSort}:${trackSortDirection}`
|
||||||
: viewMode;
|
: viewMode;
|
||||||
const listMountIdentity = `${surfaceHeadIdentity}:${sectionJumpRevision}`;
|
const listMountIdentity = `${surfaceHeadIdentity}:${sectionJumpRevision}`;
|
||||||
const activeListMountIdentity = useRef(listMountIdentity);
|
const activeListMountIdentity = useRef(listMountIdentity);
|
||||||
@@ -738,10 +766,14 @@ export default function LibraryScreen() {
|
|||||||
style={styles.sortTrigger}
|
style={styles.sortTrigger}
|
||||||
onPress={() => setSortSheetOpen(true)}
|
onPress={() => setSortSheetOpen(true)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`Sort by ${sortLabel}`}
|
accessibilityLabel={`Sort by ${sortLabel}, ${sortDirectionLabel}`}
|
||||||
>
|
>
|
||||||
<Ionicons name="swap-vertical" size={14} color={colors.textSecondary} />
|
<Ionicons
|
||||||
<Text variant="label">{sortLabel}</Text>
|
name={sortDirection === 'asc' ? 'arrow-up' : 'arrow-down'}
|
||||||
|
size={14}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
<Text variant="label">{sortLabel} · {sortDirectionLabel}</Text>
|
||||||
</AppPressable>
|
</AppPressable>
|
||||||
{activeLayout && activeLayoutLabel ? (
|
{activeLayout && activeLayoutLabel ? (
|
||||||
<AppPressable feedback="control"
|
<AppPressable feedback="control"
|
||||||
@@ -787,7 +819,7 @@ export default function LibraryScreen() {
|
|||||||
{viewMode === 'albums' ? (
|
{viewMode === 'albums' ? (
|
||||||
<ReanimatedFlashList
|
<ReanimatedFlashList
|
||||||
ref={albumListRef}
|
ref={albumListRef}
|
||||||
key={`albums-${albumSort}-${albumLayout}-${albumColumns}-${sectionJumpRevision}`}
|
key={`albums-${albumSort}-${albumSortDirection}-${albumLayout}-${albumColumns}-${sectionJumpRevision}`}
|
||||||
data={sortedAlbums}
|
data={sortedAlbums}
|
||||||
numColumns={albumColumns}
|
numColumns={albumColumns}
|
||||||
keyExtractor={(album) => album.identity_key}
|
keyExtractor={(album) => album.identity_key}
|
||||||
@@ -841,7 +873,7 @@ export default function LibraryScreen() {
|
|||||||
{viewMode === 'artists' ? (
|
{viewMode === 'artists' ? (
|
||||||
<ReanimatedFlashList
|
<ReanimatedFlashList
|
||||||
ref={artistListRef}
|
ref={artistListRef}
|
||||||
key={`artists-${artistSort}-${artistLayout}-${artistColumns}-${sectionJumpRevision}`}
|
key={`artists-${artistSort}-${artistSortDirection}-${artistLayout}-${artistColumns}-${sectionJumpRevision}`}
|
||||||
data={sortedArtists}
|
data={sortedArtists}
|
||||||
numColumns={artistColumns}
|
numColumns={artistColumns}
|
||||||
keyExtractor={(artist) => artist.artist}
|
keyExtractor={(artist) => artist.artist}
|
||||||
@@ -895,7 +927,7 @@ export default function LibraryScreen() {
|
|||||||
{viewMode === 'tracks' ? (
|
{viewMode === 'tracks' ? (
|
||||||
<ReanimatedFlashList
|
<ReanimatedFlashList
|
||||||
ref={trackListRef}
|
ref={trackListRef}
|
||||||
key={`tracks-${trackSort}-${sectionJumpRevision}`}
|
key={`tracks-${trackSort}-${trackSortDirection}-${sectionJumpRevision}`}
|
||||||
data={sortedTracks}
|
data={sortedTracks}
|
||||||
keyExtractor={(track) => String(track.id)}
|
keyExtractor={(track) => String(track.id)}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
@@ -966,11 +998,18 @@ export default function LibraryScreen() {
|
|||||||
// off its right edge, so it needs a box that matches the part of
|
// off its right edge, so it needs a box that matches the part of
|
||||||
// the list a finger can actually reach.
|
// the list a finger can actually reach.
|
||||||
<View
|
<View
|
||||||
style={[styles.railArea, { top: header.contentPaddingTop }]}
|
style={[
|
||||||
|
styles.railArea,
|
||||||
|
{
|
||||||
|
top: header.contentPaddingTop,
|
||||||
|
bottom: libraryRailBottomClearance(phoneContextBar, contextOverlayHeight),
|
||||||
|
},
|
||||||
|
]}
|
||||||
pointerEvents="box-none"
|
pointerEvents="box-none"
|
||||||
>
|
>
|
||||||
<AlphabetRail
|
<AlphabetRail
|
||||||
activeLetters={railLetters}
|
activeLetters={railLetters}
|
||||||
|
direction={sortDirection}
|
||||||
onJumpToLetter={jumpToLetter}
|
onJumpToLetter={jumpToLetter}
|
||||||
onScrubEnd={flushJump}
|
onScrubEnd={flushJump}
|
||||||
/>
|
/>
|
||||||
@@ -1015,7 +1054,7 @@ export default function LibraryScreen() {
|
|||||||
: viewMode === 'tracks'
|
: viewMode === 'tracks'
|
||||||
? {
|
? {
|
||||||
icon: 'swap-vertical',
|
icon: 'swap-vertical',
|
||||||
label: `Sort Tracks, currently ${sortLabel}`,
|
label: `Sort Tracks, currently ${sortLabel}, ${sortDirectionLabel}`,
|
||||||
onPress: () => setSortSheetOpen(true),
|
onPress: () => setSortSheetOpen(true),
|
||||||
}
|
}
|
||||||
: viewMode === 'playlists'
|
: viewMode === 'playlists'
|
||||||
@@ -1069,12 +1108,18 @@ export default function LibraryScreen() {
|
|||||||
key={key}
|
key={key}
|
||||||
label={label}
|
label={label}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onPress={() => {
|
onPress={onSelect}
|
||||||
onSelect();
|
|
||||||
setSortSheetOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<AppSheetSection label="DIRECTION" />
|
||||||
|
<SegmentedControl
|
||||||
|
value={sortDirection}
|
||||||
|
segments={[
|
||||||
|
{ key: 'asc', label: 'Ascending' },
|
||||||
|
{ key: 'desc', label: 'Descending' },
|
||||||
|
]}
|
||||||
|
onChange={(direction) => setSortDirection(direction === 'desc' ? 'desc' : 'asc')}
|
||||||
|
/>
|
||||||
</AppSheet>
|
</AppSheet>
|
||||||
) : null}
|
) : null}
|
||||||
{layoutSheetOpen && activeLayout ? (
|
{layoutSheetOpen && activeLayout ? (
|
||||||
@@ -1107,6 +1152,15 @@ export default function LibraryScreen() {
|
|||||||
onPress={onSelect}
|
onPress={onSelect}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<AppSheetSection label="DIRECTION" />
|
||||||
|
<SegmentedControl
|
||||||
|
value={sortDirection}
|
||||||
|
segments={[
|
||||||
|
{ key: 'asc', label: 'Ascending' },
|
||||||
|
{ key: 'desc', label: 'Descending' },
|
||||||
|
]}
|
||||||
|
onChange={(direction) => setSortDirection(direction === 'desc' ? 'desc' : 'asc')}
|
||||||
|
/>
|
||||||
<AppSheetSection label={layoutSheetLabel} />
|
<AppSheetSection label={layoutSheetLabel} />
|
||||||
{LIBRARY_LAYOUT_OPTIONS.map((option) => (
|
{LIBRARY_LAYOUT_OPTIONS.map((option) => (
|
||||||
<AppSheetItem
|
<AppSheetItem
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { createThemedStyles } from '@/theme/themed';
|
|||||||
import { rgbaFromHex } from '@/theme/colorUtils';
|
import { rgbaFromHex } from '@/theme/colorUtils';
|
||||||
import { playHaptic } from '@/lib/haptics';
|
import { playHaptic } from '@/lib/haptics';
|
||||||
import { usePullSearchGestureRef } from '@/components/search/PullSearchGesture';
|
import { usePullSearchGestureRef } from '@/components/search/PullSearchGesture';
|
||||||
import { RAIL_LETTERS } from '@/lib/letterIndex';
|
import { RAIL_LETTERS, railLettersForDirection } from '@/lib/letterIndex';
|
||||||
|
import type { SortDirection } from '@/lib/sortDirection';
|
||||||
|
|
||||||
const CELL_HEIGHT = 17;
|
const CELL_HEIGHT = 17;
|
||||||
const RAIL_PAD = spacing.xs;
|
const RAIL_PAD = spacing.xs;
|
||||||
@@ -19,6 +20,7 @@ const BUBBLE_SIZE = 52;
|
|||||||
interface AlphabetRailProps {
|
interface AlphabetRailProps {
|
||||||
/** Letters present in the current list — the rest render dimmed. */
|
/** Letters present in the current list — the rest render dimmed. */
|
||||||
activeLetters: ReadonlySet<string>;
|
activeLetters: ReadonlySet<string>;
|
||||||
|
direction: SortDirection;
|
||||||
onJumpToLetter: (letter: string) => void;
|
onJumpToLetter: (letter: string) => void;
|
||||||
/**
|
/**
|
||||||
* Fired when the finger lifts. The screen debounces `onJumpToLetter` so a fast
|
* Fired when the finger lifts. The screen debounces `onJumpToLetter` so a fast
|
||||||
@@ -36,10 +38,16 @@ interface AlphabetRailProps {
|
|||||||
* changes on a letter-cross). Blocks the pull-to-search gesture so a scrub at
|
* changes on a letter-cross). Blocks the pull-to-search gesture so a scrub at
|
||||||
* scroll-top never arms the search indicator.
|
* scroll-top never arms the search indicator.
|
||||||
*/
|
*/
|
||||||
export function AlphabetRail({ activeLetters, onJumpToLetter, onScrubEnd }: AlphabetRailProps) {
|
export function AlphabetRail({
|
||||||
|
activeLetters,
|
||||||
|
direction,
|
||||||
|
onJumpToLetter,
|
||||||
|
onScrubEnd,
|
||||||
|
}: AlphabetRailProps) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const pullSearchRef = usePullSearchGestureRef();
|
const pullSearchRef = usePullSearchGestureRef();
|
||||||
const [scrubLetter, setScrubLetter] = useState<string | null>(null);
|
const [scrubLetter, setScrubLetter] = useState<string | null>(null);
|
||||||
|
const railLetters = railLettersForDirection(direction);
|
||||||
const lastLetter = useSharedValue('');
|
const lastLetter = useSharedValue('');
|
||||||
// Rail's top offset inside the (vertically-centered) wrap + the finger's Y
|
// Rail's top offset inside the (vertically-centered) wrap + the finger's Y
|
||||||
// within the rail, so the bubble can be placed in wrap-space.
|
// within the rail, so the bubble can be placed in wrap-space.
|
||||||
@@ -72,7 +80,7 @@ export function AlphabetRail({ activeLetters, onJumpToLetter, onScrubEnd }: Alph
|
|||||||
0,
|
0,
|
||||||
Math.min(RAIL_LETTERS.length - 1, Math.floor((y - RAIL_PAD) / CELL_HEIGHT))
|
Math.min(RAIL_LETTERS.length - 1, Math.floor((y - RAIL_PAD) / CELL_HEIGHT))
|
||||||
);
|
);
|
||||||
const letter = RAIL_LETTERS[index];
|
const letter = railLetters[index];
|
||||||
lastLetter.value = letter;
|
lastLetter.value = letter;
|
||||||
runOnJS(scrubTo)(letter);
|
runOnJS(scrubTo)(letter);
|
||||||
})
|
})
|
||||||
@@ -86,7 +94,7 @@ export function AlphabetRail({ activeLetters, onJumpToLetter, onScrubEnd }: Alph
|
|||||||
0,
|
0,
|
||||||
Math.min(RAIL_LETTERS.length - 1, Math.floor((y - RAIL_PAD) / CELL_HEIGHT))
|
Math.min(RAIL_LETTERS.length - 1, Math.floor((y - RAIL_PAD) / CELL_HEIGHT))
|
||||||
);
|
);
|
||||||
const letter = RAIL_LETTERS[index];
|
const letter = railLetters[index];
|
||||||
if (letter === lastLetter.value) return;
|
if (letter === lastLetter.value) return;
|
||||||
lastLetter.value = letter;
|
lastLetter.value = letter;
|
||||||
runOnJS(scrubTo)(letter);
|
runOnJS(scrubTo)(letter);
|
||||||
@@ -98,7 +106,7 @@ export function AlphabetRail({ activeLetters, onJumpToLetter, onScrubEnd }: Alph
|
|||||||
});
|
});
|
||||||
return pullSearchRef ? gesture.blocksExternalGesture(pullSearchRef) : gesture;
|
return pullSearchRef ? gesture.blocksExternalGesture(pullSearchRef) : gesture;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- scrubTo/endScrub capture the latest props via render closure
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- scrubTo/endScrub capture the latest props via render closure
|
||||||
}, [lastLetter, bubbleY, railTop, pullSearchRef, activeLetters, onJumpToLetter, onScrubEnd]);
|
}, [lastLetter, bubbleY, railTop, pullSearchRef, activeLetters, railLetters, onJumpToLetter, onScrubEnd]);
|
||||||
|
|
||||||
const bubbleStyle = useAnimatedStyle(() => ({
|
const bubbleStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{ translateY: bubbleY.value - BUBBLE_SIZE / 2 }],
|
transform: [{ translateY: bubbleY.value - BUBBLE_SIZE / 2 }],
|
||||||
@@ -119,7 +127,7 @@ export function AlphabetRail({ activeLetters, onJumpToLetter, onScrubEnd }: Alph
|
|||||||
) : null}
|
) : null}
|
||||||
<GestureDetector gesture={pan}>
|
<GestureDetector gesture={pan}>
|
||||||
<View style={styles.rail} hitSlop={{ left: 12, right: 8 }} onLayout={onRailLayout}>
|
<View style={styles.rail} hitSlop={{ left: 12, right: 8 }} onLayout={onRailLayout}>
|
||||||
{RAIL_LETTERS.map((letter) => {
|
{railLetters.map((letter) => {
|
||||||
const present = activeLetters.has(letter);
|
const present = activeLetters.has(letter);
|
||||||
const scrubbing = letter === scrubLetter;
|
const scrubbing = letter === scrubLetter;
|
||||||
return (
|
return (
|
||||||
|
|||||||
+34
-8
@@ -1,4 +1,5 @@
|
|||||||
import type { Album } from '@/types/library';
|
import type { Album } from '@/types/library';
|
||||||
|
import { compareDirected, type SortDirection } from './sortDirection.ts';
|
||||||
|
|
||||||
export type AlbumSort = 'artist' | 'name' | 'recently_added' | 'year';
|
export type AlbumSort = 'artist' | 'name' | 'recently_added' | 'year';
|
||||||
|
|
||||||
@@ -9,22 +10,47 @@ export const ALBUM_SORT_LABELS: Record<AlbumSort, string> = {
|
|||||||
year: 'Year',
|
year: 'Year',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 'artist' is buildAlbumList's native order (artist → album); others sort a copy. */
|
export const ALBUM_SORT_LEGACY_DIRECTIONS: Record<AlbumSort, SortDirection> = {
|
||||||
export function sortAlbums(albums: Album[], sort: AlbumSort): Album[] {
|
artist: 'asc',
|
||||||
|
name: 'asc',
|
||||||
|
recently_added: 'desc',
|
||||||
|
year: 'desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Mirrors the native primary-direction/forward-tiebreak ordering. */
|
||||||
|
export function sortAlbums(
|
||||||
|
albums: Album[],
|
||||||
|
sort: AlbumSort,
|
||||||
|
direction: SortDirection = ALBUM_SORT_LEGACY_DIRECTIONS[sort],
|
||||||
|
): Album[] {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'artist':
|
case 'artist':
|
||||||
return albums;
|
return [...albums].sort((a, b) =>
|
||||||
|
compareDirected(a.artist, b.artist, direction, (left, right) => left.localeCompare(right)) ||
|
||||||
|
a.album.localeCompare(b.album) ||
|
||||||
|
a.identity_key.localeCompare(b.identity_key)
|
||||||
|
);
|
||||||
case 'name':
|
case 'name':
|
||||||
return [...albums].sort((a, b) => a.album.localeCompare(b.album));
|
return [...albums].sort((a, b) =>
|
||||||
|
compareDirected(a.album, b.album, direction, (left, right) => left.localeCompare(right)) ||
|
||||||
|
a.identity_key.localeCompare(b.identity_key)
|
||||||
|
);
|
||||||
case 'recently_added':
|
case 'recently_added':
|
||||||
return [...albums].sort((a, b) => b.latest_added_at - a.latest_added_at);
|
return [...albums].sort((a, b) =>
|
||||||
|
compareDirected(a.latest_added_at, b.latest_added_at, direction, (left, right) => left - right) ||
|
||||||
|
a.identity_key.localeCompare(b.identity_key)
|
||||||
|
);
|
||||||
case 'year':
|
case 'year':
|
||||||
// Newest first, unknown years last, name tiebreak.
|
// Unknown years stay last in either direction.
|
||||||
return [...albums].sort((a, b) => {
|
return [...albums].sort((a, b) => {
|
||||||
if (a.year == null && b.year == null) return a.album.localeCompare(b.album);
|
if (a.year == null && b.year == null) {
|
||||||
|
return a.album.localeCompare(b.album) || a.identity_key.localeCompare(b.identity_key);
|
||||||
|
}
|
||||||
if (a.year == null) return 1;
|
if (a.year == null) return 1;
|
||||||
if (b.year == null) return -1;
|
if (b.year == null) return -1;
|
||||||
return b.year - a.year || a.album.localeCompare(b.album);
|
return compareDirected(a.year, b.year, direction, (left, right) => left - right) ||
|
||||||
|
a.album.localeCompare(b.album) ||
|
||||||
|
a.identity_key.localeCompare(b.identity_key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-4
@@ -1,4 +1,5 @@
|
|||||||
import type { Artist } from '@/types/library';
|
import type { Artist } from '@/types/library';
|
||||||
|
import { compareDirected, type SortDirection } from './sortDirection.ts';
|
||||||
|
|
||||||
export type ArtistSort = 'name' | 'track_count';
|
export type ArtistSort = 'name' | 'track_count';
|
||||||
|
|
||||||
@@ -7,14 +8,27 @@ export const ARTIST_SORT_LABELS: Record<ArtistSort, string> = {
|
|||||||
track_count: 'Track count',
|
track_count: 'Track count',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 'name' is buildArtistList's native order; track count sorts a copy, most first. */
|
export const ARTIST_SORT_LEGACY_DIRECTIONS: Record<ArtistSort, SortDirection> = {
|
||||||
export function sortArtists(artists: Artist[], sort: ArtistSort): Artist[] {
|
name: 'asc',
|
||||||
|
track_count: 'desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Mirrors the native primary-direction/forward-tiebreak ordering. */
|
||||||
|
export function sortArtists(
|
||||||
|
artists: Artist[],
|
||||||
|
sort: ArtistSort,
|
||||||
|
direction: SortDirection = ARTIST_SORT_LEGACY_DIRECTIONS[sort],
|
||||||
|
): Artist[] {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'name':
|
case 'name':
|
||||||
return artists;
|
return [...artists].sort((a, b) =>
|
||||||
|
compareDirected(a.artist, b.artist, direction, (left, right) => left.localeCompare(right))
|
||||||
|
);
|
||||||
case 'track_count':
|
case 'track_count':
|
||||||
return [...artists].sort(
|
return [...artists].sort(
|
||||||
(a, b) => b.track_count - a.track_count || a.artist.localeCompare(b.artist)
|
(a, b) =>
|
||||||
|
compareDirected(a.track_count, b.track_count, direction, (left, right) => left - right) ||
|
||||||
|
a.artist.localeCompare(b.artist)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export const RAIL_LETTERS: readonly string[] = [
|
|||||||
...Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i)),
|
...Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DESCENDING_RAIL_LETTERS: readonly string[] = [...RAIL_LETTERS].reverse();
|
||||||
|
|
||||||
|
export function railLettersForDirection(direction: 'asc' | 'desc'): readonly string[] {
|
||||||
|
return direction === 'desc' ? DESCENDING_RAIL_LETTERS : RAIL_LETTERS;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LetterIndexEntry {
|
export interface LetterIndexEntry {
|
||||||
letter: string;
|
letter: string;
|
||||||
firstIndex: number;
|
firstIndex: number;
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { sortTracks, TRACK_SORT_LEGACY_DIRECTIONS } from './trackSort.ts';
|
||||||
|
import { sortAlbums, ALBUM_SORT_LEGACY_DIRECTIONS } from './albumSort.ts';
|
||||||
|
import { sortArtists, ARTIST_SORT_LEGACY_DIRECTIONS } from './artistSort.ts';
|
||||||
|
import { railLettersForDirection } from './letterIndex.ts';
|
||||||
|
import { parseSortDirection } from './sortDirection.ts';
|
||||||
|
import type { Album, Artist, DbTrack } from '../types/library.ts';
|
||||||
|
|
||||||
|
function track(values: Partial<DbTrack> & Pick<DbTrack, 'path' | 'title'>): DbTrack {
|
||||||
|
return {
|
||||||
|
artist: '',
|
||||||
|
album: '',
|
||||||
|
disc_number: null,
|
||||||
|
track_number: null,
|
||||||
|
duration: 0,
|
||||||
|
added_at: 0,
|
||||||
|
...values,
|
||||||
|
} as DbTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
function album(values: Partial<Album> & Pick<Album, 'identity_key' | 'album'>): Album {
|
||||||
|
return {
|
||||||
|
artist: '',
|
||||||
|
year: null,
|
||||||
|
latest_added_at: 0,
|
||||||
|
...values,
|
||||||
|
} as Album;
|
||||||
|
}
|
||||||
|
|
||||||
|
function artist(name: string, count: number): Artist {
|
||||||
|
return { artist: name, track_count: count } as Artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('missing persisted directions preserve each legacy field order', () => {
|
||||||
|
assert.deepEqual(TRACK_SORT_LEGACY_DIRECTIONS, {
|
||||||
|
artist: 'asc',
|
||||||
|
title: 'asc',
|
||||||
|
recently_added: 'desc',
|
||||||
|
duration: 'desc',
|
||||||
|
});
|
||||||
|
assert.deepEqual(ALBUM_SORT_LEGACY_DIRECTIONS, {
|
||||||
|
artist: 'asc',
|
||||||
|
name: 'asc',
|
||||||
|
recently_added: 'desc',
|
||||||
|
year: 'desc',
|
||||||
|
});
|
||||||
|
assert.deepEqual(ARTIST_SORT_LEGACY_DIRECTIONS, { name: 'asc', track_count: 'desc' });
|
||||||
|
assert.equal(parseSortDirection('asc'), 'asc');
|
||||||
|
assert.equal(parseSortDirection('desc'), 'desc');
|
||||||
|
assert.equal(parseSortDirection('sideways'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('track primary fields reverse while stable tiebreakers stay forward', () => {
|
||||||
|
const tracks = [
|
||||||
|
track({ path: 'z-2', title: 'Second', artist: 'Zulu', album: 'Beta', track_number: 2 }),
|
||||||
|
track({ path: 'z-1', title: 'First', artist: 'Zulu', album: 'Alpha', track_number: 1 }),
|
||||||
|
track({ path: 'a-1', title: 'Third', artist: 'Alpha', album: 'Gamma', track_number: 1 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'artist', 'desc').map((item) => item.path), [
|
||||||
|
'z-1',
|
||||||
|
'z-2',
|
||||||
|
'a-1',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'title', 'asc').map((item) => item.title), [
|
||||||
|
'First',
|
||||||
|
'Second',
|
||||||
|
'Third',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'title', 'desc').map((item) => item.title), [
|
||||||
|
'Third',
|
||||||
|
'Second',
|
||||||
|
'First',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('numeric track fields support both directions', () => {
|
||||||
|
const tracks = [
|
||||||
|
track({ path: 'b', title: 'B', added_at: 20, duration: 100 }),
|
||||||
|
track({ path: 'a', title: 'A', added_at: 10, duration: 200 }),
|
||||||
|
];
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'recently_added', 'asc').map((item) => item.path), ['a', 'b']);
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'recently_added', 'desc').map((item) => item.path), ['b', 'a']);
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'duration', 'asc').map((item) => item.path), ['b', 'a']);
|
||||||
|
assert.deepEqual(sortTracks(tracks, 'duration', 'desc').map((item) => item.path), ['a', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('album directions preserve natural tiebreakers and keep unknown years last', () => {
|
||||||
|
const albums = [
|
||||||
|
album({ identity_key: 'z-b', album: 'Beta', artist: 'Zulu', year: 2020, latest_added_at: 30 }),
|
||||||
|
album({ identity_key: 'z-a', album: 'Alpha', artist: 'Zulu', year: null, latest_added_at: 10 }),
|
||||||
|
album({ identity_key: 'a', album: 'Gamma', artist: 'Alpha', year: 1990, latest_added_at: 20 }),
|
||||||
|
];
|
||||||
|
assert.deepEqual(sortAlbums(albums, 'artist', 'desc').map((item) => item.identity_key), [
|
||||||
|
'z-a',
|
||||||
|
'z-b',
|
||||||
|
'a',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(sortAlbums(albums, 'year', 'asc').map((item) => item.identity_key), [
|
||||||
|
'a',
|
||||||
|
'z-b',
|
||||||
|
'z-a',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(sortAlbums(albums, 'year', 'desc').map((item) => item.identity_key), [
|
||||||
|
'z-b',
|
||||||
|
'a',
|
||||||
|
'z-a',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('artist name and count sorts reverse only their primary field', () => {
|
||||||
|
const artists = [artist('Zulu', 2), artist('Alpha', 2), artist('Beta', 1)];
|
||||||
|
assert.deepEqual(sortArtists(artists, 'name', 'desc').map((item) => item.artist), [
|
||||||
|
'Zulu',
|
||||||
|
'Beta',
|
||||||
|
'Alpha',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(sortArtists(artists, 'track_count', 'desc').map((item) => item.artist), [
|
||||||
|
'Alpha',
|
||||||
|
'Zulu',
|
||||||
|
'Beta',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(sortArtists(artists, 'track_count', 'asc').map((item) => item.artist), [
|
||||||
|
'Beta',
|
||||||
|
'Alpha',
|
||||||
|
'Zulu',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('descending alphabet rail is a complete visual flip', () => {
|
||||||
|
const ascending = railLettersForDirection('asc');
|
||||||
|
const descending = railLettersForDirection('desc');
|
||||||
|
assert.equal(ascending[0], '#');
|
||||||
|
assert.equal(ascending.at(-1), 'Z');
|
||||||
|
assert.equal(descending[0], 'Z');
|
||||||
|
assert.equal(descending.at(-1), '#');
|
||||||
|
assert.deepEqual(descending, [...ascending].reverse());
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
|
export const SORT_DIRECTION_LABELS: Record<SortDirection, string> = {
|
||||||
|
asc: 'Ascending',
|
||||||
|
desc: 'Descending',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SORT_DIRECTIONS: readonly SortDirection[] = ['asc', 'desc'];
|
||||||
|
|
||||||
|
export function parseSortDirection(value: string | null): SortDirection | null {
|
||||||
|
return value === 'asc' || value === 'desc' ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareDirected<T>(
|
||||||
|
left: T,
|
||||||
|
right: T,
|
||||||
|
direction: SortDirection,
|
||||||
|
compare: (a: T, b: T) => number,
|
||||||
|
): number {
|
||||||
|
const result = compare(left, right);
|
||||||
|
return direction === 'desc' ? -result : result;
|
||||||
|
}
|
||||||
+34
-6
@@ -1,4 +1,5 @@
|
|||||||
import type { DbTrack } from '@/types/library';
|
import type { DbTrack } from '@/types/library';
|
||||||
|
import { compareDirected, type SortDirection } from './sortDirection.ts';
|
||||||
|
|
||||||
export type TrackSort = 'artist' | 'title' | 'recently_added' | 'duration';
|
export type TrackSort = 'artist' | 'title' | 'recently_added' | 'duration';
|
||||||
|
|
||||||
@@ -9,16 +10,43 @@ export const TRACK_SORT_LABELS: Record<TrackSort, string> = {
|
|||||||
duration: 'Duration',
|
duration: 'Duration',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 'artist' is the DB's native order (getAllTracks); others sort a copy. */
|
export const TRACK_SORT_LEGACY_DIRECTIONS: Record<TrackSort, SortDirection> = {
|
||||||
export function sortTracks(tracks: DbTrack[], sort: TrackSort): DbTrack[] {
|
artist: 'asc',
|
||||||
|
title: 'asc',
|
||||||
|
recently_added: 'desc',
|
||||||
|
duration: 'desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Mirrors the native primary-direction/forward-tiebreak ordering. */
|
||||||
|
export function sortTracks(
|
||||||
|
tracks: DbTrack[],
|
||||||
|
sort: TrackSort,
|
||||||
|
direction: SortDirection = TRACK_SORT_LEGACY_DIRECTIONS[sort],
|
||||||
|
): DbTrack[] {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'artist':
|
case 'artist':
|
||||||
return tracks;
|
return [...tracks].sort((a, b) =>
|
||||||
|
compareDirected(a.artist, b.artist, direction, (left, right) => left.localeCompare(right)) ||
|
||||||
|
a.album.localeCompare(b.album) ||
|
||||||
|
(a.disc_number ?? 0) - (b.disc_number ?? 0) ||
|
||||||
|
(a.track_number ?? 0) - (b.track_number ?? 0) ||
|
||||||
|
a.title.localeCompare(b.title) ||
|
||||||
|
a.path.localeCompare(b.path)
|
||||||
|
);
|
||||||
case 'title':
|
case 'title':
|
||||||
return [...tracks].sort((a, b) => a.title.localeCompare(b.title));
|
return [...tracks].sort((a, b) =>
|
||||||
|
compareDirected(a.title, b.title, direction, (left, right) => left.localeCompare(right)) ||
|
||||||
|
a.path.localeCompare(b.path)
|
||||||
|
);
|
||||||
case 'recently_added':
|
case 'recently_added':
|
||||||
return [...tracks].sort((a, b) => b.added_at - a.added_at);
|
return [...tracks].sort((a, b) =>
|
||||||
|
compareDirected(a.added_at, b.added_at, direction, (left, right) => left - right) ||
|
||||||
|
a.path.localeCompare(b.path)
|
||||||
|
);
|
||||||
case 'duration':
|
case 'duration':
|
||||||
return [...tracks].sort((a, b) => b.duration - a.duration);
|
return [...tracks].sort((a, b) =>
|
||||||
|
compareDirected(a.duration, b.duration, direction, (left, right) => left - right) ||
|
||||||
|
a.path.localeCompare(b.path)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
libraryContextBarVisible,
|
libraryContextBarVisible,
|
||||||
libraryContextBottomClearance,
|
libraryContextBottomClearance,
|
||||||
libraryContextOverlayHeight,
|
libraryContextOverlayHeight,
|
||||||
|
libraryRailBottomClearance,
|
||||||
libraryContextScrimHeight,
|
libraryContextScrimHeight,
|
||||||
libraryDockSectionWidth,
|
libraryDockSectionWidth,
|
||||||
libraryDockShowsActiveLabel,
|
libraryDockShowsActiveLabel,
|
||||||
@@ -114,6 +115,12 @@ test('the floating bar reserves its end-of-list runway while the fade starts abo
|
|||||||
assert.equal(libraryContextScrimHeight(8), 116);
|
assert.equal(libraryContextScrimHeight(8), 116);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the alphabet rail clears phone Library chrome but keeps the wide-screen boundary', () => {
|
||||||
|
assert.equal(libraryRailBottomClearance(true, 136), 136);
|
||||||
|
assert.equal(libraryRailBottomClearance(true, -1), 0);
|
||||||
|
assert.equal(libraryRailBottomClearance(false, 136), 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('mini-player visibility mirrors target fallback semantics', () => {
|
test('mini-player visibility mirrors target fallback semantics', () => {
|
||||||
assert.equal(effectiveMiniPlayerVisible({
|
assert.equal(effectiveMiniPlayerVisible({
|
||||||
selectedTarget: 'phone',
|
selectedTarget: 'phone',
|
||||||
|
|||||||
@@ -118,6 +118,14 @@ export function libraryContextOverlayHeight(bottomClearance: number): number {
|
|||||||
LIBRARY_CONTEXT_BAR_HEIGHT;
|
LIBRARY_CONTEXT_BAR_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Keep the A-Z gesture surface above phone-only floating Library chrome. */
|
||||||
|
export function libraryRailBottomClearance(
|
||||||
|
phoneContextBar: boolean,
|
||||||
|
contextOverlayHeight: number,
|
||||||
|
): number {
|
||||||
|
return phoneContextBar ? Math.max(0, contextOverlayHeight) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the shared bottom fade above the command bar instead of at its edge,
|
* Starts the shared bottom fade above the command bar instead of at its edge,
|
||||||
* so rows disappear gradually behind both pieces of floating chrome.
|
* so rows disappear gradually behind both pieces of floating chrome.
|
||||||
|
|||||||
+214
-26
@@ -18,9 +18,22 @@ import {
|
|||||||
} from '@/library/scanner';
|
} from '@/library/scanner';
|
||||||
import { endScanService, reportScanProgress } from '@/library/scanService';
|
import { endScanService, reportScanProgress } from '@/library/scanService';
|
||||||
import { requeueMissingArtistImages } from '@/library/artistImageLookup';
|
import { requeueMissingArtistImages } from '@/library/artistImageLookup';
|
||||||
import { ALBUM_SORT_LABELS, type AlbumSort } from '@/lib/albumSort';
|
import {
|
||||||
import { ARTIST_SORT_LABELS, type ArtistSort } from '@/lib/artistSort';
|
ALBUM_SORT_LABELS,
|
||||||
import { TRACK_SORT_LABELS, type TrackSort } from '@/lib/trackSort';
|
ALBUM_SORT_LEGACY_DIRECTIONS,
|
||||||
|
type AlbumSort,
|
||||||
|
} from '@/lib/albumSort';
|
||||||
|
import {
|
||||||
|
ARTIST_SORT_LABELS,
|
||||||
|
ARTIST_SORT_LEGACY_DIRECTIONS,
|
||||||
|
type ArtistSort,
|
||||||
|
} from '@/lib/artistSort';
|
||||||
|
import {
|
||||||
|
TRACK_SORT_LABELS,
|
||||||
|
TRACK_SORT_LEGACY_DIRECTIONS,
|
||||||
|
type TrackSort,
|
||||||
|
} from '@/lib/trackSort';
|
||||||
|
import { parseSortDirection, type SortDirection } from '@/lib/sortDirection';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LIBRARY_LAYOUT,
|
DEFAULT_LIBRARY_LAYOUT,
|
||||||
parseLibraryLayout,
|
parseLibraryLayout,
|
||||||
@@ -35,6 +48,9 @@ const VIEW_MODE_KEY = 'library_view_mode';
|
|||||||
const TRACK_SORT_KEY = 'library_track_sort';
|
const TRACK_SORT_KEY = 'library_track_sort';
|
||||||
const ALBUM_SORT_KEY = 'library_album_sort';
|
const ALBUM_SORT_KEY = 'library_album_sort';
|
||||||
const ARTIST_SORT_KEY = 'library_artist_sort';
|
const ARTIST_SORT_KEY = 'library_artist_sort';
|
||||||
|
const TRACK_SORT_DIRECTION_KEY = 'library_track_sort_direction';
|
||||||
|
const ALBUM_SORT_DIRECTION_KEY = 'library_album_sort_direction';
|
||||||
|
const ARTIST_SORT_DIRECTION_KEY = 'library_artist_sort_direction';
|
||||||
const ALBUM_LAYOUT_KEY = 'library_album_layout';
|
const ALBUM_LAYOUT_KEY = 'library_album_layout';
|
||||||
const ARTIST_LAYOUT_KEY = 'library_artist_layout';
|
const ARTIST_LAYOUT_KEY = 'library_artist_layout';
|
||||||
const INCLUDE_COLLAB_ARTISTS_KEY = 'library_include_collab_artists';
|
const INCLUDE_COLLAB_ARTISTS_KEY = 'library_include_collab_artists';
|
||||||
@@ -92,6 +108,9 @@ interface LibraryStore {
|
|||||||
trackSort: TrackSort;
|
trackSort: TrackSort;
|
||||||
albumSort: AlbumSort;
|
albumSort: AlbumSort;
|
||||||
artistSort: ArtistSort;
|
artistSort: ArtistSort;
|
||||||
|
trackSortDirection: SortDirection;
|
||||||
|
albumSortDirection: SortDirection;
|
||||||
|
artistSortDirection: SortDirection;
|
||||||
albumLayout: LibraryLayout;
|
albumLayout: LibraryLayout;
|
||||||
artistLayout: LibraryLayout;
|
artistLayout: LibraryLayout;
|
||||||
includeCollabArtists: boolean;
|
includeCollabArtists: boolean;
|
||||||
@@ -133,6 +152,9 @@ interface LibraryStore {
|
|||||||
setTrackSort: (sort: TrackSort) => void;
|
setTrackSort: (sort: TrackSort) => void;
|
||||||
setAlbumSort: (sort: AlbumSort) => void;
|
setAlbumSort: (sort: AlbumSort) => void;
|
||||||
setArtistSort: (sort: ArtistSort) => void;
|
setArtistSort: (sort: ArtistSort) => void;
|
||||||
|
setTrackSortDirection: (direction: SortDirection) => void;
|
||||||
|
setAlbumSortDirection: (direction: SortDirection) => void;
|
||||||
|
setArtistSortDirection: (direction: SortDirection) => void;
|
||||||
setAlbumLayout: (layout: LibraryLayout) => void;
|
setAlbumLayout: (layout: LibraryLayout) => void;
|
||||||
setArtistLayout: (layout: LibraryLayout) => void;
|
setArtistLayout: (layout: LibraryLayout) => void;
|
||||||
setIncludeCollabArtists: (include: boolean) => void;
|
setIncludeCollabArtists: (include: boolean) => void;
|
||||||
@@ -214,15 +236,18 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const readTrackPage = (
|
const readTrackPage = (
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
sort = get().trackSort,
|
sort = get().trackSort,
|
||||||
) => AstraLibraryData.getTrackPage<DbTrack>(sort, cursor, PAGE_SIZE);
|
direction = get().trackSortDirection,
|
||||||
|
) => AstraLibraryData.getTrackPage<DbTrack>(sort, direction, cursor, PAGE_SIZE);
|
||||||
|
|
||||||
const readAlbumPage = (
|
const readAlbumPage = (
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
sort = get().albumSort,
|
sort = get().albumSort,
|
||||||
|
direction = get().albumSortDirection,
|
||||||
includeSingles = useSettingsStore.getState().includeSingles,
|
includeSingles = useSettingsStore.getState().includeSingles,
|
||||||
) =>
|
) =>
|
||||||
AstraLibraryData.getAlbumPage<Album>(
|
AstraLibraryData.getAlbumPage<Album>(
|
||||||
sort,
|
sort,
|
||||||
|
direction,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
cursor,
|
cursor,
|
||||||
PAGE_SIZE
|
PAGE_SIZE
|
||||||
@@ -231,11 +256,13 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const readArtistPage = (
|
const readArtistPage = (
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
sort = get().artistSort,
|
sort = get().artistSort,
|
||||||
|
direction = get().artistSortDirection,
|
||||||
groupingMode = useSettingsStore.getState().artistGroupingMode,
|
groupingMode = useSettingsStore.getState().artistGroupingMode,
|
||||||
includeCollaborations = get().includeCollabArtists,
|
includeCollaborations = get().includeCollabArtists,
|
||||||
) =>
|
) =>
|
||||||
AstraLibraryData.getArtistPage<Artist>(
|
AstraLibraryData.getArtistPage<Artist>(
|
||||||
sort,
|
sort,
|
||||||
|
direction,
|
||||||
groupingMode,
|
groupingMode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
cursor,
|
cursor,
|
||||||
@@ -247,21 +274,25 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const readTrackPageBefore = (
|
const readTrackPageBefore = (
|
||||||
cursor: string,
|
cursor: string,
|
||||||
sort: 'artist' | 'title',
|
sort: 'artist' | 'title',
|
||||||
) => AstraLibraryData.getTrackPageBefore<DbTrack>(sort, cursor, PAGE_SIZE);
|
direction = get().trackSortDirection,
|
||||||
|
) => AstraLibraryData.getTrackPageBefore<DbTrack>(sort, direction, cursor, PAGE_SIZE);
|
||||||
|
|
||||||
const readAlbumPageBefore = (
|
const readAlbumPageBefore = (
|
||||||
cursor: string,
|
cursor: string,
|
||||||
sort: 'artist' | 'name',
|
sort: 'artist' | 'name',
|
||||||
|
direction = get().albumSortDirection,
|
||||||
includeSingles = useSettingsStore.getState().includeSingles,
|
includeSingles = useSettingsStore.getState().includeSingles,
|
||||||
) => AstraLibraryData.getAlbumPageBefore<Album>(sort, includeSingles, cursor, PAGE_SIZE);
|
) => AstraLibraryData.getAlbumPageBefore<Album>(sort, direction, includeSingles, cursor, PAGE_SIZE);
|
||||||
|
|
||||||
const readArtistPageBefore = (
|
const readArtistPageBefore = (
|
||||||
cursor: string,
|
cursor: string,
|
||||||
|
direction = get().artistSortDirection,
|
||||||
groupingMode = useSettingsStore.getState().artistGroupingMode,
|
groupingMode = useSettingsStore.getState().artistGroupingMode,
|
||||||
includeCollaborations = get().includeCollabArtists,
|
includeCollaborations = get().includeCollabArtists,
|
||||||
) =>
|
) =>
|
||||||
AstraLibraryData.getArtistPageBefore<Artist>(
|
AstraLibraryData.getArtistPageBefore<Artist>(
|
||||||
'name',
|
'name',
|
||||||
|
direction,
|
||||||
groupingMode,
|
groupingMode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
cursor,
|
cursor,
|
||||||
@@ -280,9 +311,14 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
// PAGE_SIZE change would break it silently, leaving the list parked mid-catalog.
|
// PAGE_SIZE change would break it silently, leaving the list parked mid-catalog.
|
||||||
const resetTracks = async (forceRemount = false) => {
|
const resetTracks = async (forceRemount = false) => {
|
||||||
const sort = get().trackSort;
|
const sort = get().trackSort;
|
||||||
|
const direction = get().trackSortDirection;
|
||||||
const generation = ++pageGenerations.tracks;
|
const generation = ++pageGenerations.tracks;
|
||||||
const page = await readTrackPage(null, sort);
|
const page = await readTrackPage(null, sort, direction);
|
||||||
if (generation !== pageGenerations.tracks || get().trackSort !== sort) return false;
|
if (
|
||||||
|
generation !== pageGenerations.tracks ||
|
||||||
|
get().trackSort !== sort ||
|
||||||
|
get().trackSortDirection !== direction
|
||||||
|
) return false;
|
||||||
const items = page.items ?? [];
|
const items = page.items ?? [];
|
||||||
set((current) => ({
|
set((current) => ({
|
||||||
tracks: items,
|
tracks: items,
|
||||||
@@ -297,12 +333,14 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
|
|
||||||
const resetAlbums = async (forceRemount = false) => {
|
const resetAlbums = async (forceRemount = false) => {
|
||||||
const sort = get().albumSort;
|
const sort = get().albumSort;
|
||||||
|
const direction = get().albumSortDirection;
|
||||||
const includeSingles = useSettingsStore.getState().includeSingles;
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
const generation = ++pageGenerations.albums;
|
const generation = ++pageGenerations.albums;
|
||||||
const page = await readAlbumPage(null, sort, includeSingles);
|
const page = await readAlbumPage(null, sort, direction, includeSingles);
|
||||||
if (
|
if (
|
||||||
generation !== pageGenerations.albums ||
|
generation !== pageGenerations.albums ||
|
||||||
get().albumSort !== sort ||
|
get().albumSort !== sort ||
|
||||||
|
get().albumSortDirection !== direction ||
|
||||||
useSettingsStore.getState().includeSingles !== includeSingles
|
useSettingsStore.getState().includeSingles !== includeSingles
|
||||||
) return false;
|
) return false;
|
||||||
const items = page.items ?? [];
|
const items = page.items ?? [];
|
||||||
@@ -318,13 +356,21 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
|
|
||||||
const resetArtists = async (forceRemount = false) => {
|
const resetArtists = async (forceRemount = false) => {
|
||||||
const sort = get().artistSort;
|
const sort = get().artistSort;
|
||||||
|
const direction = get().artistSortDirection;
|
||||||
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
const includeCollaborations = get().includeCollabArtists;
|
const includeCollaborations = get().includeCollabArtists;
|
||||||
const generation = ++pageGenerations.artists;
|
const generation = ++pageGenerations.artists;
|
||||||
const page = await readArtistPage(null, sort, groupingMode, includeCollaborations);
|
const page = await readArtistPage(
|
||||||
|
null,
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
groupingMode,
|
||||||
|
includeCollaborations,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
generation !== pageGenerations.artists ||
|
generation !== pageGenerations.artists ||
|
||||||
get().artistSort !== sort ||
|
get().artistSort !== sort ||
|
||||||
|
get().artistSortDirection !== direction ||
|
||||||
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
get().includeCollabArtists !== includeCollaborations
|
get().includeCollabArtists !== includeCollaborations
|
||||||
) return false;
|
) return false;
|
||||||
@@ -356,11 +402,18 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
: state.viewMode === 'albums'
|
: state.viewMode === 'albums'
|
||||||
? state.albumSort as 'artist' | 'name'
|
? state.albumSort as 'artist' | 'name'
|
||||||
: 'name';
|
: 'name';
|
||||||
|
const direction =
|
||||||
|
state.viewMode === 'tracks'
|
||||||
|
? state.trackSortDirection
|
||||||
|
: state.viewMode === 'albums'
|
||||||
|
? state.albumSortDirection
|
||||||
|
: state.artistSortDirection;
|
||||||
const includeSingles = useSettingsStore.getState().includeSingles;
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
const anchors = await AstraLibraryData.getSectionAnchors(
|
const anchors = await AstraLibraryData.getSectionAnchors(
|
||||||
state.viewMode as 'tracks' | 'albums' | 'artists',
|
state.viewMode as 'tracks' | 'albums' | 'artists',
|
||||||
sort,
|
sort,
|
||||||
|
direction,
|
||||||
includeSingles,
|
includeSingles,
|
||||||
groupingMode,
|
groupingMode,
|
||||||
state.includeCollabArtists
|
state.includeCollabArtists
|
||||||
@@ -372,10 +425,17 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
: current.viewMode === 'albums'
|
: current.viewMode === 'albums'
|
||||||
? current.albumSort
|
? current.albumSort
|
||||||
: current.artistSort;
|
: current.artistSort;
|
||||||
|
const currentDirection =
|
||||||
|
current.viewMode === 'tracks'
|
||||||
|
? current.trackSortDirection
|
||||||
|
: current.viewMode === 'albums'
|
||||||
|
? current.albumSortDirection
|
||||||
|
: current.artistSortDirection;
|
||||||
if (
|
if (
|
||||||
generation !== anchorGeneration ||
|
generation !== anchorGeneration ||
|
||||||
current.viewMode !== state.viewMode ||
|
current.viewMode !== state.viewMode ||
|
||||||
currentSort !== sort ||
|
currentSort !== sort ||
|
||||||
|
currentDirection !== direction ||
|
||||||
useSettingsStore.getState().includeSingles !== includeSingles ||
|
useSettingsStore.getState().includeSingles !== includeSingles ||
|
||||||
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
current.includeCollabArtists !== state.includeCollabArtists
|
current.includeCollabArtists !== state.includeCollabArtists
|
||||||
@@ -432,6 +492,9 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
trackSort: 'title',
|
trackSort: 'title',
|
||||||
albumSort: 'name',
|
albumSort: 'name',
|
||||||
artistSort: 'name',
|
artistSort: 'name',
|
||||||
|
trackSortDirection: 'asc',
|
||||||
|
albumSortDirection: 'asc',
|
||||||
|
artistSortDirection: 'asc',
|
||||||
albumLayout: DEFAULT_LIBRARY_LAYOUT,
|
albumLayout: DEFAULT_LIBRARY_LAYOUT,
|
||||||
artistLayout: DEFAULT_LIBRARY_LAYOUT,
|
artistLayout: DEFAULT_LIBRARY_LAYOUT,
|
||||||
includeCollabArtists: false,
|
includeCollabArtists: false,
|
||||||
@@ -468,6 +531,9 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
TRACK_SORT_KEY,
|
TRACK_SORT_KEY,
|
||||||
ALBUM_SORT_KEY,
|
ALBUM_SORT_KEY,
|
||||||
ARTIST_SORT_KEY,
|
ARTIST_SORT_KEY,
|
||||||
|
TRACK_SORT_DIRECTION_KEY,
|
||||||
|
ALBUM_SORT_DIRECTION_KEY,
|
||||||
|
ARTIST_SORT_DIRECTION_KEY,
|
||||||
ALBUM_LAYOUT_KEY,
|
ALBUM_LAYOUT_KEY,
|
||||||
ARTIST_LAYOUT_KEY,
|
ARTIST_LAYOUT_KEY,
|
||||||
INCLUDE_COLLAB_ARTISTS_KEY,
|
INCLUDE_COLLAB_ARTISTS_KEY,
|
||||||
@@ -476,15 +542,42 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const trackSort = parseTrackSort(values[TRACK_SORT_KEY] ?? null);
|
const trackSort = parseTrackSort(values[TRACK_SORT_KEY] ?? null);
|
||||||
const albumSort = parseAlbumSort(values[ALBUM_SORT_KEY] ?? null);
|
const albumSort = parseAlbumSort(values[ALBUM_SORT_KEY] ?? null);
|
||||||
const artistSort = parseArtistSort(values[ARTIST_SORT_KEY] ?? null);
|
const artistSort = parseArtistSort(values[ARTIST_SORT_KEY] ?? null);
|
||||||
|
const restoredTrackSort = trackSort ?? get().trackSort;
|
||||||
|
const restoredAlbumSort = albumSort ?? get().albumSort;
|
||||||
|
const restoredArtistSort = artistSort ?? get().artistSort;
|
||||||
|
const trackSortDirection =
|
||||||
|
parseSortDirection(values[TRACK_SORT_DIRECTION_KEY] ?? null) ??
|
||||||
|
TRACK_SORT_LEGACY_DIRECTIONS[restoredTrackSort];
|
||||||
|
const albumSortDirection =
|
||||||
|
parseSortDirection(values[ALBUM_SORT_DIRECTION_KEY] ?? null) ??
|
||||||
|
ALBUM_SORT_LEGACY_DIRECTIONS[restoredAlbumSort];
|
||||||
|
const artistSortDirection =
|
||||||
|
parseSortDirection(values[ARTIST_SORT_DIRECTION_KEY] ?? null) ??
|
||||||
|
ARTIST_SORT_LEGACY_DIRECTIONS[restoredArtistSort];
|
||||||
set({
|
set({
|
||||||
...(viewMode ? { viewMode } : {}),
|
...(viewMode ? { viewMode } : {}),
|
||||||
...(trackSort ? { trackSort } : {}),
|
...(trackSort ? { trackSort } : {}),
|
||||||
...(albumSort ? { albumSort } : {}),
|
...(albumSort ? { albumSort } : {}),
|
||||||
...(artistSort ? { artistSort } : {}),
|
...(artistSort ? { artistSort } : {}),
|
||||||
|
trackSortDirection,
|
||||||
|
albumSortDirection,
|
||||||
|
artistSortDirection,
|
||||||
albumLayout: parseLibraryLayout(values[ALBUM_LAYOUT_KEY] ?? null),
|
albumLayout: parseLibraryLayout(values[ALBUM_LAYOUT_KEY] ?? null),
|
||||||
artistLayout: parseLibraryLayout(values[ARTIST_LAYOUT_KEY] ?? null),
|
artistLayout: parseLibraryLayout(values[ARTIST_LAYOUT_KEY] ?? null),
|
||||||
includeCollabArtists: values[INCLUDE_COLLAB_ARTISTS_KEY] === 'true',
|
includeCollabArtists: values[INCLUDE_COLLAB_ARTISTS_KEY] === 'true',
|
||||||
});
|
});
|
||||||
|
// Direction keys were introduced after sort-field persistence. Write
|
||||||
|
// the derived legacy direction once so subsequent field changes keep
|
||||||
|
// the per-view preference instead of re-deriving it.
|
||||||
|
if (!parseSortDirection(values[TRACK_SORT_DIRECTION_KEY] ?? null)) {
|
||||||
|
persistSetting(TRACK_SORT_DIRECTION_KEY, trackSortDirection);
|
||||||
|
}
|
||||||
|
if (!parseSortDirection(values[ALBUM_SORT_DIRECTION_KEY] ?? null)) {
|
||||||
|
persistSetting(ALBUM_SORT_DIRECTION_KEY, albumSortDirection);
|
||||||
|
}
|
||||||
|
if (!parseSortDirection(values[ARTIST_SORT_DIRECTION_KEY] ?? null)) {
|
||||||
|
persistSetting(ARTIST_SORT_DIRECTION_KEY, artistSortDirection);
|
||||||
|
}
|
||||||
|
|
||||||
if (!nativeSubscriptionsInstalled) {
|
if (!nativeSubscriptionsInstalled) {
|
||||||
nativeSubscriptionsInstalled = true;
|
nativeSubscriptionsInstalled = true;
|
||||||
@@ -548,6 +641,9 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const trackSort = stateAtStart.trackSort;
|
const trackSort = stateAtStart.trackSort;
|
||||||
const albumSort = stateAtStart.albumSort;
|
const albumSort = stateAtStart.albumSort;
|
||||||
const artistSort = stateAtStart.artistSort;
|
const artistSort = stateAtStart.artistSort;
|
||||||
|
const trackSortDirection = stateAtStart.trackSortDirection;
|
||||||
|
const albumSortDirection = stateAtStart.albumSortDirection;
|
||||||
|
const artistSortDirection = stateAtStart.artistSortDirection;
|
||||||
const includeSingles = useSettingsStore.getState().includeSingles;
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
const includeCollaborations = stateAtStart.includeCollabArtists;
|
const includeCollaborations = stateAtStart.includeCollabArtists;
|
||||||
@@ -568,21 +664,31 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
folders,
|
folders,
|
||||||
recentlyPlayedTracks,
|
recentlyPlayedTracks,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
viewMode === 'tracks' ? readTrackPage(null, trackSort) : Promise.resolve(null),
|
viewMode === 'tracks'
|
||||||
|
? readTrackPage(null, trackSort, trackSortDirection)
|
||||||
|
: Promise.resolve(null),
|
||||||
viewMode === 'albums'
|
viewMode === 'albums'
|
||||||
? readAlbumPage(null, albumSort, includeSingles)
|
? readAlbumPage(null, albumSort, albumSortDirection, includeSingles)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
viewMode === 'artists'
|
viewMode === 'artists'
|
||||||
? readArtistPage(null, artistSort, groupingMode, includeCollaborations)
|
? readArtistPage(
|
||||||
|
null,
|
||||||
|
artistSort,
|
||||||
|
artistSortDirection,
|
||||||
|
groupingMode,
|
||||||
|
includeCollaborations,
|
||||||
|
)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
AstraLibraryData.getAlbumPage<Album>(
|
AstraLibraryData.getAlbumPage<Album>(
|
||||||
'recently_added',
|
'recently_added',
|
||||||
|
'desc',
|
||||||
includeSingles,
|
includeSingles,
|
||||||
null,
|
null,
|
||||||
20
|
20
|
||||||
),
|
),
|
||||||
AstraLibraryData.getArtistPage<Artist>(
|
AstraLibraryData.getArtistPage<Artist>(
|
||||||
'name',
|
'name',
|
||||||
|
'asc',
|
||||||
groupingMode,
|
groupingMode,
|
||||||
includeCollaborations,
|
includeCollaborations,
|
||||||
null,
|
null,
|
||||||
@@ -596,17 +702,20 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
viewMode === 'tracks' &&
|
viewMode === 'tracks' &&
|
||||||
current.viewMode === 'tracks' &&
|
current.viewMode === 'tracks' &&
|
||||||
current.trackSort === trackSort &&
|
current.trackSort === trackSort &&
|
||||||
|
current.trackSortDirection === trackSortDirection &&
|
||||||
activeGeneration === pageGenerations.tracks;
|
activeGeneration === pageGenerations.tracks;
|
||||||
const canApplyAlbumPage =
|
const canApplyAlbumPage =
|
||||||
viewMode === 'albums' &&
|
viewMode === 'albums' &&
|
||||||
current.viewMode === 'albums' &&
|
current.viewMode === 'albums' &&
|
||||||
current.albumSort === albumSort &&
|
current.albumSort === albumSort &&
|
||||||
|
current.albumSortDirection === albumSortDirection &&
|
||||||
useSettingsStore.getState().includeSingles === includeSingles &&
|
useSettingsStore.getState().includeSingles === includeSingles &&
|
||||||
activeGeneration === pageGenerations.albums;
|
activeGeneration === pageGenerations.albums;
|
||||||
const canApplyArtistPage =
|
const canApplyArtistPage =
|
||||||
viewMode === 'artists' &&
|
viewMode === 'artists' &&
|
||||||
current.viewMode === 'artists' &&
|
current.viewMode === 'artists' &&
|
||||||
current.artistSort === artistSort &&
|
current.artistSort === artistSort &&
|
||||||
|
current.artistSortDirection === artistSortDirection &&
|
||||||
useSettingsStore.getState().artistGroupingMode === groupingMode &&
|
useSettingsStore.getState().artistGroupingMode === groupingMode &&
|
||||||
current.includeCollabArtists === includeCollaborations &&
|
current.includeCollabArtists === includeCollaborations &&
|
||||||
activeGeneration === pageGenerations.artists;
|
activeGeneration === pageGenerations.artists;
|
||||||
@@ -647,13 +756,15 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const cursor = state.trackNextCursor;
|
const cursor = state.trackNextCursor;
|
||||||
if (!cursor || forwardBusy.tracks) return;
|
if (!cursor || forwardBusy.tracks) return;
|
||||||
const sort = state.trackSort;
|
const sort = state.trackSort;
|
||||||
|
const direction = state.trackSortDirection;
|
||||||
const pageGeneration = pageGenerations.tracks;
|
const pageGeneration = pageGenerations.tracks;
|
||||||
forwardBusy.tracks = true;
|
forwardBusy.tracks = true;
|
||||||
try {
|
try {
|
||||||
const page = await readTrackPage(cursor, sort);
|
const page = await readTrackPage(cursor, sort, direction);
|
||||||
if (
|
if (
|
||||||
pageGeneration !== pageGenerations.tracks ||
|
pageGeneration !== pageGenerations.tracks ||
|
||||||
get().trackSort !== sort ||
|
get().trackSort !== sort ||
|
||||||
|
get().trackSortDirection !== direction ||
|
||||||
get().trackNextCursor !== cursor
|
get().trackNextCursor !== cursor
|
||||||
) return;
|
) return;
|
||||||
if (page.error === 'STALE_REVISION') {
|
if (page.error === 'STALE_REVISION') {
|
||||||
@@ -674,14 +785,16 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const cursor = state.albumNextCursor;
|
const cursor = state.albumNextCursor;
|
||||||
if (!cursor || forwardBusy.albums) return;
|
if (!cursor || forwardBusy.albums) return;
|
||||||
const sort = state.albumSort;
|
const sort = state.albumSort;
|
||||||
|
const direction = state.albumSortDirection;
|
||||||
const includeSingles = useSettingsStore.getState().includeSingles;
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
const pageGeneration = pageGenerations.albums;
|
const pageGeneration = pageGenerations.albums;
|
||||||
forwardBusy.albums = true;
|
forwardBusy.albums = true;
|
||||||
try {
|
try {
|
||||||
const page = await readAlbumPage(cursor, sort, includeSingles);
|
const page = await readAlbumPage(cursor, sort, direction, includeSingles);
|
||||||
if (
|
if (
|
||||||
pageGeneration !== pageGenerations.albums ||
|
pageGeneration !== pageGenerations.albums ||
|
||||||
get().albumSort !== sort ||
|
get().albumSort !== sort ||
|
||||||
|
get().albumSortDirection !== direction ||
|
||||||
useSettingsStore.getState().includeSingles !== includeSingles ||
|
useSettingsStore.getState().includeSingles !== includeSingles ||
|
||||||
get().albumNextCursor !== cursor
|
get().albumNextCursor !== cursor
|
||||||
) return;
|
) return;
|
||||||
@@ -703,15 +816,23 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const cursor = state.artistNextCursor;
|
const cursor = state.artistNextCursor;
|
||||||
if (!cursor || forwardBusy.artists) return;
|
if (!cursor || forwardBusy.artists) return;
|
||||||
const sort = state.artistSort;
|
const sort = state.artistSort;
|
||||||
|
const direction = state.artistSortDirection;
|
||||||
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
const includeCollaborations = state.includeCollabArtists;
|
const includeCollaborations = state.includeCollabArtists;
|
||||||
const pageGeneration = pageGenerations.artists;
|
const pageGeneration = pageGenerations.artists;
|
||||||
forwardBusy.artists = true;
|
forwardBusy.artists = true;
|
||||||
try {
|
try {
|
||||||
const page = await readArtistPage(cursor, sort, groupingMode, includeCollaborations);
|
const page = await readArtistPage(
|
||||||
|
cursor,
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
groupingMode,
|
||||||
|
includeCollaborations,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
pageGeneration !== pageGenerations.artists ||
|
pageGeneration !== pageGenerations.artists ||
|
||||||
get().artistSort !== sort ||
|
get().artistSort !== sort ||
|
||||||
|
get().artistSortDirection !== direction ||
|
||||||
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
get().includeCollabArtists !== includeCollaborations ||
|
get().includeCollabArtists !== includeCollaborations ||
|
||||||
get().artistNextCursor !== cursor
|
get().artistNextCursor !== cursor
|
||||||
@@ -733,14 +854,16 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const state = get();
|
const state = get();
|
||||||
const cursor = state.trackPrevCursor;
|
const cursor = state.trackPrevCursor;
|
||||||
const sort = backwardTrackSort(state.trackSort);
|
const sort = backwardTrackSort(state.trackSort);
|
||||||
|
const direction = state.trackSortDirection;
|
||||||
if (!cursor || !sort || backwardBusy.tracks) return;
|
if (!cursor || !sort || backwardBusy.tracks) return;
|
||||||
const pageGeneration = pageGenerations.tracks;
|
const pageGeneration = pageGenerations.tracks;
|
||||||
backwardBusy.tracks = true;
|
backwardBusy.tracks = true;
|
||||||
try {
|
try {
|
||||||
const page = await readTrackPageBefore(cursor, sort);
|
const page = await readTrackPageBefore(cursor, sort, direction);
|
||||||
if (
|
if (
|
||||||
pageGeneration !== pageGenerations.tracks ||
|
pageGeneration !== pageGenerations.tracks ||
|
||||||
get().trackSort !== sort ||
|
get().trackSort !== sort ||
|
||||||
|
get().trackSortDirection !== direction ||
|
||||||
get().trackPrevCursor !== cursor
|
get().trackPrevCursor !== cursor
|
||||||
) return;
|
) return;
|
||||||
if (page.error === 'STALE_REVISION') {
|
if (page.error === 'STALE_REVISION') {
|
||||||
@@ -760,15 +883,17 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const state = get();
|
const state = get();
|
||||||
const cursor = state.albumPrevCursor;
|
const cursor = state.albumPrevCursor;
|
||||||
const sort = backwardAlbumSort(state.albumSort);
|
const sort = backwardAlbumSort(state.albumSort);
|
||||||
|
const direction = state.albumSortDirection;
|
||||||
if (!cursor || !sort || backwardBusy.albums) return;
|
if (!cursor || !sort || backwardBusy.albums) return;
|
||||||
const includeSingles = useSettingsStore.getState().includeSingles;
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
const pageGeneration = pageGenerations.albums;
|
const pageGeneration = pageGenerations.albums;
|
||||||
backwardBusy.albums = true;
|
backwardBusy.albums = true;
|
||||||
try {
|
try {
|
||||||
const page = await readAlbumPageBefore(cursor, sort, includeSingles);
|
const page = await readAlbumPageBefore(cursor, sort, direction, includeSingles);
|
||||||
if (
|
if (
|
||||||
pageGeneration !== pageGenerations.albums ||
|
pageGeneration !== pageGenerations.albums ||
|
||||||
get().albumSort !== sort ||
|
get().albumSort !== sort ||
|
||||||
|
get().albumSortDirection !== direction ||
|
||||||
useSettingsStore.getState().includeSingles !== includeSingles ||
|
useSettingsStore.getState().includeSingles !== includeSingles ||
|
||||||
get().albumPrevCursor !== cursor
|
get().albumPrevCursor !== cursor
|
||||||
) return;
|
) return;
|
||||||
@@ -789,15 +914,22 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const state = get();
|
const state = get();
|
||||||
const cursor = state.artistPrevCursor;
|
const cursor = state.artistPrevCursor;
|
||||||
if (!cursor || state.artistSort !== 'name' || backwardBusy.artists) return;
|
if (!cursor || state.artistSort !== 'name' || backwardBusy.artists) return;
|
||||||
|
const direction = state.artistSortDirection;
|
||||||
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
const includeCollaborations = state.includeCollabArtists;
|
const includeCollaborations = state.includeCollabArtists;
|
||||||
const pageGeneration = pageGenerations.artists;
|
const pageGeneration = pageGenerations.artists;
|
||||||
backwardBusy.artists = true;
|
backwardBusy.artists = true;
|
||||||
try {
|
try {
|
||||||
const page = await readArtistPageBefore(cursor, groupingMode, includeCollaborations);
|
const page = await readArtistPageBefore(
|
||||||
|
cursor,
|
||||||
|
direction,
|
||||||
|
groupingMode,
|
||||||
|
includeCollaborations,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
pageGeneration !== pageGenerations.artists ||
|
pageGeneration !== pageGenerations.artists ||
|
||||||
get().artistSort !== 'name' ||
|
get().artistSort !== 'name' ||
|
||||||
|
get().artistSortDirection !== direction ||
|
||||||
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
get().includeCollabArtists !== includeCollaborations ||
|
get().includeCollabArtists !== includeCollaborations ||
|
||||||
get().artistPrevCursor !== cursor
|
get().artistPrevCursor !== cursor
|
||||||
@@ -827,15 +959,19 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
const generation = ++pageGenerations[viewMode];
|
const generation = ++pageGenerations[viewMode];
|
||||||
if (viewMode === 'tracks') {
|
if (viewMode === 'tracks') {
|
||||||
const sort = state.trackSort;
|
const sort = state.trackSort;
|
||||||
|
const direction = state.trackSortDirection;
|
||||||
const backwardSort = backwardTrackSort(sort);
|
const backwardSort = backwardTrackSort(sort);
|
||||||
const [page, before] = await Promise.all([
|
const [page, before] = await Promise.all([
|
||||||
readTrackPage(cursor, sort),
|
readTrackPage(cursor, sort, direction),
|
||||||
backwardSort ? readTrackPageBefore(cursor, backwardSort) : Promise.resolve(null),
|
backwardSort
|
||||||
|
? readTrackPageBefore(cursor, backwardSort, direction)
|
||||||
|
: Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
if (
|
if (
|
||||||
generation !== pageGenerations.tracks ||
|
generation !== pageGenerations.tracks ||
|
||||||
get().viewMode !== viewMode ||
|
get().viewMode !== viewMode ||
|
||||||
get().trackSort !== sort
|
get().trackSort !== sort ||
|
||||||
|
get().trackSortDirection !== direction
|
||||||
) return false;
|
) return false;
|
||||||
if (page.error === 'STALE_REVISION') {
|
if (page.error === 'STALE_REVISION') {
|
||||||
await resetTracks();
|
await resetTracks();
|
||||||
@@ -856,18 +992,20 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
}));
|
}));
|
||||||
} else if (viewMode === 'albums') {
|
} else if (viewMode === 'albums') {
|
||||||
const sort = state.albumSort;
|
const sort = state.albumSort;
|
||||||
|
const direction = state.albumSortDirection;
|
||||||
const includeSingles = useSettingsStore.getState().includeSingles;
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
const backwardSort = backwardAlbumSort(sort);
|
const backwardSort = backwardAlbumSort(sort);
|
||||||
const [page, before] = await Promise.all([
|
const [page, before] = await Promise.all([
|
||||||
readAlbumPage(cursor, sort, includeSingles),
|
readAlbumPage(cursor, sort, direction, includeSingles),
|
||||||
backwardSort
|
backwardSort
|
||||||
? readAlbumPageBefore(cursor, backwardSort, includeSingles)
|
? readAlbumPageBefore(cursor, backwardSort, direction, includeSingles)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
if (
|
if (
|
||||||
generation !== pageGenerations.albums ||
|
generation !== pageGenerations.albums ||
|
||||||
get().viewMode !== viewMode ||
|
get().viewMode !== viewMode ||
|
||||||
get().albumSort !== sort ||
|
get().albumSort !== sort ||
|
||||||
|
get().albumSortDirection !== direction ||
|
||||||
useSettingsStore.getState().includeSingles !== includeSingles
|
useSettingsStore.getState().includeSingles !== includeSingles
|
||||||
) return false;
|
) return false;
|
||||||
if (page.error === 'STALE_REVISION') {
|
if (page.error === 'STALE_REVISION') {
|
||||||
@@ -888,18 +1026,20 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
const sort = state.artistSort;
|
const sort = state.artistSort;
|
||||||
|
const direction = state.artistSortDirection;
|
||||||
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
const includeCollaborations = state.includeCollabArtists;
|
const includeCollaborations = state.includeCollabArtists;
|
||||||
const [page, before] = await Promise.all([
|
const [page, before] = await Promise.all([
|
||||||
readArtistPage(cursor, sort, groupingMode, includeCollaborations),
|
readArtistPage(cursor, sort, direction, groupingMode, includeCollaborations),
|
||||||
sort === 'name'
|
sort === 'name'
|
||||||
? readArtistPageBefore(cursor, groupingMode, includeCollaborations)
|
? readArtistPageBefore(cursor, direction, groupingMode, includeCollaborations)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
if (
|
if (
|
||||||
generation !== pageGenerations.artists ||
|
generation !== pageGenerations.artists ||
|
||||||
get().viewMode !== viewMode ||
|
get().viewMode !== viewMode ||
|
||||||
get().artistSort !== sort ||
|
get().artistSort !== sort ||
|
||||||
|
get().artistSortDirection !== direction ||
|
||||||
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
get().includeCollabArtists !== includeCollaborations
|
get().includeCollabArtists !== includeCollaborations
|
||||||
) return false;
|
) return false;
|
||||||
@@ -1048,6 +1188,54 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setTrackSortDirection: (trackSortDirection) => {
|
||||||
|
if (get().trackSortDirection === trackSortDirection) return;
|
||||||
|
anchorGeneration += 1;
|
||||||
|
set({
|
||||||
|
trackSortDirection,
|
||||||
|
tracks: [],
|
||||||
|
trackNextCursor: null,
|
||||||
|
trackPrevCursor: null,
|
||||||
|
jumpAnchorIndex: 0,
|
||||||
|
sectionAnchors: [],
|
||||||
|
});
|
||||||
|
persistSetting(TRACK_SORT_DIRECTION_KEY, trackSortDirection);
|
||||||
|
void resetTracks(true);
|
||||||
|
void resetSectionAnchors();
|
||||||
|
},
|
||||||
|
|
||||||
|
setAlbumSortDirection: (albumSortDirection) => {
|
||||||
|
if (get().albumSortDirection === albumSortDirection) return;
|
||||||
|
anchorGeneration += 1;
|
||||||
|
set({
|
||||||
|
albumSortDirection,
|
||||||
|
albums: [],
|
||||||
|
albumNextCursor: null,
|
||||||
|
albumPrevCursor: null,
|
||||||
|
jumpAnchorIndex: 0,
|
||||||
|
sectionAnchors: [],
|
||||||
|
});
|
||||||
|
persistSetting(ALBUM_SORT_DIRECTION_KEY, albumSortDirection);
|
||||||
|
void resetAlbums(true);
|
||||||
|
void resetSectionAnchors();
|
||||||
|
},
|
||||||
|
|
||||||
|
setArtistSortDirection: (artistSortDirection) => {
|
||||||
|
if (get().artistSortDirection === artistSortDirection) return;
|
||||||
|
anchorGeneration += 1;
|
||||||
|
set({
|
||||||
|
artistSortDirection,
|
||||||
|
artists: [],
|
||||||
|
artistNextCursor: null,
|
||||||
|
artistPrevCursor: null,
|
||||||
|
jumpAnchorIndex: 0,
|
||||||
|
sectionAnchors: [],
|
||||||
|
});
|
||||||
|
persistSetting(ARTIST_SORT_DIRECTION_KEY, artistSortDirection);
|
||||||
|
void resetArtists(true);
|
||||||
|
void resetSectionAnchors();
|
||||||
|
},
|
||||||
|
|
||||||
setAlbumLayout: (albumLayout) => {
|
setAlbumLayout: (albumLayout) => {
|
||||||
if (get().albumLayout === albumLayout) return;
|
if (get().albumLayout === albumLayout) return;
|
||||||
const state = get();
|
const state = get();
|
||||||
|
|||||||
Reference in New Issue
Block a user