mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 21:16:09 +02:00
fix slider not working correctly
This commit is contained in:
+68
@@ -180,6 +180,74 @@ class RoomLibraryRepositoryTest {
|
|||||||
assertNull(boundedPlaybackWindowStart(0, 0))
|
assertNull(boundedPlaybackWindowStart(0, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trackSectionAnchorsOpenOnTheRequestedTitleAndArtistBuckets() = runBlocking {
|
||||||
|
publish(
|
||||||
|
"anchors",
|
||||||
|
listOf(
|
||||||
|
track("anchors", 0, "//.xX_-=-FLUTE==-Xx.\\\\"),
|
||||||
|
track("anchors", 1, "Apple").copy(
|
||||||
|
artist = "Amber",
|
||||||
|
artistSortKey = SortKeys.forText("Amber"),
|
||||||
|
),
|
||||||
|
track("anchors", 2, "Saturn").copy(
|
||||||
|
artist = "Sade",
|
||||||
|
artistSortKey = SortKeys.forText("Sade"),
|
||||||
|
),
|
||||||
|
track("anchors", 3, "Zulu").copy(
|
||||||
|
artist = "Zero 7",
|
||||||
|
artistSortKey = SortKeys.forText("Zero 7"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val dao = catalog.catalogDao()
|
||||||
|
|
||||||
|
assertFalse(dao.getTitleSectionAnchors().any { it.sectionLabel == "X" })
|
||||||
|
assertTrue(dao.getTitleSectionAnchors().any { it.sectionLabel == "#" })
|
||||||
|
val titleAnchor = dao.getTitleSectionAnchors().single { it.sectionLabel == "S" }
|
||||||
|
val titlePage = dao.getTitlePage(titleAnchor.sortKey, "", 100)
|
||||||
|
assertEquals("Saturn", titlePage.first().title)
|
||||||
|
|
||||||
|
val artistAnchor = dao.getArtistSectionAnchorCandidates()
|
||||||
|
.filter { SortKeys.sectionLabel(it.artist) == "S" }
|
||||||
|
.minOf(ArtistSectionAnchorCandidate::sortKey)
|
||||||
|
val artistPage = dao.getArtistOrderPage(artistAnchor, "", 0, 0, "", "", 100)
|
||||||
|
assertEquals("Sade", artistPage.first().artist)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sectionLabelsUseOnlyTheFirstVisibleCharacter() {
|
||||||
|
assertEquals("#", SortKeys.sectionLabel("//.xX_-=-FLUTE==-Xx.\\\\"))
|
||||||
|
assertEquals("#", SortKeys.sectionLabel("! PARTY SIRENS !"))
|
||||||
|
assertEquals("#", SortKeys.sectionLabel("#iwannadance"))
|
||||||
|
assertEquals("#", SortKeys.sectionLabel("7 Rings"))
|
||||||
|
assertEquals("E", SortKeys.sectionLabel("Élan"))
|
||||||
|
assertEquals("S", SortKeys.sectionLabel(" Saturn"))
|
||||||
|
assertEquals("#", SortKeys.sectionLabel("東京の夜"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sectionLabelMigrationCorrectsExistingCatalogRows() = runBlocking {
|
||||||
|
val dao = catalog.catalogDao()
|
||||||
|
dao.insertMeta(CatalogMetaEntity(collationVersion = 1, updatedAt = 0))
|
||||||
|
dao.putSource(CatalogSourceEntity("local:1", "local", 1, "legacy", 0))
|
||||||
|
dao.insertGeneration(ScanGenerationEntity("legacy", "local:1", "active", 0))
|
||||||
|
dao.putTracks(
|
||||||
|
listOf(
|
||||||
|
track("legacy", 1, "//.xX_-=-FLUTE==-Xx.\\\\").copy(sectionLabel = "X"),
|
||||||
|
track("legacy", 2, "Xylophone").copy(sectionLabel = "X"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
dao.migrateSectionLabels(COLLATION_VERSION, 1)
|
||||||
|
|
||||||
|
assertEquals(COLLATION_VERSION, dao.getMeta()?.collationVersion)
|
||||||
|
assertEquals(
|
||||||
|
mapOf("//.xX_-=-FLUTE==-Xx.\\\\" to "#", "Xylophone" to "X"),
|
||||||
|
dao.getActiveTracks(dao.getAllPathsByTitle()).associate { it.title to it.sectionLabel },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun userSnapshotsRotateRejectDamageAndRestoreTheNewestValidCopy() = runBlocking {
|
fun userSnapshotsRotateRejectDamageAndRestoreTheNewestValidCopy() = runBlocking {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
|||||||
+19
-21
@@ -117,6 +117,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
val catalogOpen = openCatalogDatabaseWithRecovery()
|
val catalogOpen = openCatalogDatabaseWithRecovery()
|
||||||
catalogDatabase = catalogOpen
|
catalogDatabase = catalogOpen
|
||||||
val dao = catalogOpen.catalogDao()
|
val dao = catalogOpen.catalogDao()
|
||||||
|
val existingMeta = dao.getMeta()
|
||||||
dao.insertMeta(
|
dao.insertMeta(
|
||||||
CatalogMetaEntity(
|
CatalogMetaEntity(
|
||||||
revision = 0,
|
revision = 0,
|
||||||
@@ -124,6 +125,9 @@ class AstraLibraryRepository private constructor(
|
|||||||
updatedAt = System.currentTimeMillis(),
|
updatedAt = System.currentTimeMillis(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (existingMeta != null && existingMeta.collationVersion < COLLATION_VERSION) {
|
||||||
|
dao.migrateSectionLabels(COLLATION_VERSION, System.currentTimeMillis())
|
||||||
|
}
|
||||||
dao.discardAbandonedGenerations()
|
dao.discardAbandonedGenerations()
|
||||||
reconcileUserFacts()
|
reconcileUserFacts()
|
||||||
|
|
||||||
@@ -2048,7 +2052,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
"albums" -> {
|
"albums" -> {
|
||||||
val rows = dao.getAllAlbumSummaries(revision).filter { includeSingles || !it.isSingle }
|
val rows = dao.getAllAlbumSummaries(revision).filter { includeSingles || !it.isSingle }
|
||||||
rows.groupBy { row ->
|
rows.groupBy { row ->
|
||||||
if (sort == "artist") SortKeys.sectionLabel(row.artist) else row.sectionLabel
|
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.minWith(compareBy<AlbumSummaryEntity>({ it.artistSortKey }, { it.nameSortKey }, { it.identityKey }))
|
||||||
@@ -2071,7 +2075,7 @@ class AstraLibraryRepository private constructor(
|
|||||||
val mode = if (groupingMode == "fileTags") "fileTags" else "astra"
|
val mode = if (groupingMode == "fileTags") "fileTags" else "astra"
|
||||||
dao.getAllArtistSummaries(revision, mode)
|
dao.getAllArtistSummaries(revision, mode)
|
||||||
.filter { includeCollaborations || !it.isCollaboration }
|
.filter { includeCollaborations || !it.isCollaboration }
|
||||||
.groupBy(ArtistSummaryEntity::sectionLabel)
|
.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.minWith(compareBy<ArtistSummaryEntity>({ it.nameSortKey }, { it.artistKey }))
|
||||||
label to TrackPageCursor(
|
label to TrackPageCursor(
|
||||||
@@ -2082,29 +2086,23 @@ class AstraLibraryRepository private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
dao.getAllActiveTracksForNativeMatching()
|
if (sort == "artist") {
|
||||||
.groupBy { row ->
|
dao.getArtistSectionAnchorCandidates()
|
||||||
if (sort == "artist") SortKeys.sectionLabel(row.artist) else row.sectionLabel
|
.groupBy { candidate -> SortKeys.sectionLabel(candidate.artist) }
|
||||||
}
|
|
||||||
.map { (label, section) ->
|
.map { (label, section) ->
|
||||||
val first = if (sort == "artist") {
|
label to TrackPageCursor(
|
||||||
section.minWith(
|
revision,
|
||||||
compareBy<ActiveTrackView>(
|
"tracks:artist",
|
||||||
{ it.artistSortKey },
|
text1 = section.minOf(ArtistSectionAnchorCandidate::sortKey),
|
||||||
{ it.albumSortKey },
|
|
||||||
{ it.discSort },
|
|
||||||
{ it.trackSort },
|
|
||||||
{ it.titleSortKey },
|
|
||||||
{ it.path },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
section.minWith(compareBy<ActiveTrackView>({ it.titleSortKey }, { it.path }))
|
|
||||||
}
|
}
|
||||||
label to if (sort == "artist") {
|
|
||||||
TrackPageCursor(revision, "tracks:artist", text1 = first.artistSortKey)
|
|
||||||
} else {
|
} else {
|
||||||
TrackPageCursor(revision, "tracks:title", text1 = first.titleSortKey)
|
dao.getTitleSectionAnchors().map { row ->
|
||||||
|
row.sectionLabel to TrackPageCursor(
|
||||||
|
revision,
|
||||||
|
"tracks:title",
|
||||||
|
text1 = row.sortKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-3
@@ -22,6 +22,17 @@ data class SectionAnchorRow(
|
|||||||
@androidx.room.ColumnInfo(name = "sort_key") val sortKey: String,
|
@androidx.room.ColumnInfo(name = "sort_key") val sortKey: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ArtistSectionAnchorCandidate(
|
||||||
|
val artist: String,
|
||||||
|
@androidx.room.ColumnInfo(name = "sort_key") val sortKey: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TrackSectionLabelCandidate(
|
||||||
|
val id: Long,
|
||||||
|
val title: String,
|
||||||
|
@androidx.room.ColumnInfo(name = "section_label") val sectionLabel: String,
|
||||||
|
)
|
||||||
|
|
||||||
data class LibraryLoudnessStatsRow(
|
data class LibraryLoudnessStatsRow(
|
||||||
val lufsCount: Long,
|
val lufsCount: Long,
|
||||||
val medianLufs: Double?,
|
val medianLufs: Double?,
|
||||||
@@ -50,6 +61,16 @@ interface CatalogDao {
|
|||||||
@Query("SELECT revision FROM catalog_meta WHERE id = 1")
|
@Query("SELECT revision FROM catalog_meta WHERE id = 1")
|
||||||
suspend fun getRevision(): Long
|
suspend fun getRevision(): Long
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
UPDATE catalog_meta
|
||||||
|
SET collation_version = :version,
|
||||||
|
updated_at = :updatedAt
|
||||||
|
WHERE id = 1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun setCollationVersion(version: Int, updatedAt: Long)
|
||||||
|
|
||||||
@Query("SELECT * FROM catalog_sources WHERE source_key = :sourceKey")
|
@Query("SELECT * FROM catalog_sources WHERE source_key = :sourceKey")
|
||||||
suspend fun getSource(sourceKey: String): CatalogSourceEntity?
|
suspend fun getSource(sourceKey: String): CatalogSourceEntity?
|
||||||
|
|
||||||
@@ -471,13 +492,30 @@ interface CatalogDao {
|
|||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT section_label, MIN(artist_sort_key) AS sort_key
|
SELECT artist, MIN(artist_sort_key) AS sort_key
|
||||||
FROM active_tracks
|
FROM active_tracks
|
||||||
GROUP BY section_label
|
GROUP BY artist
|
||||||
ORDER BY sort_key
|
ORDER BY sort_key
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
suspend fun getArtistSectionAnchors(): List<SectionAnchorRow>
|
suspend fun getArtistSectionAnchorCandidates(): List<ArtistSectionAnchorCandidate>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT id, title, section_label
|
||||||
|
FROM tracks
|
||||||
|
WHERE id > :afterId
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT :limit
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun getTrackSectionLabelCandidates(
|
||||||
|
afterId: Long,
|
||||||
|
limit: Int,
|
||||||
|
): List<TrackSectionLabelCandidate>
|
||||||
|
|
||||||
|
@Query("UPDATE tracks SET section_label = :sectionLabel WHERE id = :trackId")
|
||||||
|
suspend fun updateTrackSectionLabel(trackId: Long, sectionLabel: String)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun putAlbumSummaries(rows: List<AlbumSummaryEntity>)
|
suspend fun putAlbumSummaries(rows: List<AlbumSummaryEntity>)
|
||||||
@@ -1027,6 +1065,22 @@ interface CatalogDao {
|
|||||||
deleteAbandonedGenerationRecords()
|
deleteAbandonedGenerationRecords()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun migrateSectionLabels(version: Int, updatedAt: Long) {
|
||||||
|
var afterId = 0L
|
||||||
|
do {
|
||||||
|
val rows = getTrackSectionLabelCandidates(afterId, 1_000)
|
||||||
|
for (row in rows) {
|
||||||
|
val corrected = SortKeys.sectionLabel(row.title)
|
||||||
|
if (corrected != row.sectionLabel) {
|
||||||
|
updateTrackSectionLabel(row.id, corrected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
afterId = rows.lastOrNull()?.id ?: afterId
|
||||||
|
} while (rows.size == 1_000)
|
||||||
|
setCollationVersion(version, updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun publishGeneration(
|
suspend fun publishGeneration(
|
||||||
sourceKey: String,
|
sourceKey: String,
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@ import java.util.Locale
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
|
||||||
const val COLLATION_VERSION = 1
|
const val COLLATION_VERSION = 2
|
||||||
const val DEFAULT_PAGE_SIZE = 100
|
const val DEFAULT_PAGE_SIZE = 100
|
||||||
const val MAX_PAGE_SIZE = 200
|
const val MAX_PAGE_SIZE = 200
|
||||||
|
|
||||||
@@ -59,9 +59,9 @@ object SortKeys {
|
|||||||
|
|
||||||
fun sectionLabel(value: String): String {
|
fun sectionLabel(value: String): String {
|
||||||
val normalized = Normalizer.normalize(value.trim(), Normalizer.Form.NFD)
|
val normalized = Normalizer.normalize(value.trim(), Normalizer.Form.NFD)
|
||||||
val first = normalized.firstOrNull { Character.isLetterOrDigit(it) } ?: return "#"
|
val first = normalized.firstOrNull() ?: return "#"
|
||||||
val upper = first.uppercaseChar()
|
val upper = first.uppercaseChar()
|
||||||
return if (upper in 'A'..'Z' || upper in '0'..'9') upper.toString() else "#"
|
return if (upper in 'A'..'Z') upper.toString() else "#"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +10,7 @@ import {
|
|||||||
StyleSheet
|
StyleSheet
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { FlashList, type FlashListRef } from '@shopify/flash-list';
|
import { FlashList } from '@shopify/flash-list';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { Screen } from '@/components/Screen';
|
import { Screen } from '@/components/Screen';
|
||||||
import { Text } from '@/components/Text';
|
import { Text } from '@/components/Text';
|
||||||
@@ -62,10 +61,7 @@ import {
|
|||||||
ARTIST_SORT_LABELS,
|
ARTIST_SORT_LABELS,
|
||||||
type ArtistSort
|
type ArtistSort
|
||||||
} from '@/lib/artistSort';
|
} from '@/lib/artistSort';
|
||||||
import { RAIL_LETTERS } from '@/lib/letterIndex';
|
|
||||||
import type {
|
import type {
|
||||||
Album,
|
|
||||||
Artist,
|
|
||||||
DbTrack
|
DbTrack
|
||||||
} from '@/types/library';
|
} from '@/types/library';
|
||||||
|
|
||||||
@@ -92,6 +88,7 @@ export default function LibraryScreen() {
|
|||||||
const loadNextAlbums = useLibraryStore((s) => s.loadNextAlbums);
|
const loadNextAlbums = useLibraryStore((s) => s.loadNextAlbums);
|
||||||
const loadNextArtists = useLibraryStore((s) => s.loadNextArtists);
|
const loadNextArtists = useLibraryStore((s) => s.loadNextArtists);
|
||||||
const sectionAnchors = useLibraryStore((s) => s.sectionAnchors);
|
const sectionAnchors = useLibraryStore((s) => s.sectionAnchors);
|
||||||
|
const sectionJumpRevision = useLibraryStore((s) => s.sectionJumpRevision);
|
||||||
const jumpToSection = useLibraryStore((s) => s.jumpToSection);
|
const jumpToSection = useLibraryStore((s) => s.jumpToSection);
|
||||||
const isScanning = useLibraryStore((s) => s.isScanning);
|
const isScanning = useLibraryStore((s) => s.isScanning);
|
||||||
const scanError = useLibraryStore((s) => s.scanError);
|
const scanError = useLibraryStore((s) => s.scanError);
|
||||||
@@ -107,10 +104,6 @@ export default function LibraryScreen() {
|
|||||||
const [playlistPickerOpen, setPlaylistPickerOpen] = useState(false);
|
const [playlistPickerOpen, setPlaylistPickerOpen] = useState(false);
|
||||||
const scrollTop = useScrollTopGate();
|
const scrollTop = useScrollTopGate();
|
||||||
|
|
||||||
const tracksListRef = useRef<FlashListRef<DbTrack>>(null);
|
|
||||||
const albumsListRef = useRef<FlashListRef<Album>>(null);
|
|
||||||
const artistsListRef = useRef<FlashListRef<Artist>>(null);
|
|
||||||
|
|
||||||
const showLibraryStatus =
|
const showLibraryStatus =
|
||||||
totalTrackCount === 0 &&
|
totalTrackCount === 0 &&
|
||||||
!isScanning &&
|
!isScanning &&
|
||||||
@@ -141,16 +134,10 @@ export default function LibraryScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const jumpToLetter = (letter: string) => {
|
const jumpToLetter = (letter: string) => {
|
||||||
const requestedIndex = RAIL_LETTERS.indexOf(letter);
|
const anchor = sectionAnchors.find((entry) => entry.label === letter);
|
||||||
const anchor =
|
|
||||||
sectionAnchors.find((entry) => entry.label === letter) ??
|
|
||||||
sectionAnchors.find((entry) => RAIL_LETTERS.indexOf(entry.label) >= requestedIndex) ??
|
|
||||||
sectionAnchors.at(-1);
|
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
void jumpToSection(anchor.cursor).then(() => {
|
void jumpToSection(anchor.cursor).then((applied) => {
|
||||||
if (viewMode === 'tracks') tracksListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
if (applied) scrollTop.setScrollAtTop(true);
|
||||||
else if (viewMode === 'albums') albumsListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
|
||||||
else if (viewMode === 'artists') artistsListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +288,7 @@ export default function LibraryScreen() {
|
|||||||
<View style={styles.listArea}>
|
<View style={styles.listArea}>
|
||||||
{viewMode === 'albums' ? (
|
{viewMode === 'albums' ? (
|
||||||
<FlashList
|
<FlashList
|
||||||
ref={albumsListRef}
|
key={`albums-${albumSort}-${sectionJumpRevision}`}
|
||||||
data={sortedAlbums}
|
data={sortedAlbums}
|
||||||
numColumns={3}
|
numColumns={3}
|
||||||
keyExtractor={(album) => album.identity_key}
|
keyExtractor={(album) => album.identity_key}
|
||||||
@@ -330,7 +317,7 @@ export default function LibraryScreen() {
|
|||||||
|
|
||||||
{viewMode === 'artists' ? (
|
{viewMode === 'artists' ? (
|
||||||
<FlashList
|
<FlashList
|
||||||
ref={artistsListRef}
|
key={`artists-${artistSort}-${sectionJumpRevision}`}
|
||||||
data={sortedArtists}
|
data={sortedArtists}
|
||||||
numColumns={3}
|
numColumns={3}
|
||||||
keyExtractor={(artist) => artist.artist}
|
keyExtractor={(artist) => artist.artist}
|
||||||
@@ -359,7 +346,7 @@ export default function LibraryScreen() {
|
|||||||
|
|
||||||
{viewMode === 'tracks' ? (
|
{viewMode === 'tracks' ? (
|
||||||
<FlashList
|
<FlashList
|
||||||
ref={tracksListRef}
|
key={`tracks-${trackSort}-${sectionJumpRevision}`}
|
||||||
data={sortedTracks}
|
data={sortedTracks}
|
||||||
keyExtractor={(track) => String(track.id)}
|
keyExtractor={(track) => String(track.id)}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ export function AlphabetRail({ activeLetters, onJumpToLetter }: AlphabetRailProp
|
|||||||
const bubbleY = useSharedValue(0);
|
const bubbleY = useSharedValue(0);
|
||||||
|
|
||||||
const scrubTo = (letter: string) => {
|
const scrubTo = (letter: string) => {
|
||||||
|
if (!activeLetters.has(letter)) {
|
||||||
|
setScrubLetter(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setScrubLetter(letter);
|
setScrubLetter(letter);
|
||||||
|
playHaptic('frequentStep');
|
||||||
onJumpToLetter(letter);
|
onJumpToLetter(letter);
|
||||||
};
|
};
|
||||||
const endScrub = () => setScrubLetter(null);
|
const endScrub = () => setScrubLetter(null);
|
||||||
@@ -60,7 +65,6 @@ export function AlphabetRail({ activeLetters, onJumpToLetter }: AlphabetRailProp
|
|||||||
);
|
);
|
||||||
const letter = RAIL_LETTERS[index];
|
const letter = RAIL_LETTERS[index];
|
||||||
lastLetter.value = letter;
|
lastLetter.value = letter;
|
||||||
runOnJS(playHaptic)('frequentStep');
|
|
||||||
runOnJS(scrubTo)(letter);
|
runOnJS(scrubTo)(letter);
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
@@ -76,7 +80,6 @@ export function AlphabetRail({ activeLetters, onJumpToLetter }: AlphabetRailProp
|
|||||||
const letter = RAIL_LETTERS[index];
|
const letter = RAIL_LETTERS[index];
|
||||||
if (letter === lastLetter.value) return;
|
if (letter === lastLetter.value) return;
|
||||||
lastLetter.value = letter;
|
lastLetter.value = letter;
|
||||||
runOnJS(playHaptic)('frequentStep');
|
|
||||||
runOnJS(scrubTo)(letter);
|
runOnJS(scrubTo)(letter);
|
||||||
})
|
})
|
||||||
.onFinalize(() => {
|
.onFinalize(() => {
|
||||||
@@ -85,8 +88,8 @@ export function AlphabetRail({ activeLetters, onJumpToLetter }: AlphabetRailProp
|
|||||||
runOnJS(endScrub)();
|
runOnJS(endScrub)();
|
||||||
});
|
});
|
||||||
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 onJumpToLetter via render closure
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- scrubTo/endScrub capture the latest props via render closure
|
||||||
}, [lastLetter, bubbleY, railTop, pullSearchRef, onJumpToLetter]);
|
}, [lastLetter, bubbleY, railTop, pullSearchRef, activeLetters, onJumpToLetter]);
|
||||||
|
|
||||||
const bubbleStyle = useAnimatedStyle(() => ({
|
const bubbleStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{ translateY: bubbleY.value - BUBBLE_SIZE / 2 }],
|
transform: [{ translateY: bubbleY.value - BUBBLE_SIZE / 2 }],
|
||||||
|
|||||||
+296
-68
@@ -87,13 +87,14 @@ interface LibraryStore {
|
|||||||
albumNextCursor: string | null;
|
albumNextCursor: string | null;
|
||||||
artistNextCursor: string | null;
|
artistNextCursor: string | null;
|
||||||
sectionAnchors: LibrarySectionAnchor[];
|
sectionAnchors: LibrarySectionAnchor[];
|
||||||
|
sectionJumpRevision: number;
|
||||||
|
|
||||||
initialize: () => Promise<void>;
|
initialize: () => Promise<void>;
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
loadNextTracks: () => Promise<void>;
|
loadNextTracks: () => Promise<void>;
|
||||||
loadNextAlbums: () => Promise<void>;
|
loadNextAlbums: () => Promise<void>;
|
||||||
loadNextArtists: () => Promise<void>;
|
loadNextArtists: () => Promise<void>;
|
||||||
jumpToSection: (cursor: string) => Promise<void>;
|
jumpToSection: (cursor: string) => Promise<boolean>;
|
||||||
recordTrackPlayed: (path: string) => Promise<void>;
|
recordTrackPlayed: (path: string) => Promise<void>;
|
||||||
recomputeArtists: () => void;
|
recomputeArtists: () => void;
|
||||||
recomputeAlbums: () => void;
|
recomputeAlbums: () => void;
|
||||||
@@ -122,51 +123,105 @@ function appendWindow<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useLibraryStore = create<LibraryStore>((set, get) => {
|
export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||||
|
const pageGenerations = {
|
||||||
|
tracks: 0,
|
||||||
|
albums: 0,
|
||||||
|
artists: 0,
|
||||||
|
};
|
||||||
|
let anchorGeneration = 0;
|
||||||
|
let loadingGeneration = 0;
|
||||||
|
|
||||||
|
const beginLoading = () => {
|
||||||
|
const generation = ++loadingGeneration;
|
||||||
|
set({ isPageLoading: true });
|
||||||
|
return generation;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishLoading = (generation: number) => {
|
||||||
|
if (generation === loadingGeneration) set({ isPageLoading: false });
|
||||||
|
};
|
||||||
|
|
||||||
const onProgress = (progress: ScanProgress) => {
|
const onProgress = (progress: ScanProgress) => {
|
||||||
set({ scanProgress: progress });
|
set({ scanProgress: progress });
|
||||||
void reportScanProgress(progress);
|
void reportScanProgress(progress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const readTrackPage = (cursor: string | null) =>
|
const readTrackPage = (
|
||||||
AstraLibraryData.getTrackPage<DbTrack>(get().trackSort, cursor, PAGE_SIZE);
|
cursor: string | null,
|
||||||
|
sort = get().trackSort,
|
||||||
|
) => AstraLibraryData.getTrackPage<DbTrack>(sort, cursor, PAGE_SIZE);
|
||||||
|
|
||||||
const readAlbumPage = (cursor: string | null) =>
|
const readAlbumPage = (
|
||||||
|
cursor: string | null,
|
||||||
|
sort = get().albumSort,
|
||||||
|
includeSingles = useSettingsStore.getState().includeSingles,
|
||||||
|
) =>
|
||||||
AstraLibraryData.getAlbumPage<Album>(
|
AstraLibraryData.getAlbumPage<Album>(
|
||||||
get().albumSort,
|
sort,
|
||||||
useSettingsStore.getState().includeSingles,
|
includeSingles,
|
||||||
cursor,
|
cursor,
|
||||||
PAGE_SIZE
|
PAGE_SIZE
|
||||||
);
|
);
|
||||||
|
|
||||||
const readArtistPage = (cursor: string | null) =>
|
const readArtistPage = (
|
||||||
|
cursor: string | null,
|
||||||
|
sort = get().artistSort,
|
||||||
|
groupingMode = useSettingsStore.getState().artistGroupingMode,
|
||||||
|
includeCollaborations = get().includeCollabArtists,
|
||||||
|
) =>
|
||||||
AstraLibraryData.getArtistPage<Artist>(
|
AstraLibraryData.getArtistPage<Artist>(
|
||||||
get().artistSort,
|
sort,
|
||||||
useSettingsStore.getState().artistGroupingMode,
|
groupingMode,
|
||||||
get().includeCollabArtists,
|
includeCollaborations,
|
||||||
cursor,
|
cursor,
|
||||||
PAGE_SIZE
|
PAGE_SIZE
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetTracks = async () => {
|
const resetTracks = async () => {
|
||||||
const page = await readTrackPage(null);
|
const sort = get().trackSort;
|
||||||
|
const generation = ++pageGenerations.tracks;
|
||||||
|
const page = await readTrackPage(null, sort);
|
||||||
|
if (generation !== pageGenerations.tracks || get().trackSort !== sort) return false;
|
||||||
set({
|
set({
|
||||||
tracks: page.items ?? [],
|
tracks: page.items ?? [],
|
||||||
trackNextCursor: page.nextCursor ?? null,
|
trackNextCursor: page.nextCursor ?? null,
|
||||||
totalTrackCount: page.totalCount ?? 0,
|
totalTrackCount: page.totalCount ?? 0,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetAlbums = async () => {
|
const resetAlbums = async () => {
|
||||||
const page = await readAlbumPage(null);
|
const sort = get().albumSort;
|
||||||
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
|
const generation = ++pageGenerations.albums;
|
||||||
|
const page = await readAlbumPage(null, sort, includeSingles);
|
||||||
|
if (
|
||||||
|
generation !== pageGenerations.albums ||
|
||||||
|
get().albumSort !== sort ||
|
||||||
|
useSettingsStore.getState().includeSingles !== includeSingles
|
||||||
|
) return false;
|
||||||
set({ albums: page.items ?? [], albumNextCursor: page.nextCursor ?? null });
|
set({ albums: page.items ?? [], albumNextCursor: page.nextCursor ?? null });
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetArtists = async () => {
|
const resetArtists = async () => {
|
||||||
const page = await readArtistPage(null);
|
const sort = get().artistSort;
|
||||||
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
|
const includeCollaborations = get().includeCollabArtists;
|
||||||
|
const generation = ++pageGenerations.artists;
|
||||||
|
const page = await readArtistPage(null, sort, groupingMode, includeCollaborations);
|
||||||
|
if (
|
||||||
|
generation !== pageGenerations.artists ||
|
||||||
|
get().artistSort !== sort ||
|
||||||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
|
get().includeCollabArtists !== includeCollaborations
|
||||||
|
) return false;
|
||||||
set({ artists: page.items ?? [], artistNextCursor: page.nextCursor ?? null });
|
set({ artists: page.items ?? [], artistNextCursor: page.nextCursor ?? null });
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetSectionAnchors = async () => {
|
const resetSectionAnchors = async () => {
|
||||||
|
const generation = ++anchorGeneration;
|
||||||
const state = get();
|
const state = get();
|
||||||
const sortable =
|
const sortable =
|
||||||
(state.viewMode === 'tracks' && (state.trackSort === 'artist' || state.trackSort === 'title')) ||
|
(state.viewMode === 'tracks' && (state.trackSort === 'artist' || state.trackSort === 'title')) ||
|
||||||
@@ -182,15 +237,31 @@ 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';
|
||||||
set({
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
sectionAnchors: await AstraLibraryData.getSectionAnchors(
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
|
const anchors = await AstraLibraryData.getSectionAnchors(
|
||||||
state.viewMode as 'tracks' | 'albums' | 'artists',
|
state.viewMode as 'tracks' | 'albums' | 'artists',
|
||||||
sort,
|
sort,
|
||||||
useSettingsStore.getState().includeSingles,
|
includeSingles,
|
||||||
useSettingsStore.getState().artistGroupingMode,
|
groupingMode,
|
||||||
state.includeCollabArtists
|
state.includeCollabArtists
|
||||||
),
|
);
|
||||||
});
|
const current = get();
|
||||||
|
const currentSort =
|
||||||
|
current.viewMode === 'tracks'
|
||||||
|
? current.trackSort
|
||||||
|
: current.viewMode === 'albums'
|
||||||
|
? current.albumSort
|
||||||
|
: current.artistSort;
|
||||||
|
if (
|
||||||
|
generation !== anchorGeneration ||
|
||||||
|
current.viewMode !== state.viewMode ||
|
||||||
|
currentSort !== sort ||
|
||||||
|
useSettingsStore.getState().includeSingles !== includeSingles ||
|
||||||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
|
current.includeCollabArtists !== state.includeCollabArtists
|
||||||
|
) return;
|
||||||
|
set({ sectionAnchors: anchors });
|
||||||
};
|
};
|
||||||
|
|
||||||
const runScan = async (scan: () => Promise<ScanResult | null>) => {
|
const runScan = async (scan: () => Promise<ScanResult | null>) => {
|
||||||
@@ -235,6 +306,7 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
albumNextCursor: null,
|
albumNextCursor: null,
|
||||||
artistNextCursor: null,
|
artistNextCursor: null,
|
||||||
sectionAnchors: [],
|
sectionAnchors: [],
|
||||||
|
sectionJumpRevision: 0,
|
||||||
|
|
||||||
initialize: () => {
|
initialize: () => {
|
||||||
if (!initPromise) {
|
if (!initPromise) {
|
||||||
@@ -282,14 +354,20 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
AstraLibraryData.addListener('onCatalogChanged', () => {
|
AstraLibraryData.addListener('onCatalogChanged', () => {
|
||||||
|
anchorGeneration += 1;
|
||||||
|
set({ sectionAnchors: [] });
|
||||||
void get().refresh();
|
void get().refresh();
|
||||||
});
|
});
|
||||||
useSettingsStore.subscribe((next, previous) => {
|
useSettingsStore.subscribe((next, previous) => {
|
||||||
if (next.artistGroupingMode !== previous.artistGroupingMode) {
|
if (next.artistGroupingMode !== previous.artistGroupingMode) {
|
||||||
|
anchorGeneration += 1;
|
||||||
|
set({ sectionAnchors: [] });
|
||||||
void resetArtists();
|
void resetArtists();
|
||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
}
|
}
|
||||||
if (next.includeSingles !== previous.includeSingles) {
|
if (next.includeSingles !== previous.includeSingles) {
|
||||||
|
anchorGeneration += 1;
|
||||||
|
set({ sectionAnchors: [] });
|
||||||
void resetAlbums();
|
void resetAlbums();
|
||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
}
|
}
|
||||||
@@ -310,7 +388,22 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: async () => {
|
refresh: async () => {
|
||||||
const viewMode = get().viewMode;
|
const stateAtStart = get();
|
||||||
|
const viewMode = stateAtStart.viewMode;
|
||||||
|
const trackSort = stateAtStart.trackSort;
|
||||||
|
const albumSort = stateAtStart.albumSort;
|
||||||
|
const artistSort = stateAtStart.artistSort;
|
||||||
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
|
const includeCollaborations = stateAtStart.includeCollabArtists;
|
||||||
|
const activeGeneration =
|
||||||
|
viewMode === 'tracks'
|
||||||
|
? ++pageGenerations.tracks
|
||||||
|
: viewMode === 'albums'
|
||||||
|
? ++pageGenerations.albums
|
||||||
|
: viewMode === 'artists'
|
||||||
|
? ++pageGenerations.artists
|
||||||
|
: null;
|
||||||
const [
|
const [
|
||||||
trackPage,
|
trackPage,
|
||||||
albumPage,
|
albumPage,
|
||||||
@@ -320,36 +413,59 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
folders,
|
folders,
|
||||||
recentlyPlayedTracks,
|
recentlyPlayedTracks,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
viewMode === 'tracks' ? readTrackPage(null) : Promise.resolve(null),
|
viewMode === 'tracks' ? readTrackPage(null, trackSort) : Promise.resolve(null),
|
||||||
viewMode === 'albums' ? readAlbumPage(null) : Promise.resolve(null),
|
viewMode === 'albums'
|
||||||
viewMode === 'artists' ? readArtistPage(null) : Promise.resolve(null),
|
? readAlbumPage(null, albumSort, includeSingles)
|
||||||
|
: Promise.resolve(null),
|
||||||
|
viewMode === 'artists'
|
||||||
|
? readArtistPage(null, artistSort, groupingMode, includeCollaborations)
|
||||||
|
: Promise.resolve(null),
|
||||||
AstraLibraryData.getAlbumPage<Album>(
|
AstraLibraryData.getAlbumPage<Album>(
|
||||||
'recently_added',
|
'recently_added',
|
||||||
useSettingsStore.getState().includeSingles,
|
includeSingles,
|
||||||
null,
|
null,
|
||||||
20
|
20
|
||||||
),
|
),
|
||||||
AstraLibraryData.getArtistPage<Artist>(
|
AstraLibraryData.getArtistPage<Artist>(
|
||||||
'name',
|
'name',
|
||||||
useSettingsStore.getState().artistGroupingMode,
|
groupingMode,
|
||||||
get().includeCollabArtists,
|
includeCollaborations,
|
||||||
null,
|
null,
|
||||||
50
|
50
|
||||||
),
|
),
|
||||||
loadFolders(),
|
loadFolders(),
|
||||||
AstraLibraryData.getRecentlyPlayed<DbTrack>(20),
|
AstraLibraryData.getRecentlyPlayed<DbTrack>(20),
|
||||||
]);
|
]);
|
||||||
|
const current = get();
|
||||||
|
const canApplyTrackPage =
|
||||||
|
viewMode === 'tracks' &&
|
||||||
|
current.viewMode === 'tracks' &&
|
||||||
|
current.trackSort === trackSort &&
|
||||||
|
activeGeneration === pageGenerations.tracks;
|
||||||
|
const canApplyAlbumPage =
|
||||||
|
viewMode === 'albums' &&
|
||||||
|
current.viewMode === 'albums' &&
|
||||||
|
current.albumSort === albumSort &&
|
||||||
|
useSettingsStore.getState().includeSingles === includeSingles &&
|
||||||
|
activeGeneration === pageGenerations.albums;
|
||||||
|
const canApplyArtistPage =
|
||||||
|
viewMode === 'artists' &&
|
||||||
|
current.viewMode === 'artists' &&
|
||||||
|
current.artistSort === artistSort &&
|
||||||
|
useSettingsStore.getState().artistGroupingMode === groupingMode &&
|
||||||
|
current.includeCollabArtists === includeCollaborations &&
|
||||||
|
activeGeneration === pageGenerations.artists;
|
||||||
set({
|
set({
|
||||||
...(trackPage ? {
|
...(trackPage && canApplyTrackPage ? {
|
||||||
tracks: trackPage.items ?? [],
|
tracks: trackPage.items ?? [],
|
||||||
trackNextCursor: trackPage.nextCursor ?? null,
|
trackNextCursor: trackPage.nextCursor ?? null,
|
||||||
totalTrackCount: trackPage.totalCount ?? get().totalTrackCount,
|
totalTrackCount: trackPage.totalCount ?? current.totalTrackCount,
|
||||||
} : {}),
|
} : {}),
|
||||||
...(albumPage ? {
|
...(albumPage && canApplyAlbumPage ? {
|
||||||
albums: albumPage.items ?? [],
|
albums: albumPage.items ?? [],
|
||||||
albumNextCursor: albumPage.nextCursor ?? null,
|
albumNextCursor: albumPage.nextCursor ?? null,
|
||||||
} : {}),
|
} : {}),
|
||||||
...(artistPage ? {
|
...(artistPage && canApplyArtistPage ? {
|
||||||
artists: artistPage.items ?? [],
|
artists: artistPage.items ?? [],
|
||||||
artistNextCursor: artistPage.nextCursor ?? null,
|
artistNextCursor: artistPage.nextCursor ?? null,
|
||||||
} : {}),
|
} : {}),
|
||||||
@@ -362,76 +478,178 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadNextTracks: async () => {
|
loadNextTracks: async () => {
|
||||||
const cursor = get().trackNextCursor;
|
const state = get();
|
||||||
if (!cursor || get().isPageLoading) return;
|
const cursor = state.trackNextCursor;
|
||||||
set({ isPageLoading: true });
|
if (!cursor || state.isPageLoading) return;
|
||||||
|
const sort = state.trackSort;
|
||||||
|
const pageGeneration = pageGenerations.tracks;
|
||||||
|
const loading = beginLoading();
|
||||||
try {
|
try {
|
||||||
const page = await readTrackPage(cursor);
|
const page = await readTrackPage(cursor, sort);
|
||||||
if (page.error === 'STALE_REVISION') return resetTracks();
|
if (
|
||||||
|
pageGeneration !== pageGenerations.tracks ||
|
||||||
|
get().trackSort !== sort ||
|
||||||
|
get().trackNextCursor !== cursor
|
||||||
|
) return;
|
||||||
|
if (page.error === 'STALE_REVISION') {
|
||||||
|
await resetTracks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
tracks: appendWindow(state.tracks, page.items, (track) => track.path),
|
tracks: appendWindow(state.tracks, page.items, (track) => track.path),
|
||||||
trackNextCursor: page.nextCursor,
|
trackNextCursor: page.nextCursor,
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
set({ isPageLoading: false });
|
finishLoading(loading);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
loadNextAlbums: async () => {
|
loadNextAlbums: async () => {
|
||||||
const cursor = get().albumNextCursor;
|
const state = get();
|
||||||
if (!cursor || get().isPageLoading) return;
|
const cursor = state.albumNextCursor;
|
||||||
set({ isPageLoading: true });
|
if (!cursor || state.isPageLoading) return;
|
||||||
|
const sort = state.albumSort;
|
||||||
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
|
const pageGeneration = pageGenerations.albums;
|
||||||
|
const loading = beginLoading();
|
||||||
try {
|
try {
|
||||||
const page = await readAlbumPage(cursor);
|
const page = await readAlbumPage(cursor, sort, includeSingles);
|
||||||
if (page.error === 'STALE_REVISION') return resetAlbums();
|
if (
|
||||||
|
pageGeneration !== pageGenerations.albums ||
|
||||||
|
get().albumSort !== sort ||
|
||||||
|
useSettingsStore.getState().includeSingles !== includeSingles ||
|
||||||
|
get().albumNextCursor !== cursor
|
||||||
|
) return;
|
||||||
|
if (page.error === 'STALE_REVISION') {
|
||||||
|
await resetAlbums();
|
||||||
|
return;
|
||||||
|
}
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
albums: appendWindow(state.albums, page.items, (album) => album.identity_key),
|
albums: appendWindow(state.albums, page.items, (album) => album.identity_key),
|
||||||
albumNextCursor: page.nextCursor,
|
albumNextCursor: page.nextCursor,
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
set({ isPageLoading: false });
|
finishLoading(loading);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
loadNextArtists: async () => {
|
loadNextArtists: async () => {
|
||||||
const cursor = get().artistNextCursor;
|
const state = get();
|
||||||
if (!cursor || get().isPageLoading) return;
|
const cursor = state.artistNextCursor;
|
||||||
set({ isPageLoading: true });
|
if (!cursor || state.isPageLoading) return;
|
||||||
|
const sort = state.artistSort;
|
||||||
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
|
const includeCollaborations = state.includeCollabArtists;
|
||||||
|
const pageGeneration = pageGenerations.artists;
|
||||||
|
const loading = beginLoading();
|
||||||
try {
|
try {
|
||||||
const page = await readArtistPage(cursor);
|
const page = await readArtistPage(cursor, sort, groupingMode, includeCollaborations);
|
||||||
if (page.error === 'STALE_REVISION') return resetArtists();
|
if (
|
||||||
|
pageGeneration !== pageGenerations.artists ||
|
||||||
|
get().artistSort !== sort ||
|
||||||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
|
get().includeCollabArtists !== includeCollaborations ||
|
||||||
|
get().artistNextCursor !== cursor
|
||||||
|
) return;
|
||||||
|
if (page.error === 'STALE_REVISION') {
|
||||||
|
await resetArtists();
|
||||||
|
return;
|
||||||
|
}
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
artists: appendWindow(state.artists, page.items, (artist) => artist.artist),
|
artists: appendWindow(state.artists, page.items, (artist) => artist.artist),
|
||||||
artistNextCursor: page.nextCursor,
|
artistNextCursor: page.nextCursor,
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
set({ isPageLoading: false });
|
finishLoading(loading);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
jumpToSection: async (cursor) => {
|
jumpToSection: async (cursor) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
set({ isPageLoading: true });
|
const viewMode = state.viewMode;
|
||||||
|
if (viewMode !== 'tracks' && viewMode !== 'albums' && viewMode !== 'artists') return false;
|
||||||
|
const generation = ++pageGenerations[viewMode];
|
||||||
|
const loading = beginLoading();
|
||||||
try {
|
try {
|
||||||
if (state.viewMode === 'tracks') {
|
if (viewMode === 'tracks') {
|
||||||
const page = await readTrackPage(cursor);
|
const sort = state.trackSort;
|
||||||
if (page.error === 'STALE_REVISION') return resetTracks();
|
const page = await readTrackPage(cursor, sort);
|
||||||
set({
|
if (
|
||||||
|
generation !== pageGenerations.tracks ||
|
||||||
|
get().viewMode !== viewMode ||
|
||||||
|
get().trackSort !== sort
|
||||||
|
) return false;
|
||||||
|
if (page.error === 'STALE_REVISION') {
|
||||||
|
await resetTracks();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (page.items.length === 0) {
|
||||||
|
void resetSectionAnchors();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
set((current) => ({
|
||||||
tracks: page.items,
|
tracks: page.items,
|
||||||
trackNextCursor: page.nextCursor,
|
trackNextCursor: page.nextCursor,
|
||||||
totalTrackCount: page.totalCount,
|
totalTrackCount: page.totalCount,
|
||||||
});
|
sectionJumpRevision: current.sectionJumpRevision + 1,
|
||||||
} else if (state.viewMode === 'albums') {
|
}));
|
||||||
const page = await readAlbumPage(cursor);
|
} else if (viewMode === 'albums') {
|
||||||
if (page.error === 'STALE_REVISION') return resetAlbums();
|
const sort = state.albumSort;
|
||||||
set({ albums: page.items, albumNextCursor: page.nextCursor });
|
const includeSingles = useSettingsStore.getState().includeSingles;
|
||||||
} else if (state.viewMode === 'artists') {
|
const page = await readAlbumPage(cursor, sort, includeSingles);
|
||||||
const page = await readArtistPage(cursor);
|
if (
|
||||||
if (page.error === 'STALE_REVISION') return resetArtists();
|
generation !== pageGenerations.albums ||
|
||||||
set({ artists: page.items, artistNextCursor: page.nextCursor });
|
get().viewMode !== viewMode ||
|
||||||
|
get().albumSort !== sort ||
|
||||||
|
useSettingsStore.getState().includeSingles !== includeSingles
|
||||||
|
) return false;
|
||||||
|
if (page.error === 'STALE_REVISION') {
|
||||||
|
await resetAlbums();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
if (page.items.length === 0) {
|
||||||
|
void resetSectionAnchors();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
set((current) => ({
|
||||||
|
albums: page.items,
|
||||||
|
albumNextCursor: page.nextCursor,
|
||||||
|
sectionJumpRevision: current.sectionJumpRevision + 1,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const sort = state.artistSort;
|
||||||
|
const groupingMode = useSettingsStore.getState().artistGroupingMode;
|
||||||
|
const includeCollaborations = state.includeCollabArtists;
|
||||||
|
const page = await readArtistPage(
|
||||||
|
cursor,
|
||||||
|
sort,
|
||||||
|
groupingMode,
|
||||||
|
includeCollaborations,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
generation !== pageGenerations.artists ||
|
||||||
|
get().viewMode !== viewMode ||
|
||||||
|
get().artistSort !== sort ||
|
||||||
|
useSettingsStore.getState().artistGroupingMode !== groupingMode ||
|
||||||
|
get().includeCollabArtists !== includeCollaborations
|
||||||
|
) return false;
|
||||||
|
if (page.error === 'STALE_REVISION') {
|
||||||
|
await resetArtists();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (page.items.length === 0) {
|
||||||
|
void resetSectionAnchors();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
set((current) => ({
|
||||||
|
artists: page.items,
|
||||||
|
artistNextCursor: page.nextCursor,
|
||||||
|
sectionJumpRevision: current.sectionJumpRevision + 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
set({ isPageLoading: false });
|
finishLoading(loading);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -449,7 +667,8 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setViewMode: (viewMode) => {
|
setViewMode: (viewMode) => {
|
||||||
set({ viewMode });
|
anchorGeneration += 1;
|
||||||
|
set({ viewMode, sectionAnchors: [] });
|
||||||
persistSetting(VIEW_MODE_KEY, viewMode);
|
persistSetting(VIEW_MODE_KEY, viewMode);
|
||||||
if (viewMode === 'tracks' && get().tracks.length === 0) void resetTracks();
|
if (viewMode === 'tracks' && get().tracks.length === 0) void resetTracks();
|
||||||
if (viewMode === 'albums' && get().albums.length === 0) void resetAlbums();
|
if (viewMode === 'albums' && get().albums.length === 0) void resetAlbums();
|
||||||
@@ -458,28 +677,37 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setTrackSort: (trackSort) => {
|
setTrackSort: (trackSort) => {
|
||||||
set({ trackSort, tracks: [], trackNextCursor: null });
|
anchorGeneration += 1;
|
||||||
|
set({ trackSort, tracks: [], trackNextCursor: null, sectionAnchors: [] });
|
||||||
persistSetting(TRACK_SORT_KEY, trackSort);
|
persistSetting(TRACK_SORT_KEY, trackSort);
|
||||||
void resetTracks();
|
void resetTracks();
|
||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
},
|
},
|
||||||
|
|
||||||
setAlbumSort: (albumSort) => {
|
setAlbumSort: (albumSort) => {
|
||||||
set({ albumSort, albums: [], albumNextCursor: null });
|
anchorGeneration += 1;
|
||||||
|
set({ albumSort, albums: [], albumNextCursor: null, sectionAnchors: [] });
|
||||||
persistSetting(ALBUM_SORT_KEY, albumSort);
|
persistSetting(ALBUM_SORT_KEY, albumSort);
|
||||||
void resetAlbums();
|
void resetAlbums();
|
||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
},
|
},
|
||||||
|
|
||||||
setArtistSort: (artistSort) => {
|
setArtistSort: (artistSort) => {
|
||||||
set({ artistSort, artists: [], artistNextCursor: null });
|
anchorGeneration += 1;
|
||||||
|
set({ artistSort, artists: [], artistNextCursor: null, sectionAnchors: [] });
|
||||||
persistSetting(ARTIST_SORT_KEY, artistSort);
|
persistSetting(ARTIST_SORT_KEY, artistSort);
|
||||||
void resetArtists();
|
void resetArtists();
|
||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
},
|
},
|
||||||
|
|
||||||
setIncludeCollabArtists: (includeCollabArtists) => {
|
setIncludeCollabArtists: (includeCollabArtists) => {
|
||||||
set({ includeCollabArtists, artists: [], artistNextCursor: null });
|
anchorGeneration += 1;
|
||||||
|
set({
|
||||||
|
includeCollabArtists,
|
||||||
|
artists: [],
|
||||||
|
artistNextCursor: null,
|
||||||
|
sectionAnchors: [],
|
||||||
|
});
|
||||||
persistSetting(INCLUDE_COLLAB_ARTISTS_KEY, includeCollabArtists ? 'true' : 'false');
|
persistSetting(INCLUDE_COLLAB_ARTISTS_KEY, includeCollabArtists ? 'true' : 'false');
|
||||||
void resetArtists();
|
void resetArtists();
|
||||||
void resetSectionAnchors();
|
void resetSectionAnchors();
|
||||||
|
|||||||
Reference in New Issue
Block a user