diff --git a/config/detekt.yml b/config/detekt.yml index cee9db0a4..aa3156a5d 100644 --- a/config/detekt.yml +++ b/config/detekt.yml @@ -8,6 +8,9 @@ complexity: - "Composable" TooManyFunctions: active: false +exceptions: + TooGenericExceptionCaught: + active: false formatting: ImportOrdering: active: false diff --git a/data/database/config/baseline.xml b/data/database/config/baseline.xml index e5bc929a5..b51355e95 100644 --- a/data/database/config/baseline.xml +++ b/data/database/config/baseline.xml @@ -8,7 +8,5 @@ ReturnCount:ToRelationDatabaseModel.kt$fun RelationMusicBrainzModel.toRelationDatabaseModel( entityId: String, order: Int, ): RelationWithOrder? SwallowedException:ArtistCreditDao.kt$ArtistCreditDao$ex: Exception SwallowedException:CollectionEntityDao.kt$CollectionEntityDao$ex: Exception - TooGenericExceptionCaught:ArtistCreditDao.kt$ArtistCreditDao$ex: Exception - TooGenericExceptionCaught:CollectionEntityDao.kt$CollectionEntityDao$ex: Exception diff --git a/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/ArtistDao.kt b/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/ArtistDao.kt index 3f96be163..573d08fbd 100644 --- a/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/ArtistDao.kt +++ b/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/ArtistDao.kt @@ -46,6 +46,10 @@ class ArtistDao( ).executeAsOneOrNull() } + fun delete(artistId: String) { + transacter.delete(artistId) + } + private fun toArtistScaffoldModel( id: String, name: String, diff --git a/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/RelationDao.kt b/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/RelationDao.kt index 0c7e9f365..9cbd6285c 100644 --- a/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/RelationDao.kt +++ b/data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/RelationDao.kt @@ -63,6 +63,12 @@ class RelationDao( }, ) + fun deleteRelationshipsExcludingUrlsByEntity( + entityId: String, + ) { + transacter.deleteRelationshipsExcludingUrlsByEntity(entityId) + } + fun getEntityUrlRelationships( entityId: String, ): List { @@ -73,6 +79,12 @@ class RelationDao( ).executeAsList() } + fun deleteUrlRelationshipsByEntity( + entityId: String, + ) { + transacter.deleteUrlRelationshipssByEntity(entityId) + } + private fun toRelationListItemModel( linkedEntityId: String, linkedEntity: MusicBrainzEntity, @@ -93,12 +105,6 @@ class RelationDao( additionalInfo = additionalInfo, ) - fun deleteRelationshipsExcludingUrlsByEntity( - entityId: String, - ) { - transacter.deleteRelationshipsExcludingUrlsByEntity(entityId) - } - fun getCountOfEachRelationshipType(entityId: String): Flow> = transacter.countOfEachRelationshipType(entityId) .asFlow() diff --git a/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/artist.sq b/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/artist.sq index 5765e8502..6ec0f35db 100644 --- a/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/artist.sq +++ b/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/artist.sq @@ -34,3 +34,7 @@ SELECT FROM artist a LEFT JOIN mbid_image mi ON mi.mbid = a.id WHERE id = :artistId; + +delete: +DELETE FROM artist +WHERE id = :artistId; diff --git a/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/relation.sq b/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/relation.sq index ddbbc9dbc..a5d3a8e64 100644 --- a/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/relation.sq +++ b/data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/relation.sq @@ -66,6 +66,10 @@ attributes LIKE :query OR additional_info LIKE :query) ORDER BY linked_entity, label, `order` LIMIT :limit OFFSET :offset; +deleteRelationshipsExcludingUrlsByEntity: +DELETE FROM relation +WHERE entity_id = :entityId AND linked_entity != "url"; + getEntityUrlRelationships: SELECT `linked_entity_id`, @@ -82,9 +86,9 @@ WHERE entity_id = :entityId AND linked_entity = "url" AND attributes LIKE :query OR additional_info LIKE :query) ORDER BY linked_entity, label, `order`; -deleteRelationshipsExcludingUrlsByEntity: +deleteUrlRelationshipssByEntity: DELETE FROM relation -WHERE entity_id = :entityId AND linked_entity != "url"; +WHERE entity_id = :entityId AND linked_entity = "url"; countOfEachRelationshipType: SELECT linked_entity, count(entity_id) AS entity_count diff --git a/data/musicbrainz/config/baseline.xml b/data/musicbrainz/config/baseline.xml index bf416f8b1..2487b2112 100644 --- a/data/musicbrainz/config/baseline.xml +++ b/data/musicbrainz/config/baseline.xml @@ -9,6 +9,5 @@ MaxLineLength:RelationshipHeaders.kt$"7fd5fbc0-fbf4-4d04-be23-417d50a4dc30" to ("holds phonographic copyright (℗) for" to "phonographic copyright (℗) by") MaxLineLength:RelationshipHeaders.kt$"fc399d47-23a7-4c28-bfcf-0607a562b644" to ("transliterated/translated track listings" to "transliterated/translated track listing of") MaxLineLength:RelationshipHeaders.kt$"fd841726-ba3c-47f7-af8e-6734ab6243ff" to ("holds phonographic copyright (℗) for" to "phonographic copyright (℗) by") - TooGenericExceptionCaught:Logout.kt$Logout$ex: Exception diff --git a/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/RelationRepositoryImpl.kt b/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/RelationRepositoryImpl.kt index 52ac690be..5c9eaa4da 100644 --- a/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/RelationRepositoryImpl.kt +++ b/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/RelationRepositoryImpl.kt @@ -201,6 +201,10 @@ class RelationRepositoryImpl( entityId = entityId, ) + override fun deleteUrlRelationshipsByEntity(entityId: String) { + relationDao.deleteUrlRelationshipsByEntity(entityId) + } + override fun getCountOfEachRelationshipType(entityId: String): Flow> = relationDao.getCountOfEachRelationshipType(entityId).map { it.map { countOfEachRelationshipType: CountOfEachRelationshipType -> diff --git a/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/artist/ArtistRepositoryImpl.kt b/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/artist/ArtistRepositoryImpl.kt index 604a2cd42..fef08b3d3 100644 --- a/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/artist/ArtistRepositoryImpl.kt +++ b/data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/artist/ArtistRepositoryImpl.kt @@ -1,9 +1,9 @@ package ly.david.musicsearch.data.repository.artist -import ly.david.musicsearch.data.musicbrainz.models.core.ArtistMusicBrainzModel -import ly.david.musicsearch.data.musicbrainz.api.MusicBrainzApi import ly.david.musicsearch.core.models.artist.ArtistScaffoldModel import ly.david.musicsearch.data.database.dao.ArtistDao +import ly.david.musicsearch.data.musicbrainz.api.MusicBrainzApi +import ly.david.musicsearch.data.musicbrainz.models.core.ArtistMusicBrainzModel import ly.david.musicsearch.data.repository.internal.toRelationWithOrderList import ly.david.musicsearch.domain.artist.ArtistRepository import ly.david.musicsearch.domain.relation.RelationRepository @@ -14,11 +14,23 @@ class ArtistRepositoryImpl( private val relationRepository: RelationRepository, ) : ArtistRepository { - override suspend fun lookupArtist(artistId: String): ArtistScaffoldModel { + override suspend fun lookupArtist( + artistId: String, + forceRefresh: Boolean, + ): ArtistScaffoldModel { + if (forceRefresh) { + relationRepository.deleteUrlRelationshipsByEntity(artistId) + artistDao.delete(artistId) + } + val artistScaffoldModel = artistDao.getArtistForDetails(artistId) val urlRelations = relationRepository.getEntityUrlRelationships(artistId) val hasUrlsBeenSavedForEntity = relationRepository.hasUrlsBeenSavedFor(artistId) - if (artistScaffoldModel != null && hasUrlsBeenSavedForEntity) { + if ( + artistScaffoldModel != null && + hasUrlsBeenSavedForEntity && + !forceRefresh + ) { return artistScaffoldModel.copy( urls = urlRelations, ) @@ -26,7 +38,10 @@ class ArtistRepositoryImpl( val artistMusicBrainzModel = musicBrainzApi.lookupArtist(artistId) cache(artistMusicBrainzModel) - return lookupArtist(artistId) + return lookupArtist( + artistId = artistId, + forceRefresh = false, + ) } private fun cache(artist: ArtistMusicBrainzModel) { diff --git a/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/artist/ArtistRepository.kt b/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/artist/ArtistRepository.kt index a58867943..9c48ce2ae 100644 --- a/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/artist/ArtistRepository.kt +++ b/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/artist/ArtistRepository.kt @@ -3,5 +3,8 @@ package ly.david.musicsearch.domain.artist import ly.david.musicsearch.core.models.artist.ArtistScaffoldModel interface ArtistRepository { - suspend fun lookupArtist(artistId: String): ArtistScaffoldModel + suspend fun lookupArtist( + artistId: String, + forceRefresh: Boolean, + ): ArtistScaffoldModel } diff --git a/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/relation/RelationRepository.kt b/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/relation/RelationRepository.kt index 4861b300f..b05ac1032 100644 --- a/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/relation/RelationRepository.kt +++ b/domain/src/commonMain/kotlin/ly/david/musicsearch/domain/relation/RelationRepository.kt @@ -30,5 +30,9 @@ interface RelationRepository { entityId: String, ): List + fun deleteUrlRelationshipsByEntity( + entityId: String, + ) + fun getCountOfEachRelationshipType(entityId: String): Flow> } diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/area/AreaUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/area/AreaUi.kt index 097fbae91..449e43036 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/area/AreaUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/area/AreaUi.kt @@ -170,18 +170,18 @@ internal fun AreaUi( when (state.tabs[page]) { AreaTab.DETAILS -> { DetailsWithErrorHandling( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(AreaUiEvent.ForceRefresh) }, scaffoldModel = state.area, ) { AreaDetailsUi( area = it, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistPresenter.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistPresenter.kt index d2e628d7f..2bfe92584 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistPresenter.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistPresenter.kt @@ -19,7 +19,6 @@ import ly.david.musicsearch.core.models.artist.ArtistScaffoldModel import ly.david.musicsearch.core.models.getNameWithDisambiguation import ly.david.musicsearch.core.models.history.LookupHistory import ly.david.musicsearch.core.models.network.MusicBrainzEntity -import ly.david.musicsearch.data.common.network.RecoverableNetworkException import ly.david.musicsearch.data.spotify.ArtistImageRepository import ly.david.musicsearch.domain.artist.ArtistRepository import ly.david.musicsearch.domain.history.usecase.IncrementLookupHistory @@ -61,6 +60,7 @@ internal class ArtistPresenter( @Composable override fun present(): ArtistUiState { var title by rememberSaveable { mutableStateOf(screen.title.orEmpty()) } + var isLoading by rememberSaveable { mutableStateOf(true) } var isError by rememberSaveable { mutableStateOf(false) } var recordedHistory by rememberSaveable { mutableStateOf(false) } var query by rememberSaveable { mutableStateOf("") } @@ -87,14 +87,18 @@ internal class ArtistPresenter( LaunchedEffect(forceRefreshDetails) { try { - val artistScaffoldModel = repository.lookupArtist(screen.id) + isLoading = true + val artistScaffoldModel = repository.lookupArtist( + artistId = screen.id, + forceRefresh = forceRefreshDetails, + ) if (title.isEmpty()) { title = artistScaffoldModel.getNameWithDisambiguation() } artist = artistScaffoldModel imageUrl = fetchArtistImage(artistScaffoldModel) isError = false - } catch (ex: RecoverableNetworkException) { + } catch (ex: Exception) { logger.e(ex) isError = true } @@ -109,6 +113,8 @@ internal class ArtistPresenter( ) recordedHistory = true } + isLoading = false + forceRefreshDetails = false } LaunchedEffect( @@ -221,6 +227,7 @@ internal class ArtistPresenter( return ArtistUiState( title = title, + isLoading = isLoading, isError = isError, artist = artist, imageUrl = imageUrl, @@ -257,6 +264,7 @@ internal class ArtistPresenter( @Stable internal data class ArtistUiState( val title: String, + val isLoading: Boolean, val isError: Boolean, val artist: ArtistScaffoldModel?, val imageUrl: String, diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistUi.kt index 774b9a4c4..c1adccd20 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/artist/ArtistUi.kt @@ -152,19 +152,19 @@ internal fun ArtistUi( when (state.tabs[page]) { ArtistTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + showLoading = state.isLoading, showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(ArtistUiEvent.ForceRefresh) }, scaffoldModel = state.artist, ) { artist -> ArtistDetailsUi( artist = artist, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, imageUrl = state.imageUrl, lazyListState = detailsLazyListState, diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/event/EventUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/event/EventUi.kt index e3126b5ce..067b98130 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/event/EventUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/event/EventUi.kt @@ -118,19 +118,18 @@ internal fun EventUi( when (state.tabs[page]) { EventTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(EventUiEvent.ForceRefresh) }, scaffoldModel = state.event, ) { event -> EventDetailsUi( event = event, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/genre/GenreUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/genre/GenreUi.kt index 9d92ac937..040608409 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/genre/GenreUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/genre/GenreUi.kt @@ -68,11 +68,12 @@ internal fun GenreUi( }, ) { innerPadding -> DetailsWithErrorHandling( + modifier = Modifier.padding(innerPadding), showError = isError, - onRetryClick = onRetryClick, + onRefresh = onRetryClick, scaffoldModel = genre, ) { - FullScreenContent(modifier = Modifier.padding(innerPadding)) { + FullScreenContent { Text( modifier = Modifier.padding(16.dp), textAlign = TextAlign.Center, diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/instrument/InstrumentUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/instrument/InstrumentUi.kt index 3f4ef657c..dd1f61845 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/instrument/InstrumentUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/instrument/InstrumentUi.kt @@ -118,19 +118,18 @@ internal fun InstrumentUi( when (state.tabs[page]) { InstrumentTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(InstrumentUiEvent.ForceRefresh) }, scaffoldModel = state.instrument, ) { instrument -> InstrumentDetailsUi( instrument = instrument, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/label/LabelUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/label/LabelUi.kt index 1441fac15..d415a52e3 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/label/LabelUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/label/LabelUi.kt @@ -131,19 +131,18 @@ internal fun LabelUi( when (state.tabs[page]) { LabelTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(LabelUiEvent.ForceRefresh) }, scaffoldModel = state.label, ) { label -> LabelDetailsUi( label = label, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/place/PlaceUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/place/PlaceUi.kt index 3921364e7..30f5b3879 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/place/PlaceUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/place/PlaceUi.kt @@ -115,19 +115,18 @@ internal fun PlaceUi( when (state.tabs[page]) { PlaceTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(PlaceUiEvent.ForceRefresh) }, scaffoldModel = state.place, ) { place -> PlaceDetailsUi( place = place, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/recording/RecordingUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/recording/RecordingUi.kt index e8d0bd98d..381cbb67c 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/recording/RecordingUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/recording/RecordingUi.kt @@ -153,19 +153,18 @@ internal fun RecordingUi( when (state.tabs[page]) { RecordingTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(RecordingUiEvent.ForceRefresh) }, scaffoldModel = state.recording, ) { recording -> RecordingDetailsUi( recording = recording, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/release/ReleaseUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/release/ReleaseUi.kt index 730586505..78160dfa5 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/release/ReleaseUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/release/ReleaseUi.kt @@ -154,19 +154,18 @@ internal fun ReleaseUi( when (state.tabs[page]) { ReleaseTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(ReleaseUiEvent.ForceRefresh) }, scaffoldModel = state.release, ) { release -> ReleaseDetailsUi( release = release, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, imageUrl = state.imageUrl, lazyListState = detailsLazyListState, diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/releasegroup/ReleaseGroupUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/releasegroup/ReleaseGroupUi.kt index 1092d0fb9..9b102a150 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/releasegroup/ReleaseGroupUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/releasegroup/ReleaseGroupUi.kt @@ -160,19 +160,18 @@ internal fun ReleaseGroupUi( when (state.tabs[page]) { ReleaseGroupTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(ReleaseGroupUiEvent.ForceRefresh) }, scaffoldModel = state.releaseGroup, ) { releaseGroup -> ReleaseGroupDetailsUi( releaseGroup = releaseGroup, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, imageUrl = state.imageUrl, lazyListState = detailsLazyListState, diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/series/SeriesUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/series/SeriesUi.kt index 16099e32c..fb6bb8c24 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/series/SeriesUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/series/SeriesUi.kt @@ -118,19 +118,18 @@ internal fun SeriesUi( when (state.tabs[page]) { SeriesTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(SeriesUiEvent.ForceRefresh) }, scaffoldModel = state.series, ) { series -> SeriesDetailsUi( series = series, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/work/WorkUi.kt b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/work/WorkUi.kt index 3ab6e3fa3..152f6cb10 100644 --- a/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/work/WorkUi.kt +++ b/shared/feature/details/src/commonMain/kotlin/ly/david/musicsearch/shared/feature/details/work/WorkUi.kt @@ -117,19 +117,18 @@ internal fun WorkUi( when (state.tabs[page]) { WorkTab.DETAILS -> { DetailsWithErrorHandling( - modifier = Modifier.padding(innerPadding), + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), showError = state.isError, - onRetryClick = { + onRefresh = { eventSink(WorkUiEvent.ForceRefresh) }, scaffoldModel = state.work, ) { work -> WorkDetailsUi( work = work, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), filterText = state.query, lazyListState = detailsLazyListState, onItemClick = { entity, id, title -> diff --git a/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NIGHT].png b/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NIGHT].png index c161cffe8..741f55156 100644 --- a/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NIGHT].png +++ b/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NIGHT].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:087b8ee70f655fef1bfce6274d70fd65af3ca5c5d70e2f681e0b02a8cb09b163 -size 26005 +oid sha256:6c16288326235f57c02d34f04f5084d9e75f81ca433b837ee8907be5529b5e29 +size 26098 diff --git a/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NOTNIGHT].png b/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NOTNIGHT].png index 29ed24af5..5cc1e1e45 100644 --- a/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NOTNIGHT].png +++ b/shared/feature/details/src/test/snapshots/images/ly.david.musicsearch.shared.feature.details.area_AreaUiTest_detailsError[NOTNIGHT].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a93e41c88a0d5e75c1b1d00496d3ffff293bd59ed56a610cf776841d7de4e00 -size 26231 +oid sha256:833bd5dc002ab5730cdbf2db1db11705edb0f3ebb22a9bfea518ca8d1f9a0b9d +size 26452 diff --git a/ui/common/config/baseline.xml b/ui/common/config/baseline.xml index 1c42077f1..0727ee58d 100644 --- a/ui/common/config/baseline.xml +++ b/ui/common/config/baseline.xml @@ -3,8 +3,6 @@ MagicNumber:DotsFlashing.kt$4 - TooGenericExceptionCaught:ReleaseGroupsByEntityPresenter.kt$ReleaseGroupsByEntityPresenter$ex: Exception - TooGenericExceptionCaught:ReleasesByEntityPresenter.kt$ReleasesByEntityPresenter$ex: Exception UnstableCollections:ExposedDropdownMenuBox.kt$List<MusicBrainzEntity> UnstableCollections:MultipleChoiceDialog.kt$List<String> UnstableCollections:TabsBar.kt$List<String> diff --git a/ui/common/src/commonMain/kotlin/ly/david/ui/common/fullscreen/DetailsWithErrorHandling.kt b/ui/common/src/commonMain/kotlin/ly/david/ui/common/fullscreen/DetailsWithErrorHandling.kt index ef084a7dc..6284e032e 100644 --- a/ui/common/src/commonMain/kotlin/ly/david/ui/common/fullscreen/DetailsWithErrorHandling.kt +++ b/ui/common/src/commonMain/kotlin/ly/david/ui/common/fullscreen/DetailsWithErrorHandling.kt @@ -1,32 +1,57 @@ package ly.david.ui.common.fullscreen +import androidx.compose.foundation.layout.Box +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier /** * For displaying a [detailsScreen], showing a loading indicator when [scaffoldModel] is null, - * handling errors when [showError], and delegating retry with [onRetryClick]. + * handling errors when [showError], and delegating retry with [onRefresh]. + * Supports pull to refresh, delegating to [onRefresh]. */ +@OptIn(ExperimentalMaterialApi::class) @Composable fun DetailsWithErrorHandling( - showError: Boolean, - onRetryClick: () -> Unit, scaffoldModel: T?, + onRefresh: () -> Unit, modifier: Modifier = Modifier, + showLoading: Boolean = false, + showError: Boolean = false, detailsScreen: @Composable ((T) -> Unit), ) { - when { - showError -> { - FullScreenErrorWithRetry( - modifier = modifier, - onClick = onRetryClick, - ) - } - scaffoldModel == null -> { - FullScreenLoadingIndicator(modifier = modifier) - } - else -> { - detailsScreen(scaffoldModel) + val refreshState = rememberPullRefreshState( + refreshing = showLoading, + onRefresh = { onRefresh() }, + ) + Box( + modifier = modifier.pullRefresh(refreshState), + ) { + when { + showError -> { + FullScreenErrorWithRetry( + onClick = onRefresh, + ) + } + + scaffoldModel == null -> { + FullScreenLoadingIndicator() + } + + else -> { + detailsScreen(scaffoldModel) + + PullRefreshIndicator( + refreshing = showLoading, + state = refreshState, + modifier = Modifier + .align(Alignment.TopCenter), + ) + } } } }