Skip to content

Commit 674ad36

Browse files
committed
fix: improve add to collection messages and make them localizable; show syncing message when adding to or deleting from a remote collection; use colored snackbar in all details screens
#1968
1 parent 7296fd2 commit 674ad36

File tree

28 files changed

+442
-264
lines changed

28 files changed

+442
-264
lines changed

data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/collection/CollectionRepositoryImpl.kt

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import ly.david.musicsearch.data.database.dao.CollectionEntityDao
1111
import ly.david.musicsearch.data.musicbrainz.api.CollectionApi
1212
import ly.david.musicsearch.data.repository.internal.paging.BrowseEntityRemoteMediator
1313
import ly.david.musicsearch.shared.domain.browse.BrowseRemoteMetadataRepository
14-
import ly.david.musicsearch.shared.domain.collection.CollectionFeedback
1514
import ly.david.musicsearch.shared.domain.collection.CollectionRepository
1615
import ly.david.musicsearch.shared.domain.collection.CollectionSortOption
16+
import ly.david.musicsearch.shared.domain.collection.EditACollectionFeedback
1717
import ly.david.musicsearch.shared.domain.error.Action
1818
import ly.david.musicsearch.shared.domain.error.ActionableResult
1919
import ly.david.musicsearch.shared.domain.error.ErrorResolution
@@ -31,6 +31,7 @@ class CollectionRepositoryImpl(
3131
private val collectionEntityDao: CollectionEntityDao,
3232
private val browseRemoteMetadataDao: BrowseRemoteMetadataDao,
3333
private val browseEntityCountRepository: BrowseRemoteMetadataRepository,
34+
private val clock: Clock,
3435
) : CollectionRepository {
3536

3637
@OptIn(ExperimentalPagingApi::class)
@@ -126,14 +127,14 @@ class CollectionRepositoryImpl(
126127
override fun markDeletedFromCollection(
127128
collection: CollectionListItemModel,
128129
collectableIds: List<String>,
129-
): Flow<Feedback<CollectionFeedback>> = flow {
130+
): Flow<Feedback<EditACollectionFeedback>> = flow {
130131
collectionEntityDao.markDeletedFromCollection(
131132
collectionId = collection.id,
132133
collectableIds = collectableIds,
133134
)
134135
emit(
135136
Feedback.Actionable(
136-
data = CollectionFeedback.Deleting(
137+
data = EditACollectionFeedback.Deleting(
137138
count = collectableIds.size,
138139
collectionName = collection.name,
139140
),
@@ -148,11 +149,11 @@ class CollectionRepositoryImpl(
148149

149150
override suspend fun deleteFromCollection(
150151
collection: CollectionListItemModel,
151-
): Flow<Feedback<CollectionFeedback>> = flow {
152-
emit(Feedback.Loading(CollectionFeedback.Loading))
153-
152+
): Flow<Feedback<EditACollectionFeedback>> = flow {
154153
val idsMarkedForDeletion = collectionEntityDao.getIdsMarkedForDeletionFromCollection(collection.id)
154+
155155
if (collection.isRemote) {
156+
emit(Feedback.Loading(EditACollectionFeedback.SyncingWithMusicBrainz))
156157
try {
157158
// TODO: handle deleting more than 400 items at a time
158159
// https://musicbrainz.org/doc/MusicBrainz_API#collections
@@ -164,7 +165,7 @@ class CollectionRepositoryImpl(
164165
} catch (ex: HandledException) {
165166
emit(
166167
Feedback.Error(
167-
data = CollectionFeedback.Failed(
168+
data = EditACollectionFeedback.FailedToDelete(
168169
collectionName = collection.name,
169170
errorMessage = ex.userMessage,
170171
),
@@ -179,7 +180,7 @@ class CollectionRepositoryImpl(
179180
collectionEntityDao.deleteFromCollection(collectionId = collection.id)
180181
emit(
181182
Feedback.Success(
182-
data = CollectionFeedback.Deleted(
183+
data = EditACollectionFeedback.Deleted(
183184
count = idsMarkedForDeletion.size,
184185
collectionName = collection.name,
185186
),
@@ -191,11 +192,26 @@ class CollectionRepositoryImpl(
191192
collectionId: String,
192193
entityType: MusicBrainzEntityType,
193194
entityIds: List<String>,
194-
): ActionableResult {
195-
val collection = collectionDao.getCollection(collectionId) ?: return ActionableResult("Does not exist")
195+
): Flow<Feedback<EditACollectionFeedback>> = flow {
196+
val collection = collectionDao.getCollection(collectionId)
197+
if (collection == null) {
198+
emit(
199+
Feedback.Error(
200+
data = EditACollectionFeedback.DoesNotExist,
201+
errorResolution = ErrorResolution.None,
202+
time = clock.now(),
203+
),
204+
)
205+
return@flow
206+
}
196207

197-
var result = ActionableResult()
198208
if (collection.isRemote) {
209+
emit(
210+
Feedback.Loading(
211+
data = EditACollectionFeedback.SyncingWithMusicBrainz,
212+
time = clock.now(),
213+
),
214+
)
199215
try {
200216
// TODO: support adding more than 16KB worth of items at a time
201217
collectionApi.addToCollection(
@@ -204,31 +220,65 @@ class CollectionRepositoryImpl(
204220
mbids = entityIds,
205221
)
206222
} catch (ex: HandledException) {
207-
val userFacingError = "Failed to add to ${collection.name}. ${ex.userMessage}"
208-
return ActionableResult(
209-
message = userFacingError,
210-
action = Action.Login.takeIf { ex.errorResolution == ErrorResolution.Login },
211-
errorResolution = ex.errorResolution,
223+
emit(
224+
Feedback.Error(
225+
data = EditACollectionFeedback.FailedToAdd(
226+
collectionName = collection.name,
227+
errorMessage = ex.userMessage,
228+
),
229+
action = Action.Login.takeIf { ex.errorResolution == ErrorResolution.Login },
230+
errorResolution = ex.errorResolution,
231+
time = clock.now(),
232+
),
212233
)
234+
return@flow
213235
}
214236
}
215237

216-
collectionEntityDao.withTransaction {
217-
val insertions = collectionEntityDao.addAllToCollection(
218-
collectionId = collectionId,
219-
entityIds = entityIds.toList(),
238+
val feedback: EditACollectionFeedback = insertAndGetResult(
239+
collectionId = collectionId,
240+
entityIds = entityIds,
241+
collection = collection,
242+
)
243+
emit(
244+
Feedback.Success(
245+
data = feedback,
246+
time = clock.now(),
247+
),
248+
)
249+
}
250+
251+
private fun insertAndGetResult(
252+
collectionId: String,
253+
entityIds: List<String>,
254+
collection: CollectionListItemModel,
255+
): EditACollectionFeedback {
256+
val insertions = collectionEntityDao.addAllToCollection(
257+
collectionId = collectionId,
258+
entityIds = entityIds,
259+
).toInt()
260+
261+
val data: EditACollectionFeedback = when {
262+
insertions == 0 -> EditACollectionFeedback.AlreadyIn(
263+
collectionName = collection.name,
220264
)
221265

222-
result = ActionableResult(
223-
message = when {
224-
insertions == 0L -> "Already in ${collection.name}."
225-
entityIds.size == 1 -> "Added to ${collection.name}."
226-
else -> "Added ${insertions.toInt()} to ${collection.name}."
227-
},
266+
entityIds.size == 1 -> EditACollectionFeedback.AddedOne(
267+
collectionName = collection.name,
228268
)
229-
}
230269

231-
return result
270+
insertions == entityIds.size -> EditACollectionFeedback.AddedMultiple(
271+
newInsertions = insertions,
272+
collectionName = collection.name,
273+
)
274+
275+
else -> EditACollectionFeedback.AddedMultipleSomeAlreadyAdded(
276+
newInsertions = insertions,
277+
collectionName = collection.name,
278+
countAlreadyAdded = entityIds.size - insertions,
279+
)
280+
}
281+
return data
232282
}
233283

234284
override fun markDeletedCollections(

data/repository/src/jvmTest/kotlin/ly/david/musicsearch/data/repository/collection/CollectionRepositoryImplTest.kt

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
77
import kotlinx.coroutines.test.runTest
88
import ly.david.data.test.KoinTestRule
99
import ly.david.data.test.api.FakeCollectionApi
10+
import ly.david.data.test.clock.FixedClock
1011
import ly.david.data.test.zutomayoArtistMusicBrainzNetworkModel
1112
import ly.david.musicsearch.data.database.dao.BrowseRemoteMetadataDao
1213
import ly.david.musicsearch.data.database.dao.CollectionDao
@@ -18,13 +19,16 @@ import ly.david.musicsearch.data.musicbrainz.models.relation.SerializableMusicBr
1819
import ly.david.musicsearch.data.repository.BrowseRemoteMetadataRepositoryImpl
1920
import ly.david.musicsearch.shared.domain.collection.CollectionRepository
2021
import ly.david.musicsearch.shared.domain.collection.CollectionSortOption
22+
import ly.david.musicsearch.shared.domain.collection.EditACollectionFeedback
23+
import ly.david.musicsearch.shared.domain.error.Feedback
2124
import ly.david.musicsearch.shared.domain.listitem.CollectionListItemModel
2225
import ly.david.musicsearch.shared.domain.network.MusicBrainzEntityType
2326
import org.junit.Assert
2427
import org.junit.Rule
2528
import org.junit.Test
2629
import org.koin.test.KoinTest
2730
import org.koin.test.inject
31+
import kotlin.time.Instant
2832

2933
private const val NEW_COLLECTION_ID = "f3fff548-8282-4c9a-9cea-0e2af40029fe"
3034

@@ -37,6 +41,8 @@ class CollectionRepositoryImplTest : KoinTest {
3741
private val collectionEntityDao: CollectionEntityDao by inject()
3842
private val browseRemoteMetadataDao: BrowseRemoteMetadataDao by inject()
3943

44+
private val now = Instant.parse("1970-01-02T05:00:00Z")
45+
4046
private fun createRepository(
4147
collectionApi: CollectionApi,
4248
): CollectionRepository {
@@ -48,6 +54,7 @@ class CollectionRepositoryImplTest : KoinTest {
4854
browseEntityCountRepository = BrowseRemoteMetadataRepositoryImpl(
4955
browseRemoteMetadataDao = browseRemoteMetadataDao,
5056
),
57+
clock = FixedClock(now = now),
5158
)
5259
}
5360

@@ -543,7 +550,9 @@ class CollectionRepositoryImplTest : KoinTest {
543550
}
544551

545552
@Test
546-
fun `observe entity is part of a collection`() = runTest {
553+
fun `observe entity is part of a remote collection`() = runTest {
554+
val collectionName = "Artists"
555+
547556
val repository = createRepository(
548557
collectionApi = object : FakeCollectionApi() {
549558
override suspend fun browseCollectionsByUser(
@@ -558,7 +567,7 @@ class CollectionRepositoryImplTest : KoinTest {
558567
musicBrainzModels = listOf(
559568
CollectionMusicBrainzNetworkModel(
560569
id = "1",
561-
name = "Artists",
570+
name = collectionName,
562571
entityType = SerializableMusicBrainzEntity.ARTIST,
563572
),
564573
),
@@ -578,7 +587,7 @@ class CollectionRepositoryImplTest : KoinTest {
578587
listOf(
579588
CollectionListItemModel(
580589
id = "1",
581-
name = "Artists",
590+
name = collectionName,
582591
entity = MusicBrainzEntityType.ARTIST,
583592
isRemote = true,
584593
),
@@ -595,7 +604,23 @@ class CollectionRepositoryImplTest : KoinTest {
595604
collectionId = "1",
596605
entityType = MusicBrainzEntityType.ARTIST,
597606
entityIds = listOf(entityId),
598-
)
607+
).test {
608+
Assert.assertEquals(
609+
Feedback.Loading(
610+
data = EditACollectionFeedback.SyncingWithMusicBrainz,
611+
time = now,
612+
),
613+
awaitItem(),
614+
)
615+
Assert.assertEquals(
616+
Feedback.Success(
617+
data = EditACollectionFeedback.AddedOne(collectionName = collectionName),
618+
time = now,
619+
),
620+
awaitItem(),
621+
)
622+
awaitComplete()
623+
}
599624
Assert.assertEquals(true, awaitItem())
600625
}
601626
}

shared/domain/src/commonMain/kotlin/ly/david/musicsearch/shared/domain/collection/CollectionRepository.kt

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,21 @@ interface CollectionRepository {
3131
fun markDeletedFromCollection(
3232
collection: CollectionListItemModel,
3333
collectableIds: List<String>,
34-
): Flow<Feedback<CollectionFeedback>>
34+
): Flow<Feedback<EditACollectionFeedback>>
3535

3636
fun unMarkDeletedFromCollection(
3737
collectionId: String,
3838
)
3939

4040
suspend fun deleteFromCollection(
4141
collection: CollectionListItemModel,
42-
): Flow<Feedback<CollectionFeedback>>
42+
): Flow<Feedback<EditACollectionFeedback>>
4343

4444
suspend fun addToCollection(
4545
collectionId: String,
4646
entityType: MusicBrainzEntityType,
4747
entityIds: List<String>,
48-
): ActionableResult
48+
): Flow<Feedback<EditACollectionFeedback>>
4949

5050
fun markDeletedCollections(
5151
collectionIds: List<String>,
@@ -61,9 +61,43 @@ interface CollectionRepository {
6161
}
6262

6363
@Parcelize
64-
sealed interface CollectionFeedback : CommonParcelable {
65-
data object Loading : CollectionFeedback
66-
data class Deleting(val count: Int, val collectionName: String) : CollectionFeedback
67-
data class Deleted(val count: Int, val collectionName: String) : CollectionFeedback
68-
data class Failed(val collectionName: String, val errorMessage: String) : CollectionFeedback
64+
sealed interface EditACollectionFeedback : CommonParcelable {
65+
data object SyncingWithMusicBrainz : EditACollectionFeedback
66+
67+
data class Deleting(
68+
val count: Int,
69+
val collectionName: String,
70+
) : EditACollectionFeedback
71+
72+
data class Deleted(
73+
val count: Int,
74+
val collectionName: String,
75+
) : EditACollectionFeedback
76+
77+
data class FailedToDelete(
78+
val collectionName: String,
79+
val errorMessage: String,
80+
) : EditACollectionFeedback
81+
82+
data class FailedToAdd(
83+
val collectionName: String,
84+
val errorMessage: String,
85+
) : EditACollectionFeedback
86+
87+
data object DoesNotExist : EditACollectionFeedback
88+
89+
data class AlreadyIn(val collectionName: String) : EditACollectionFeedback
90+
91+
data class AddedOne(val collectionName: String) : EditACollectionFeedback
92+
93+
data class AddedMultiple(
94+
val newInsertions: Int,
95+
val collectionName: String,
96+
) : EditACollectionFeedback
97+
98+
data class AddedMultipleSomeAlreadyAdded(
99+
val newInsertions: Int,
100+
val collectionName: String,
101+
val countAlreadyAdded: Int,
102+
) : EditACollectionFeedback
69103
}

0 commit comments

Comments
 (0)