mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
address review round 3: keep the search bar across recomposition, fix tab landing, page the queried songs queue
This commit is contained in:
@@ -35,6 +35,13 @@ internal const val SONGS_IDS_SELECT =
|
|||||||
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
|
||||||
"(:starredFilter = 2 AND songs.starred IS NULL)) "
|
"(:starredFilter = 2 AND songs.starred IS NULL)) "
|
||||||
|
|
||||||
|
internal const val SONGS_LIST_SELECT =
|
||||||
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
|
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||||
|
"WHERE songs.sourceId = :sourceId " +
|
||||||
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
|
||||||
|
"(:starredFilter = 2 AND songs.starred IS NULL)) "
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
|
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
|
||||||
interface LibraryDao {
|
interface LibraryDao {
|
||||||
@@ -357,7 +364,7 @@ interface LibraryDao {
|
|||||||
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
|
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
|
||||||
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
|
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
|
||||||
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
|
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
|
||||||
"songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
|
"songs.title COLLATE NOCASE, songs.id",
|
||||||
)
|
)
|
||||||
fun songs(
|
fun songs(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
@@ -622,4 +629,104 @@ interface LibraryDao {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredFilter: Int,
|
starredFilter: Int,
|
||||||
): List<String>
|
): List<String>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.title COLLATE NOCASE, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByAlbum(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY albums.albumArtist COLLATE NOCASE DESC, songs.album COLLATE NOCASE DESC, songs.disc DESC, songs.track DESC, songs.title COLLATE NOCASE DESC, songs.id DESC LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByAlbumReversed(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByTitle(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByTitleReversed(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.artist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByArtist(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.artist COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByArtistReversed(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.starred IS NULL, songs.starred DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByStarred(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.starred IS NULL, songs.starred ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByStarredReversed(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.created DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByAdded(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"${SONGS_LIST_SELECT}ORDER BY songs.created ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
suspend fun songsPageByAddedReversed(
|
||||||
|
sourceId: Long,
|
||||||
|
starredFilter: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,17 @@ private val Context.preferences: DataStore<Preferences> by preferencesDataStore(
|
|||||||
class UserPreferences(
|
class UserPreferences(
|
||||||
private val store: DataStore<Preferences>,
|
private val store: DataStore<Preferences>,
|
||||||
) {
|
) {
|
||||||
fun listQuery(tab: LibraryListTab): Flow<ListQuery> = store.data.map { prefs -> decode(prefs[listQueryKey(tab)], tab.defaultSort) }
|
fun listQuery(tab: LibraryListTab): Flow<ListQuery> =
|
||||||
|
store.data.map { prefs -> decode(prefs[listQueryKey(tab)] ?: legacyQuery(prefs, tab), tab.defaultSort) }
|
||||||
|
|
||||||
|
private fun legacyQuery(
|
||||||
|
prefs: Preferences,
|
||||||
|
tab: LibraryListTab,
|
||||||
|
): String? {
|
||||||
|
if (tab != LibraryListTab.Albums) return null
|
||||||
|
val sort = prefs[ALBUM_SORT]?.let { if (it == "RecentlyAdded") "Added" else it } ?: return null
|
||||||
|
return "$sort|0|0"
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setListQuery(
|
suspend fun setListQuery(
|
||||||
tab: LibraryListTab,
|
tab: LibraryListTab,
|
||||||
@@ -83,6 +93,7 @@ class UserPreferences(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
val ALBUM_SORT = stringPreferencesKey("album_sort")
|
||||||
val MAX_BITRATE = intPreferencesKey("max_bitrate")
|
val MAX_BITRATE = intPreferencesKey("max_bitrate")
|
||||||
val STREAM_FORMAT = stringPreferencesKey("stream_format")
|
val STREAM_FORMAT = stringPreferencesKey("stream_format")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.room3.deferredTransaction
|
|||||||
import androidx.room3.immediateTransaction
|
import androidx.room3.immediateTransaction
|
||||||
import androidx.room3.useReaderConnection
|
import androidx.room3.useReaderConnection
|
||||||
import androidx.room3.useWriterConnection
|
import androidx.room3.useWriterConnection
|
||||||
|
import com.subtracks.data.db.LibraryDao
|
||||||
import com.subtracks.data.db.SubtracksDatabase
|
import com.subtracks.data.db.SubtracksDatabase
|
||||||
import com.subtracks.data.model.PlaybackCursor
|
import com.subtracks.data.model.PlaybackCursor
|
||||||
import com.subtracks.data.model.QueueEntry
|
import com.subtracks.data.model.QueueEntry
|
||||||
@@ -173,6 +174,56 @@ class QueueRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun LibraryDao.songsPage(
|
||||||
|
sourceId: Long,
|
||||||
|
query: QueueSongQuery,
|
||||||
|
limit: Int,
|
||||||
|
offset: Long,
|
||||||
|
): List<SongListItem> {
|
||||||
|
val starred = query.starred.ordinal
|
||||||
|
return when (query.sort) {
|
||||||
|
SongSort.Album -> {
|
||||||
|
if (query.descending) {
|
||||||
|
songsPageByAlbumReversed(sourceId, starred, limit, offset)
|
||||||
|
} else {
|
||||||
|
songsPageByAlbum(sourceId, starred, limit, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SongSort.Title -> {
|
||||||
|
if (query.descending) {
|
||||||
|
songsPageByTitleReversed(sourceId, starred, limit, offset)
|
||||||
|
} else {
|
||||||
|
songsPageByTitle(sourceId, starred, limit, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SongSort.Artist -> {
|
||||||
|
if (query.descending) {
|
||||||
|
songsPageByArtistReversed(sourceId, starred, limit, offset)
|
||||||
|
} else {
|
||||||
|
songsPageByArtist(sourceId, starred, limit, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SongSort.Starred -> {
|
||||||
|
if (query.descending) {
|
||||||
|
songsPageByStarredReversed(sourceId, starred, limit, offset)
|
||||||
|
} else {
|
||||||
|
songsPageByStarred(sourceId, starred, limit, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SongSort.Added -> {
|
||||||
|
if (query.descending) {
|
||||||
|
songsPageByAddedReversed(sourceId, starred, limit, offset)
|
||||||
|
} else {
|
||||||
|
songsPageByAdded(sourceId, starred, limit, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun replace(
|
suspend fun replace(
|
||||||
entries: List<QueueEntry>,
|
entries: List<QueueEntry>,
|
||||||
shuffleOrder: LongArray? = null,
|
shuffleOrder: LongArray? = null,
|
||||||
@@ -247,23 +298,29 @@ class QueueRepository(
|
|||||||
snapshot: QueueSnapshot,
|
snapshot: QueueSnapshot,
|
||||||
position: Long,
|
position: Long,
|
||||||
): SongListItem? {
|
): SongListItem? {
|
||||||
val flat = snapshot.flatPosition(position) ?: return null
|
if (snapshot.shuffled) {
|
||||||
val id = flatIds(snapshot).getOrNull(flat.toInt()) ?: return null
|
val sourceId =
|
||||||
val sourceId =
|
snapshot.entries
|
||||||
snapshot.entries
|
.firstOrNull()
|
||||||
.firstOrNull()
|
?.entry
|
||||||
?.entry
|
?.sourceId ?: return null
|
||||||
?.sourceId ?: return null
|
val flat = snapshot.flatPosition(position) ?: return null
|
||||||
return dao.songsByIds(sourceId, listOf(id)).firstOrNull()
|
val id = flatIds(snapshot).getOrNull(flat.toInt()) ?: return null
|
||||||
|
return dao.songsByIds(sourceId, listOf(id)).firstOrNull()
|
||||||
|
}
|
||||||
|
val (entry, offset) = snapshot.locate(position) ?: return null
|
||||||
|
return rows(entry, entry.offset + offset, 1).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun flatIndexOf(
|
suspend fun flatIndexOf(
|
||||||
snapshot: QueueSnapshot,
|
snapshot: QueueSnapshot,
|
||||||
songId: String,
|
songId: String,
|
||||||
): Long? {
|
): Long? {
|
||||||
if (!snapshot.shuffled) return null
|
val order = snapshot.shuffleOrder ?: return null
|
||||||
val index = flatIds(snapshot).indexOf(songId)
|
val flat = flatIds(snapshot).indexOf(songId)
|
||||||
return if (index >= 0) index.toLong() else null
|
if (flat < 0) return null
|
||||||
|
val position = order.indexOf(flat.toLong())
|
||||||
|
return if (position >= 0) position.toLong() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun range(
|
suspend fun range(
|
||||||
@@ -271,22 +328,37 @@ class QueueRepository(
|
|||||||
first: Long,
|
first: Long,
|
||||||
last: Long,
|
last: Long,
|
||||||
): List<QueueWindowItem> {
|
): List<QueueWindowItem> {
|
||||||
if (snapshot.size == 0L) return emptyList()
|
if (snapshot.shuffled) {
|
||||||
val ids = flatIds(snapshot)
|
val sourceId =
|
||||||
val requested = mutableListOf<Pair<Long, String>>()
|
snapshot.entries
|
||||||
for (position in first..last) {
|
.firstOrNull()
|
||||||
val flat = snapshot.flatPosition(position) ?: continue
|
?.entry
|
||||||
val id = ids.getOrNull(flat.toInt()) ?: continue
|
?.sourceId ?: return emptyList()
|
||||||
requested += position to id
|
val ids = flatIds(snapshot)
|
||||||
|
val requested = mutableListOf<Pair<Long, String>>()
|
||||||
|
for (position in first..last) {
|
||||||
|
val flat = snapshot.flatPosition(position) ?: continue
|
||||||
|
val id = ids.getOrNull(flat.toInt()) ?: continue
|
||||||
|
requested += position to id
|
||||||
|
}
|
||||||
|
if (requested.isEmpty()) return emptyList()
|
||||||
|
val byId = dao.songsByIds(sourceId, requested.map { it.second }.distinct()).associateBy { it.song.id }
|
||||||
|
return requested.mapNotNull { (position, id) -> byId[id]?.let { QueueWindowItem(position, it) } }
|
||||||
}
|
}
|
||||||
if (requested.isEmpty()) return emptyList()
|
val items = mutableListOf<QueueWindowItem>()
|
||||||
val sourceId =
|
var start = 0L
|
||||||
snapshot.entries
|
for (resolved in snapshot.entries) {
|
||||||
.firstOrNull()
|
val end = start + resolved.length - 1
|
||||||
?.entry
|
val entryStart = start
|
||||||
?.sourceId ?: return emptyList()
|
start += resolved.length
|
||||||
val byId = dao.songsByIds(sourceId, requested.map { it.second }.distinct()).associateBy { it.song.id }
|
if (resolved.length == 0L || end < first || entryStart > last) continue
|
||||||
return requested.mapNotNull { (position, id) -> byId[id]?.let { QueueWindowItem(position, it) } }
|
val from = maxOf(first, entryStart)
|
||||||
|
val to = minOf(last, end)
|
||||||
|
val offset = resolved.entry.offset + (from - entryStart)
|
||||||
|
rows(resolved.entry, offset, (to - from + 1).toInt())
|
||||||
|
.forEachIndexed { index, item -> items += QueueWindowItem(from + index, item) }
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun window(
|
suspend fun window(
|
||||||
@@ -386,6 +458,31 @@ class QueueRepository(
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun rows(
|
||||||
|
entry: QueueEntry,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int,
|
||||||
|
): List<SongListItem> =
|
||||||
|
when (entry.kind) {
|
||||||
|
QueueKind.Playlist -> {
|
||||||
|
dao.playlistSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueKind.Album -> {
|
||||||
|
dao.albumSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueKind.Songs -> {
|
||||||
|
songsQuery(entry.refId)?.let { query ->
|
||||||
|
db.libraryDao().songsPage(entry.sourceId, query, limit, offset)
|
||||||
|
} ?: dao.songs(entry.sourceId, offset, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueKind.Song -> {
|
||||||
|
dao.song(entry.sourceId, entry.refId, offset, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun QueueEntry.resolvedLength(): Long {
|
private suspend fun QueueEntry.resolvedLength(): Long {
|
||||||
val total =
|
val total =
|
||||||
when (kind) {
|
when (kind) {
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ fun LibraryScreen(
|
|||||||
val searchFocus = remember { FocusRequester() }
|
val searchFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(pagerState) {
|
LaunchedEffect(pagerState) {
|
||||||
snapshotFlow { pagerState.currentPage }.collect { page ->
|
snapshotFlow { pagerState.settledPage }.collect { page ->
|
||||||
onTabSelected(LibraryTab.entries[page])
|
onTabSelected(LibraryTab.entries[page])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ fun LibraryScreen(
|
|||||||
if (pagerState.currentPage != selectedTab.ordinal) {
|
if (pagerState.currentPage != selectedTab.ordinal) {
|
||||||
pagerState.animateScrollToPage(selectedTab.ordinal)
|
pagerState.animateScrollToPage(selectedTab.ordinal)
|
||||||
}
|
}
|
||||||
searchActive = false
|
searchActive = search.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|||||||
Reference in New Issue
Block a user